# Task UX Evolution — Design

**Date:** 2026-05-08
**Scope:** adiuvAI desktop app (renderer + main process)
**Status:** Approved by user, ready for implementation plan

## Goal

Evolve the task management UX:

1. Replace the list-of-cards view with a paginated **shadcn Table**, keep the card grid as an alternative view, share pagination across both.
2. Replace `TaskDetailDialog` with a **right-side `Sheet`** (sticky header + scrolling body + sticky composer), add **attachments** support.
3. Redesign the create dialog as a **quick-capture form** with pill-style property controls. Edit dialog reuses the same shell.
4. Apply the same task list view inside the **project detail page**, scoped to that project.

Single spec, single implementation plan. All four subsystems ship together.

## Non-goals

- AI-driven estimate generation (column added now, populated by a future agent).
- Comment attachments (composer has no attach icon).
- Per-column header sorting in the table (existing `Order by` Select stays).
- Reporter / Tags fields (image reference includes them but spec excludes).

## 1. Architecture & shared state

A new `TaskListView` component owns the task list rendering for both the Tasks page and the Project detail page. It encapsulates the toolbar, the table or grid body, and the pager. The page consuming it passes a task array plus an optional `hideProjectColumn` flag.

**Persisted state (`localStorage`):**
- `tasksViewMode`: `'list' | 'grid'` (already exists, kept).
- `tasksPageSize`: `10 | 25 | 50 | 100` — default `25`.

Page index is component-local and resets per route entry. It also resets when the search, status filter, or `Order by` changes.

**Pagination scope:** Tasks page and Project page each maintain their own page state. Toggling list ↔ grid within a page preserves the current page.

**Why client-side slicing:** task list is already fully loaded via `trpc.tasks.list`. No backend pagination required at this scale.

## 2. Database schema

Two changes to `src/main/db/schema.ts`:

```ts
// tasks: add column
estimate: integer('estimate'),  // minutes, nullable

// new table
export const taskAttachments = sqliteTable('task_attachments', {
  id: text('id').primaryKey().$defaultFn(() => randomUUID()),
  taskId: text('task_id').notNull(),
  filename: text('filename').notNull(),
  mimeType: text('mime_type'),
  sizeBytes: integer('size_bytes').notNull(),
  storedPath: text('stored_path').notNull(),  // relative to userData/attachments
  createdAt: integer('created_at').notNull(),
});
```

Migration generated with `drizzle-kit generate`. Per project convention, no foreign key constraint — cascade is handled in the `tasks.delete` tRPC procedure (delete attachment files + rows before deleting the task).

## 3. Attachments — file storage and IPC

**Storage path:** `app.getPath('userData') / attachments / <taskId> / <uuid>-<sanitizedFilename>`.

**Sanitization:** strip path separators, control characters, leading dots; cap filename at 200 chars.

**Limits:**
- Soft cap 50 MB per file. Larger files trigger a warning toast and are not uploaded.
- No per-task total cap.

**New tRPC sub-router `taskAttachments`** (in `src/main/router/index.ts`):

| Procedure | Input | Behavior |
|---|---|---|
| `list` | `{ taskId }` | Returns attachment rows for the task. |
| `pick` | `{}` | Main: `dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'] })`. Returns `Array<{ path, name, size }>`. |
| `create` | `{ taskId, sourcePath, filename, sizeBytes, mimeType? }` | Main: `fs.mkdir(userData/attachments/<taskId>, recursive)`, copy file with new uuid name, insert row. |
| `delete` | `{ id }` | Look up row, `fs.unlink(storedPath)`, delete row. |
| `open` | `{ id }` | `shell.openPath(absoluteStoredPath)`. |

**Helper module `src/main/attachments/storage.ts`:** path resolution, sanitize, copy, delete. Keeps tRPC procedures thin.

**Tasks router updates:**
- `tasks.update` accepts `estimate?: number | null`.
- `tasks.delete` enumerates `taskAttachments` for the task and deletes files + rows before deleting the task row.

## 4. Table view

**Component:** `TaskTable` using shadcn `Table` / `TableHeader` / `TableBody` / `TableRow` / `TableCell`.

**Container styling (translucent card over gradient bg):**
```
bg-card/65  backdrop-blur-xl  border border-border/50  rounded-lg  shadow
```
The `--card` token is used (not a hard-coded color), so dark mode works.

