29 tasks across 7 phases (schema+backend, building blocks, detail sheet, form dialog, table+pager+list view, project page, i18n). Each task is a single commit, manual smoke verify in dev (no test suite).
2938 lines
93 KiB
Markdown
2938 lines
93 KiB
Markdown
# Task UX Evolution — Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Spec:** [`2026-05-08-task-ux-evolution-design.md`](./2026-05-08-task-ux-evolution-design.md) (commit `3104103`)
|
||
|
||
**Goal:** Ship a paginated shadcn Table list view for tasks with shared pagination, a right-side detail Sheet with attachments, a redesigned quick-capture create/edit dialog, and reuse the same list view on the project detail page (replacing the current Kanban board there).
|
||
|
||
**Architecture:** New shared `TaskListView` orchestrator owns toolbar + table/grid + pager state. Tasks page and project detail page both render it (project page uses `hideProjectColumn`). Detail moves from `Dialog` to `Sheet` with three fixed regions (sticky header, scrolling body, sticky composer). Attachments are copied into Electron `userData/attachments/<taskId>/` and managed by a new `taskAttachments` tRPC sub-router.
|
||
|
||
**Tech stack:** React 19, TanStack Router, shadcn/ui (Table, Sheet, ContextMenu, DropdownMenu, Popover, Calendar), Tailwind 4, lucide-react, TypeScript, tRPC v11, Drizzle ORM + better-sqlite3, Electron `dialog` + `shell` + `fs/promises`, react-i18next.
|
||
|
||
**Test infrastructure note:** adiuvAI has no automated test suite. Verification per task uses `npm run lint` (must pass) and a manual smoke check in `npm start` (dev with hot reload). Lessons-learned slots are filled during execution.
|
||
|
||
**Repo note:** All paths below are relative to the `adiuvAI/` submodule root unless otherwise stated. Commit each task in the submodule. The plan file itself lives in the workspace root `docs/`.
|
||
|
||
**Discovered detail (not in original spec):** the project detail page currently renders a `KanbanBoard`, not a list of TaskRows. Replacing it with `TaskListView` removes the Kanban board entirely (intended per user choice "Replace + hide Project col"). `KanbanBoard.tsx` is only referenced by `ProjectDetail.tsx`, so it gets deleted.
|
||
|
||
---
|
||
|
||
## Conventions
|
||
|
||
- Each task ends with a single commit. Commit messages: `feat: …` for new features, `chore: …` for housekeeping, `refactor: …` for in-place restructure, `fix: …` for bugfixes.
|
||
- Lint runs from the `adiuvAI/` submodule: `cd adiuvAI && npm run lint`.
|
||
- Dev server: `cd adiuvAI && source ~/.nvm/nvm.sh && npm start` on macOS/Linux, `cd adiuvAI; npm start` on Windows.
|
||
- `git add` lists explicit files (no `git add -A`).
|
||
- Path alias `@/*` resolves to `src/renderer/*`.
|
||
|
||
---
|
||
|
||
## Phase A — Schema & backend
|
||
|
||
### Task 1: Add `estimate` column and `taskAttachments` table to schema
|
||
|
||
**Files:**
|
||
- Modify: `adiuvAI/src/main/db/schema.ts`
|
||
|
||
**Steps:**
|
||
|
||
- [ ] **Step 1.1:** Add `estimate` to the `tasks` table definition (after `dueDate`):
|
||
|
||
```ts
|
||
export const tasks = sqliteTable('tasks', {
|
||
id: text('id').primaryKey(),
|
||
projectId: text('project_id'),
|
||
title: text('title').notNull(),
|
||
description: text('description'),
|
||
status: text('status').notNull().default('todo'),
|
||
priority: text('priority').notNull().default('medium'),
|
||
assignee: text('assignee'),
|
||
dueDate: integer('due_date', { mode: 'number' }),
|
||
estimate: integer('estimate', { mode: 'number' }), // minutes, nullable
|
||
isAiSuggested: integer('is_ai_suggested', { mode: 'number' }).notNull().default(0),
|
||
createdAt: integer('created_at', { mode: 'number' }).notNull(),
|
||
completedAt: integer('completed_at', { mode: 'number' }),
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 1.2:** Append the `taskAttachments` table definition after `taskComments`:
|
||
|
||
```ts
|
||
export const taskAttachments = sqliteTable('task_attachments', {
|
||
id: text('id').primaryKey(),
|
||
taskId: text('task_id').notNull(),
|
||
filename: text('filename').notNull(),
|
||
mimeType: text('mime_type'),
|
||
sizeBytes: integer('size_bytes', { mode: 'number' }).notNull(),
|
||
storedPath: text('stored_path').notNull(), // relative to userData/attachments
|
||
createdAt: integer('created_at', { mode: 'number' }).notNull(),
|
||
});
|
||
|
||
export type TaskAttachment = InferSelectModel<typeof taskAttachments>;
|
||
export type NewTaskAttachment = InferInsertModel<typeof taskAttachments>;
|
||
```
|
||
|
||
- [ ] **Step 1.3:** Generate the migration:
|
||
|
||
```bash
|
||
cd adiuvAI
|
||
npx drizzle-kit generate
|
||
```
|
||
|
||
Expected: a new file under `src/main/db/migrations/` containing `ALTER TABLE tasks ADD COLUMN estimate` and `CREATE TABLE task_attachments`.
|
||
|
||
- [ ] **Step 1.4:** Apply the migration locally to verify:
|
||
|
||
```bash
|
||
npx drizzle-kit push
|
||
```
|
||
|
||
- [ ] **Step 1.5:** Run lint:
|
||
|
||
```bash
|
||
npm run lint
|
||
```
|
||
|
||
Expected: passes.
|
||
|
||
- [ ] **Step 1.6:** Commit:
|
||
|
||
```bash
|
||
git add src/main/db/schema.ts src/main/db/migrations/
|
||
git commit -m "feat: add tasks.estimate column and task_attachments table"
|
||
```
|
||
|
||
**Lessons learned:** _(filled during execution)_
|
||
|
||
---
|
||
|
||
### Task 2: Attachments storage helper module
|
||
|
||
**Files:**
|
||
- Create: `adiuvAI/src/main/attachments/storage.ts`
|
||
|
||
**Steps:**
|
||
|
||
- [ ] **Step 2.1:** Create `src/main/attachments/storage.ts`:
|
||
|
||
```ts
|
||
import { app } from 'electron';
|
||
import { promises as fs } from 'node:fs';
|
||
import path from 'node:path';
|
||
import { randomUUID } from 'node:crypto';
|
||
|
||
const FILENAME_MAX = 200;
|
||
|
||
function sanitizeFilename(name: string): string {
|
||
const stripped = name
|
||
.replace(/[\\/]/g, '_')
|
||
.replace(/[ |