**Columns:**
1. **Task** — title, single line, truncate with tooltip.
2. **Project** — `Client › Project` breadcrumb. Client text muted, project text foreground. Hidden when `hideProjectColumn` is set. Click navigates to the project page.
3. **Priority** — existing `<PriorityBadge>` component (arrow icon + colored text, no pill).
4. **Due** — `formatDueDate(t.dueDate, prefs)`. Overdue: red text. None: muted `—`.
5. **Assignee** — `<AssigneeStack>`: overlapping avatars (max 2 visible), `+N` chip if more, tooltip listing all. None: muted `—`.

**Row interaction:**
- Click row → opens `TaskDetailSheet`.
- Right-click / context menu (kept from current `TaskRow` behavior): **Edit**, **Delete**, **Change status →** submenu (To Do / In Progress / Done with checkmark on current).

**Sorting:** existing `Order by` Select in the toolbar remains the only sort control. No per-column header sort.

**Empty state:** existing `<Empty>` component spans all columns when the filtered list is empty.

## 5. Pagination

**Component:** `TaskPager` rendered in its own translucent card box below the list/grid (same style tokens as the table card, separate box).

**Layout:**

```
┌──────────────────────────────────────────────────────────────────┐
│  Showing 1–25 of 312 tasks    Rows per page: [25 ▾]              │
│                                  ‹ 1 2 3 4 5 … 13 ›              │
└──────────────────────────────────────────────────────────────────┘
```

**Behavior:**
- Page-number window: always include first, last, current. Up to 7 buttons total. Ellipsis when the gap is greater than 1.
- `ResizeObserver` on the pager → reduce visible buttons on narrow widths (7 → 5 → 3 → just prev/next).
- Page-size change resets `pageIndex` to 0.
- If filters trim the total below `pageIndex * pageSize`, snap `pageIndex` to the last valid page.
- Pager renders **for both list and grid views**, identically.

## 6. Detail sheet

**Replaces:** `TaskDetailDialog.tsx` → new `TaskDetailSheet.tsx` using shadcn `Sheet`, right side, width ~480 px.

**Three fixed regions:**

```
┌─────────────────────────────────┐  ← STICKY HEADER
│  Acme › Communications          │     breadcrumb (small, muted)
│  Draft Q2 investor update email │     title (18 px, semibold)
│  [↑ High priority] [● In progress]    chip row
│                          [⋯]    │     overflow menu (Edit, Delete)
├─────────────────────────────────┤
│                                 │  ← SCROLLING BODY
│  ┌─ Properties card ──────────┐ │
│  │ Assignee  | Due            │ │
│  │ Estimate  | Created        │ │
│  │ Files: [chip] [chip] [+Add]│ │
│  └────────────────────────────┘ │
│                                 │
│  Description                    │
│  <body text>                    │
│                                 │
│  ── separator ──                │
│                                 │
│  Comments · 4                   │
│  <comment list, no inner scroll>│
│                                 │
├─────────────────────────────────┤  ← STICKY COMPOSER
│  [👤]  ┌──────────────┐  [↑]   │
│        │ Write comment│         │
│        └──────────────┘         │
└─────────────────────────────────┘
```

**Header chips:**
- Priority: existing `<PriorityBadge>` (arrow + colored text).
- Status: pill using existing `STATUS_CONFIG` colors.
- Both are clickable → popover to change.

**Properties card** (translucent inner box, 2-column grid):
- Assignee, Due, Estimate, Created — each row is small uppercase label + value.
- Estimate shows muted `—` until the AI agent ships.
- Files row spans both columns: horizontal chip strip. Each chip: `📎 filename · sizeKB ×`. `+ Add` is a dashed pill that triggers `taskAttachments.pick`.
  - Click chip filename → `taskAttachments.open`.
  - Click × → confirm + `taskAttachments.delete`.

**Comments:** no inner ScrollArea — they scroll with the body.

**Composer (sticky bottom):** reuses the home / AI input wrapper styling:

```
rounded-2xl  bg-background/70  backdrop-blur-xl
border border-border/50  shadow-lg  ring-1 ring-border/20
focus-within:shadow-xl  focus-within:border-ring/50
```

Internally reuses the existing `ChatInputBox` component via a new `'comment'` variant (auto-grow textarea + `ArrowUp` send button, draft persistence, `⌘ + Enter` submit). **No attach icon in the composer.**

**Edit / Delete:** moved into the header overflow menu. The previous footer action bar is removed.

## 7. Create / Edit dialog

**Components:**
- `TaskFormDialog` — shared shell, props: `mode: 'create' | 'edit'`, initial values, `onSubmit`.
- `NewTaskDialog` and `EditTaskDialog` become thin wrappers (different default values + mutation).

**Layout:**

```
┌─ New task ─────────────────── ⌘+Enter to create ─┐
│                                                  │
│  What needs to be done?           ← 22 px input  │
│  Add a description…               ← textarea    │
│                                                  │
│  PROPERTIES                                      │
│  [📁 Project: Acme › Communications]             │
│  [↑ Priority: High]  [● Status: To Do]           │
│  [📅 Due: Apr 30, 2026]                          │
│  [+ Add assignees]   ← dashed (empty)            │
│                                                  │
│ ────────────────────────────────────────────────│
│  [📎] *                  [Cancel] [Create task]  │
└──────────────────────────────────────────────────┘
```

`*` 📎 icon-pill is **only visible in Edit mode**. Attachments are blocked in Create mode (need a `taskId`); the user saves first, then attaches via the detail sheet or via Edit.

**Pill states:**
- Set: `bg-card/70`, solid border, label + value visible.
- Empty: dashed border, muted text.

**Pill click → field-specific shadcn `Popover`:**
- Project — Select w/ inline create flow (existing logic preserved: project + client + sub-client).
- Priority — three-option select (high / medium / low).
- Status — three-option select.
- Due — Calendar + optional hour/minute selectors.
- Assignees — existing Popover (known-assignees list + add-new).

**Keyboard:** `⌘ / Ctrl + Enter` submits. Title `autoFocus`.

## 8. Project page integration

**File:** `src/renderer/routes/projects.$projectId.tsx`.

The existing tasks tab content is replaced by `<TaskListView projectId={...} hideProjectColumn />`. The toolbar (status tabs, search, `Order by`, view toggle, **New task** button) is identical to the Tasks page. Pagination state is local to this page (separate from the Tasks page state). Clicking a row opens the same `TaskDetailSheet`.

No changes to other project tabs (overview, notes, timeline).

## 9. Files

**New files:**

```
src/renderer/components/tasks/
  TaskListView.tsx       — shared toolbar + table/grid + pager
  TaskTable.tsx          — shadcn Table renderer
  TaskTableRow.tsx       — single row + context menu
  TaskPager.tsx          — pagination card box
  TaskDetailSheet.tsx    — right-side Sheet replacing TaskDetailDialog
  TaskFormDialog.tsx     — shared shell for create/edit
  TaskAttachmentChip.tsx — file chip
  AssigneeStack.tsx      — overlapping avatars + overflow chip
  StatusBadge.tsx        — status pill (existing STATUS_CONFIG colors)

src/main/
  attachments/storage.ts — path resolution, sanitize, copy, delete helpers
```

**Modified files:**

```
src/renderer/routes/tasks.tsx                       — render <TaskListView>
src/renderer/routes/projects.$projectId.tsx        — tasks tab uses <TaskListView hideProjectColumn>
src/renderer/components/tasks/NewTaskDialog.tsx    — wrapper around TaskFormDialog (mode='create')
src/renderer/components/tasks/EditTaskDialog.tsx   — wrapper around TaskFormDialog (mode='edit')
src/main/db/schema.ts                               — tasks.estimate, taskAttachments
src/main/db/migrations/                             — new generated migration
src/main/router/index.ts                            — taskAttachments router; tasks.update accepts estimate; tasks.delete cascades attachments
```

**Deleted files:**

```
src/renderer/components/tasks/TaskDetailDialog.tsx  — replaced by TaskDetailSheet
src/renderer/components/tasks/TaskRow.tsx           — replaced by TaskTableRow (TaskCard kept for grid)
```

## 10. i18n keys

New keys in the `tasks.*` namespace, added to all five language files (en, it, es, fr, de):

```
tasks.colTask, tasks.colProject, tasks.colPriority, tasks.colDue, tasks.colAssignee
tasks.rowsPerPage, tasks.showingNofM (plural-aware), tasks.noAssignees
tasks.estimate, tasks.attachments, tasks.addFile, tasks.removeFile, tasks.fileTooLarge
tasks.changeStatus, tasks.properties
tasks.confirmDeleteAttachment
```

## 11. Out-of-scope follow-ups

- AI agent that generates `estimate` for a task.
- Comment attachments.
- Per-column sort in the table.
- Backend pagination (only needed if task counts grow much larger).

## 12. Implementation order (suggested)

A natural shipping order; the implementation plan can refine:

1. DB migration (estimate column + taskAttachments table).
2. Main-process attachments storage module + tRPC sub-router.
3. `TaskDetailSheet` with attachment UI (deletes the old dialog).
4. `TaskFormDialog` shared shell; rewire `NewTaskDialog` / `EditTaskDialog`.
5. `TaskListView`, `TaskTable`, `TaskPager`, `AssigneeStack`, `StatusBadge`. Wire into Tasks page.
6. Wire `TaskListView` into project detail page with `hideProjectColumn`.
7. i18n keys for all five languages.
