next-steps.md30.5 KBView on GitHub
# Next-Step Tasks — Date-Grouped Block Components

> **Scope:** This doc designs the **reusable block-list components** (`TaskBlock`, `TaskBlockList`, `groupTasksByDate`) plus the standalone `NextStepsCard`. The six-tab conversation list is **Inbox · Next Steps · Agents · Files · CRM · Contacts** (see [overview.md](./overview.md)). These components mount on three surfaces:
> 1. **Per-conversation Next Steps tab** — `TaskBlockList` fed by `conversationData.data.userTasks`, scoped to one conversation.
> 2. **Pinned next-step card** at the top of the Inbox tab's `SlackTimeline` — `NextStepsCard` only (wired in [timeline.md](./timeline.md)).
> 3. **Global Agenda view** in the Conversations page's sidebar — `TaskBlockList` fed by a cross-conversation `userTasks.listAssignedToMe` query spanning **all** of the user's conversations (wired in [conversations-page.md](./conversations-page.md)).
>
> The grouping/rendering logic is identical across surfaces (1) and (3); the only difference is the data source. Surface (2) is just the pinned-card section, not the full block list.

## 1) Introduction — goal, present state, future state

The Next Steps tab today is the TipTap-backed `ConversationAgendaDocument` rendered as the body of the `TASK_AGGREGATOR` agent row — a document that mixes server-derived `dateHeading` / `agendaTask` nodes with the pinned next-step text described in [conversation-agenda-next-step-node.md](../conversation-agenda-next-step-node.md). We want to keep the pinned `NextStepsCard` at the top but replace the agenda-document task list with a date-grouped list of self-contained **task blocks**. Each block shows the task title/body plus an inline **Delete** + **Open draft** (or **Create draft**) button pair, and groups are headed by `Overdue` / `Today` / `Tomorrow` / `This week` / `Next week` / weekly headers (`Week of Mar 23`) / `No date` / `Completed`.

## 2) Present state

### 2.1 Architecture diagram

```text
ConversationBodyLayout (variant='view')
  └── AgentRow stack
        └── AgentRow [TASK_AGGREGATOR]                       (Phase 2 of overview.md extracts this body)
              └── ConversationAgendaDocument                 ← TipTap + Y.Doc
                    ├── nextStepNode             (pinned, edits crm_conversations.nextSteps)
                    ├── scheduledMeetingsNode    (pinned, read-only)
                    ├── dateHeading              (per-day, server-seeded)
                    └── agendaTask*              (one TipTap node per user_task)
                          (DocumentSaveHook → INSERT/UPDATE/DELETE on user_tasks)

ConversationView body, when expandedSections['__timeline']
  └── PastEventsTimeline
        └── TimelineTaskItem  ← per-task row used in the timeline view
              (Delete + Create draft hover affordances, due-date badge)
```

### 2.2 Step-by-step walkthrough

1. **Aggregator row** — `AgentRow` at [AgentRow.tsx:311-320](apps/mail/modules/conversations/components/AgentRow.tsx) renders the `TASK_AGGREGATOR` body as a single `<ConversationAgendaDocument conversationId aopAgentId />`. The row's right-hand badge is a `TaskCountBadge` whose count comes from the inline filter at [AgentRow.tsx:689-696](apps/mail/modules/conversations/components/AgentRow.tsx):
   ```ts
   const isTaskAgent = agent.name === SYSTEM_AGENT_NAMES.TASK_AGGREGATOR;
   const dueTaskCount = isTaskAgent
     ? (conversationData.data.userTasks ?? []).filter((t) => {
         if (t.status !== 'todo') return false;
         if (t.completedAt) return false;
         return t.dueDate ? new Date(t.dueDate) <= new Date() : false;
       }).length
     : 0;
   ```
2. **Agenda doc fetch** — `ConversationAgendaDocument` at [ConversationAgendaDocument.tsx:75](apps/mail/modules/agentCanvas/components/ConversationAgendaDocument.tsx) calls `documents.getDoc({ documentType: 'conversation_agenda', path })`; server seeds + reconciles via `buildConversationAgendaJson` / `reconcileConversationAgendaYDoc` at [conversation-agenda-reconciler.ts:105-444](apps/server/src/services/agenda/conversation-agenda-reconciler.ts).
3. **Pinned next-step** — the top of the doc is a `NextStepsCard`-equivalent set of TipTap nodes (`nextStepNode`, `scheduledMeetingsNode`) injected by the server reconciler; standalone `NextStepsCard` lives at [NextStepsCard.tsx:112](apps/mail/modules/conversations/components/timeline/NextStepsCard.tsx) and is what overview.md Phase 2 promotes into the new `nextSteps` tab.
4. **Tasks data path** — every consumer reads from `conversationData.data.userTasks` (`ConversationUserTask[]`). Type at [crm/types/index.ts:893-920](apps/mail/modules/crm/types/index.ts):
   ```ts
   interface ConversationUserTask {
     id: string;
     description: string | null;
     status: 'todo' | 'done' | 'deleted' | 'agent_deleted';
     dueDate: Date | null;
     taskChannel: 'email' | 'slack' | 'multi-action';
     taskType: 'response' | 'follow-up' | 'post-meeting' | 'pre-meeting' | 'reactivation' | 'manual' | null;
     taskCreatedBy: 'agent' | 'user' | null;
     taskActionData: ConversationTaskActionData | null;
     agentExecutionEnabled: boolean;
     executionRunId: string | null;
     completedAt: Date | null;
     // …
   }
   type ConversationTaskActionData =
     | { channel: 'email'; threadId: string; draftId?: string; emailHeaderMessageId?: string }
     | { channel: 'slack'; channelId: string; channelName?: string; workspaceId?: string; threadTs?: string; message?: string }
     | { channel: 'calendar'; eventId: string; calendarId: string; htmlLink?: string; startTime?: string };
   ```
   The `userTasks` array is hydrated by `crm.getConversation` ([apps/server/src/services/crm/conversations.ts:3120](apps/server/src/services/crm/conversations.ts)).
5. **DB row** — `user_tasks` defined at [aop-schema.ts:827](apps/server/src/db/aop-schema.ts). `taskActionData` is a `jsonb` column; **there is no `taskId` foreign key on a drafts table** — drafts live in Gmail (and Slack `taskActionData.message`), and the back-reference is `task.taskActionData.draftId`.
6. **Current per-task UI** — `TimelineTaskItem` at [TimelineTaskItem.tsx:45](apps/mail/modules/conversations/components/timeline/TimelineTaskItem.tsx) is the closest existing task block. It already:
   - Reads `hasDraft` from `task.taskActionData?.channel === 'email' && !!task.taskActionData.draftId` at [TimelineTaskItem.tsx:86-88](apps/mail/modules/conversations/components/timeline/TimelineTaskItem.tsx).
   - On click of the row when `hasDraft`, calls `selectThreadId(threadId)` + `setIsThreadOpen(true)` ([TimelineTaskItem.tsx:158-166](apps/mail/modules/conversations/components/timeline/TimelineTaskItem.tsx)) to open the email thread / draft.
   - On the "Create draft" badge, calls `optimisticExecuteTaskNow(taskId, conversationId)` at [TimelineTaskItem.tsx:180-193](apps/mail/modules/conversations/components/timeline/TimelineTaskItem.tsx) (Slack-channel and CRM-opportunity tasks have their own affordances).
7. **Optimistic actions** — `useOptimisticTaskActions` at [use-optimistic-task-actions.ts:36](apps/mail/modules/userTasks/hooks/use-optimistic-task-actions.ts) wraps the server mutations: `optimisticDeleteTask` (`trpc.userTasks.deleteTask`), `optimisticCompleteTaskDelayed` (`trpc.userTasks.completeTask`), `optimisticUpdateTaskDescription` / `optimisticUpdateTaskDueDate` (`trpc.userTasks.updateTask`), `optimisticExecuteTaskNow` (`trpc.agentExecutions.executeTaskNow`), `sendSlackDraftFromTask` (`trpc.integrations.slack.sendMessage` + `completeTask`).
8. **tRPC surface** — task mutations live in [user-tasks.ts:286-1755](apps/server/src/trpc/routes/user-tasks.ts): `listUserTasks`, `createTask`, `updateTask`, `completeTask`, `markTaskAsRead`, `updateTaskStatus`, `updateTaskLabel`, `updateTaskDueDate`, `deleteTask`, `deleteTasks`, `applyFieldChange`. `executeTaskNow` (which is what creates the draft for a task) is at [agent-executions.ts:667](apps/server/src/trpc/routes/agent-executions.ts) → `executeTaskNowDirect` ([apps/server/src/lib/scheduling/task-now.ts](apps/server/src/lib/scheduling/task-now.ts)), and writes the resulting `draftId` back onto `user_tasks.task_action_data`. Email drafts themselves are managed via [drafts.ts:35](apps/server/src/trpc/routes/drafts.ts) (`drafts.create / get / delete / send`) against the Gmail provider.
9. **No `Draft` table** — verified: there is no Cedar-side drafts table. A draft is identified by `(threadId, draftId)` in Gmail, or by `taskActionData.message` for Slack, or by `taskActionData.eventId` for calendar. Any "open draft for task" entry point therefore reads `task.taskActionData` and routes accordingly; there is no `drafts.taskId` foreign key to add.

## 3) Designed state

### 3.1 Architecture diagram

```text
ConversationTabBody [tab='nextSteps']                       (six-tab shell from overview.md §3)
  └── TaskBlockList                                         ← NEW: scroll region (per-conversation tasks)
        ├── group header  "Overdue"      (red)
        │     └── TaskBlock × n
        ├── group header  "Today"
        │     └── TaskBlock × n          (rendered even when empty? no — skip if empty)
        ├── group header  "Tomorrow"
        │     └── TaskBlock × n
        ├── group header  "This week"
        │     └── TaskBlock × n
        ├── group header  "Next week"
        │     └── TaskBlock × n
        ├── group header  "Week of Mar 23"   (weekly headers for anything later)
        │     └── TaskBlock × n
        ├── group header  "No date"
        │     └── TaskBlock × n
        └── ──── ▾ Show 12 completed tasks ────       ← toggle; expanded:
              group header  "Completed"
              └── TaskBlock × n (dimmed)

groupTasksByDate(tasks, now, tz)  →  Group[]                ← NEW: pure helper
```

Task block sketch:

```text
┌─────────────────────────────────────────────────────────────────────┐
│ ○  Reply to Acme procurement question                               │   ← row 1: checkbox + title
│    Draft ready — references pricing v3                              │   ← row 2: optional one-line subtitle
│                                                                     │
│                                          [ Delete ]   [ Open draft ]│   ← row 3: action pair, right-aligned
└─────────────────────────────────────────────────────────────────────┘
```

### 3.2 Step-by-step walkthrough

1. **Next Steps tab body** — the `nextSteps` case of `ConversationTabBody` (from overview.md §3) renders:
   ```tsx
   <div className="flex flex-col h-full">
     <TaskBlockList tasks={conversationData.data.userTasks ?? []} conversationId={conversationData.data.conversation.id} />
   </div>
   ```
   `NextStepsCard` is **not** stacked above the list here — that pinned card lives at the top of `SlackTimeline` in the Inbox tab (see [timeline.md](./timeline.md)). The Next Steps tab is just the date-grouped task list.
2. **`TaskBlockList`** at [apps/mail/modules/conversations/components/nextSteps/TaskBlockList.tsx](apps/mail/modules/conversations/components/nextSteps/TaskBlockList.tsx) — scroll region (`flex-1 overflow-auto`). Pipeline:
   ```ts
   const visibleTasks = tasks.filter((t) => t.status === 'todo' || t.status === 'done');
   const [showCompleted, setShowCompleted] = useState(false);
   const groups = useMemo(
     () => groupTasksByDate(visibleTasks, new Date(), browserTimezone, { showCompleted }),
     [visibleTasks, showCompleted],
   );
   ```
   Renders one `<TaskBlockGroup label … />` per non-empty group; empty groups are skipped. The `Completed` group is always last and is gated by the toggle.
3. **`TaskBlock`** at [apps/mail/modules/conversations/components/nextSteps/TaskBlock.tsx](apps/mail/modules/conversations/components/nextSteps/TaskBlock.tsx) — one bordered card per task. Props `{ task: ConversationUserTask; conversationId: string }`. Layout:
   - **Row 1**: animated checkbox (reuse `AnimatedCheckmark` from [TimelineTaskItem.tsx:1-3,447-453](apps/mail/modules/conversations/components/timeline/TimelineTaskItem.tsx)) + inline-editable title. Click title → swap to `Textarea` (same pattern as [TimelineTaskItem.tsx:470-481](apps/mail/modules/conversations/components/timeline/TimelineTaskItem.tsx)); blur calls `optimisticUpdateTaskDescription`.
   - **Row 2 (optional)**: derived one-line subtitle. For `email` with draft: `Draft ready — <subject snippet>`. For `slack`: `Reply in #<channel>`. For tasks with `notes`: first 80 chars. Hidden when empty.
   - **Row 3**: action pair, right-aligned. Always shows `[ Delete ]` and `[ Open draft ]` (or `[ Create draft ]`).
4. **Action: Delete** — calls `optimisticDeleteTask(task.id, true, conversationId)` from [use-optimistic-task-actions.ts:93](apps/mail/modules/userTasks/hooks/use-optimistic-task-actions.ts). Existing 5-second undo toast applies. No new server route needed.
5. **Action: Open draft / Create draft** — derived from `task.taskActionData`:
   ```ts
   function resolveDraftAction(task: ConversationUserTask): DraftAction {
     const d = task.taskActionData;
     if (d?.channel === 'email' && d.draftId)  return { kind: 'open',   target: { channel: 'email',  threadId: d.threadId,  draftId: d.draftId } };
     if (d?.channel === 'slack' && d.message)  return { kind: 'open',   target: { channel: 'slack',  workspaceId: d.workspaceId!, channelId: d.channelId, message: d.message } };
     if (d?.channel === 'calendar')            return { kind: 'open',   target: { channel: 'calendar', eventId: d.eventId, calendarId: d.calendarId, htmlLink: d.htmlLink } };
     return { kind: 'create' };
   }
   ```
   - **Open (email)**: switch the conversation's active tab to `timeline` (`useCedarStore.getState().setActiveConversationTab('timeline')`) and dispatch a composer-open event picked up by the `SlackComposer` planned in [overview.md §3.2.10](./overview.md). **Decision:** use the universal Timeline composer rather than the legacy `/mail/[threadId]` route — this keeps the user inside the conversation workspace and reuses the composer's send pipeline. If the composer is not yet wired (pre-Phase-3 of overview), fall back to `selectThreadId(threadId)` + `setIsThreadOpen(true)` exactly like [TimelineTaskItem.tsx:158-166](apps/mail/modules/conversations/components/timeline/TimelineTaskItem.tsx).
   - **Open (slack)**: same — switch to Timeline tab, pre-fill the `SlackComposer` with `task.taskActionData.message`, target = `{ workspaceId, channelId }`.
   - **Open (calendar)**: open `htmlLink` in a new tab (no in-app calendar editor today).
   - **Create**: call `optimisticExecuteTaskNow(task.id, conversationId)` from [use-optimistic-task-actions.ts:1168](apps/mail/modules/userTasks/hooks/use-optimistic-task-actions.ts) — this invokes `trpc.agentExecutions.executeTaskNow` ([agent-executions.ts:667](apps/server/src/trpc/routes/agent-executions.ts)), which writes a new `draftId` (or Slack `message`) back onto `task.taskActionData`. On resolution, immediately re-run `resolveDraftAction` and invoke the matching open path.
6. **Task edit affordance** — **Decision:** inline edit on the title only (Textarea swap, matches `TimelineTaskItem`). Due-date edit stays as a `DatePickerWithNaturalInput` popover anchored to a small date chip rendered between the title and the action row only when a due date exists (or as a `+ date` ghost when none). No side panel.
7. **`groupTasksByDate`** at [apps/mail/modules/conversations/components/nextSteps/groupTasksByDate.ts](apps/mail/modules/conversations/components/nextSteps/groupTasksByDate.ts) — pure helper.
   ```ts
   type TaskGroupKey =
     | 'overdue'
     | 'today'
     | 'tomorrow'
     | 'thisWeek'
     | 'nextWeek'
     | { kind: 'weekOf'; mondayIso: string }   // e.g. '2026-03-23'
     | 'noDate'
     | 'completed';

   interface TaskGroup {
     key=[redacted];
     label: string;            // 'Overdue' | 'Today' | 'Tomorrow' | 'This week' | 'Next week' | 'Week of Mar 23' | 'No date' | 'Completed'
     tone?: 'danger';          // 'overdue' only
     tasks: ConversationUserTask[];
   }

   function groupTasksByDate(
     tasks: ConversationUserTask[],
     now: Date,
     timezone: string,
     opts?: { showCompleted?: boolean },
   ): TaskGroup[];
   ```
   Rules (all in the user's local timezone):
   - **Overdue** — `task.status === 'todo' && task.dueDate && task.dueDate < startOfToday`. Sorted ascending (oldest first).
   - **Today** — `task.dueDate` between `startOfToday` and `endOfToday` (inclusive). Sorted by `dueDate` ascending, then `createdAt`.
   - **Tomorrow** — `dueDate` between `startOfTomorrow` and `endOfTomorrow`.
   - **This week** — after `endOfTomorrow` and before `startOfNextMonday` (Monday-anchored week; if today is already late in the week the group may be empty).
   - **Next week** — `startOfNextMonday` … `endOfNextSunday`.
   - **Weekly headers** — anything further out is grouped by the Monday of its week; label = `Week of ${MMM d}`. One group per non-empty week, sorted ascending.
   - **No date** — `task.status === 'todo' && task.dueDate == null`.
   - **Completed** — `task.status === 'done'`, sorted by `completedAt` descending. Returned only when `opts.showCompleted === true`.
   - Skip any group whose `tasks` array is empty (always-shown headers create visual noise on quiet days).
8. **Empty-state copy** — when *every* group is empty after filtering:
   ```
   No follow-ups yet. The Next Steps Agent will create tasks as the
   conversation evolves, or you can add one manually.    [ + Add task ]
   ```
   When a specific group would be empty: just hide the header (rule above). One persistent footer `[ + Add task ]` button at the bottom of `TaskBlockList` calls `trpc.userTasks.createTask` ([user-tasks.ts:286](apps/server/src/trpc/routes/user-tasks.ts)) with `conversationId`, `description: ''`, `dueDate = startOfTomorrow`, `taskCreatedBy: 'user'`, then opens the new block's title in edit mode.
9. **Completed visibility** — **Decision:** hidden by default; surface a single divider-row toggle `▾ Show N completed tasks` directly under the last non-completed group. Toggling expands a `Completed` group at the bottom; tasks render dimmed with strikethrough title and only the **Delete** action (no "Open draft"). Persisted in-component (no store) — Tab-local state is enough.
10. **Universal-composer integration** — **Decision:** Open-draft does **not** open the composer in a modal or sheet on top of the Next Steps tab; it switches the active tab to `timeline` and opens the composer there (single composer instance, no z-stacking with the date-grouped list). Justification: the composer planned in [overview.md §3.2.10](./overview.md) is the canonical send surface and lives inside Timeline; surfacing a second instance on Next Steps means two composers competing for "draft is open" state. Switching tabs also leaves the email/slack thread context visible above the composer, which is what users actually need when reviewing a draft.
11. **Reuse from overview Phase 2** — overview.md Phase 2 has a checkbox: "Extract `NextStepsCard` as a standalone export from [AgentRow.tsx:689](apps/mail/modules/conversations/components/AgentRow.tsx) (the TASK_AGGREGATOR body) into `apps/mail/modules/conversations/components/NextStepsCard.tsx`." That extraction is a **prerequisite** for this doc — `NextStepsTab` imports the extracted component. If it has not landed, do step (1) of overview Phase 2 first.
12. **Sidebar variant** — same `NextStepsTab` renders in both `view` and `sidebar` variants of `ConversationTabBody`. The date-grouped block list collapses gracefully under ~360 px because each `TaskBlock` is a vertically-stacked layout (title → optional subtitle → right-aligned action row), no horizontal grid.
13. **No DB schema change** — the existing `task.taskActionData.draftId` (Gmail-side draft) + `taskActionData.message` (queued Slack message) + `taskActionData.eventId` (calendar) links are sufficient for "open draft for task". There is no Cedar-side `drafts` table and this redesign does not need one.
14. **Server invariant** — when `optimisticDeleteTask` removes a task, the existing `agendaTaskSyncHook` ([apps/server/src/services/document-saving/hooks/agenda-task-sync.ts](apps/server/src/services/document-saving/hooks/agenda-task-sync.ts)) is bypassed (we're not editing the agenda doc anymore); deletion goes through `trpc.userTasks.deleteTask` directly. Once overview.md Phase 2 is done, `ConversationAgendaDocument` and the agenda reconciler are no longer mounted in the Next Steps tab, so the agenda Y.Doc is not a source of truth for this surface — the `userTasks` array from `crm.getConversation` is.

## 4) Implementation phases

### Phase 1 — Pure date-grouping helper + NextStepsTab skeleton

**Goal:** Land `groupTasksByDate`, `NextStepsTab`, and `TaskBlockList` rendering read-only blocks (no actions yet), wired into the `nextSteps` tab from overview.md Phase 2. Confirms the data path and the grouping rules in isolation.

- [ ] Create `apps/mail/modules/conversations/components/nextSteps/groupTasksByDate.ts` with the `TaskGroup`, `TaskGroupKey` types and the pure `groupTasksByDate(tasks, now, timezone, opts)` function per §3.2.7.
- [ ] Create `apps/mail/modules/conversations/components/nextSteps/TaskBlock.tsx` rendering a read-only block: checkbox · title · optional subtitle. No action buttons in this phase.
- [ ] Create `apps/mail/modules/conversations/components/nextSteps/TaskBlockList.tsx` calling `groupTasksByDate` and rendering one section per non-empty group, with the `Completed` toggle wired but with delete/open disabled.
- [ ] Create `apps/mail/modules/conversations/components/nextSteps/NextStepsTab.tsx` composing `<NextStepsCard />` on top of `<TaskBlockList />`. Reuse the existing extracted `NextStepsCard` (overview.md Phase 2 prerequisite); if not yet extracted, do that extraction here.
- [ ] Wire `<TaskBlockList tasks={conversationData.data.userTasks ?? []} conversationId={…} />` into the `nextSteps` case of `ConversationTabBody` (added in overview.md Phase 2). The Next Steps tab body is a thin wrapper: `<div className="flex flex-col h-full"><TaskBlockList … /></div>` — `NextStepsCard` is **not** stacked above it here (the pinned card lives in Inbox).
- [ ] Other surfaces wire the same components independently:
  - **Inbox pinned card** — `<NextStepsCard />` mounted at the top of `SlackTimeline`'s scroll region. See [timeline.md](./timeline.md) Phase 1 for the exact checkbox.
  - **Conversations Agenda view** — `<TaskBlockList tasks={allTasks} />` mounted as the Agenda body when the sidebar's Agenda item is selected. See [conversations-page.md](./conversations-page.md) for the cross-conversation data source and exact checkbox.
- [ ] Ship `NextStepsCard`, `TaskBlockList`, `TaskBlock`, and `groupTasksByDate` as four independent exports so the three surfaces above can compose them directly without a shared orchestrator.
- [ ] Add `browserTimezone` from [apps/mail/modules/conversations/utils](apps/mail/modules/conversations/utils) (reused in [NextStepsCard.tsx:126](apps/mail/modules/conversations/components/timeline/NextStepsCard.tsx)) as the `timezone` argument.

**Tests:**

- [ ] `pnpm --filter @cedar/mail typecheck`
- [ ] `pnpm --filter @cedar/mail lint`
- [ ] Unit test `groupTasksByDate.test.ts` covering: overdue/today/tomorrow/thisWeek/nextWeek/weekOf/noDate/completed buckets; empty input; tasks straddling midnight in non-UTC timezones; toggle for completed.
- [ ] Manual: open a conversation with tasks spanning several weeks; confirm the headers render in the expected order and only non-empty groups are shown.

### Phase 2 — Task block actions (Delete + Open draft / Create draft)

**Goal:** Wire the per-block action pair. Delete uses the existing optimistic action; Open-draft routes to the resolved channel; Create-draft invokes `executeTaskNow` and then opens.

- [ ] Add `resolveDraftAction(task)` helper inside `TaskBlock.tsx` per §3.2.5.
- [ ] Wire **Delete** button to `optimisticDeleteTask(task.id, true, conversationId)` from [use-optimistic-task-actions.ts:93](apps/mail/modules/userTasks/hooks/use-optimistic-task-actions.ts).
- [ ] Wire **Open draft (email)** to `useCedarStore.getState().setActiveConversationTab('timeline')` followed by a `openComposerForDraft({ threadId, draftId })` event picked up by the Timeline composer. Fallback (composer not yet wired): `selectThreadId(threadId)` + `setIsThreadOpen(true)`, mirroring [TimelineTaskItem.tsx:158-166](apps/mail/modules/conversations/components/timeline/TimelineTaskItem.tsx).
- [ ] Wire **Open draft (slack)** to `setActiveConversationTab('timeline')` + `openComposerForDraft({ workspaceId, channelId, message })`.
- [ ] Wire **Open (calendar)** to `window.open(htmlLink, '_blank', 'noopener')` when present.
- [ ] Wire **Create draft** to `optimisticExecuteTaskNow(task.id, conversationId)` from [use-optimistic-task-actions.ts:1168](apps/mail/modules/userTasks/hooks/use-optimistic-task-actions.ts); on resolve, re-read `task.taskActionData` from the freshly-invalidated `crm.getConversation` cache and call the matching open path.
- [ ] Add `isExecuting` local state to disable the button + show a spinner while the create-draft mutation is in flight (pattern from [TimelineTaskItem.tsx:64,180-193](apps/mail/modules/conversations/components/timeline/TimelineTaskItem.tsx)).
- [ ] Render the checkbox via `AnimatedCheckmark` and wire its click to `optimisticCompleteTaskDelayed(task.id, conversationId)`.

**Tests:**

- [ ] `pnpm --filter @cedar/mail typecheck`
- [ ] `pnpm --filter @cedar/mail lint`
- [ ] Manual: on a conversation with one email task (no draft), one email task (with draft), one slack task with a queued message, and one task with no `taskActionData`: confirm that the button reads `Create draft` only on the first and fourth; clicking it on the first creates a draft and immediately switches to Timeline with the composer open; the second goes straight to the composer with the existing draft; the third opens the Slack composer pre-filled.
- [ ] Manual: delete a task from a block; confirm the 5-second undo toast restores it; confirm the optimistic remove keeps the surrounding group headers from flickering when only one task in a group is removed.

### Phase 3 — Inline edit, due-date chip, empty state, completed toggle polish

**Goal:** Make every block fully interactive without leaving the tab. Adds title inline edit, date chip with picker, the "+ Add task" footer, and the persistent completed toggle.

- [ ] Add inline title editing to `TaskBlock`: click title → swap to `Textarea`; blur → `optimisticUpdateTaskDescription(task.id, trimmed, conversationId)`. Mirror the focus/blur dance from [TimelineTaskItem.tsx:204-216,470-495](apps/mail/modules/conversations/components/timeline/TimelineTaskItem.tsx).
- [ ] Add a due-date chip rendered when `task.dueDate` is present; wrap in `DatePickerWithNaturalInput` and wire `onChange` to `optimisticUpdateTaskDueDate(task.id, date, conversationId)`. When absent, render a ghost `+ date` chip with the same picker.
- [ ] Render the empty-state CTA copy from §3.2.8 when `groups.length === 0`; wire the `[ + Add task ]` button to `trpc.userTasks.createTask` ([user-tasks.ts:286](apps/server/src/trpc/routes/user-tasks.ts)) with `{ conversationId, description: '', dueDate: startOfTomorrow.toISOString(), taskCreatedBy: 'user' }`; on success, focus the new block's title.
- [ ] Render the persistent footer `[ + Add task ]` button (only when `groups.length > 0`).
- [ ] Render the `▾ Show N completed tasks` toggle directly under the last non-completed group; persist state in `useState` inside `TaskBlockList`.
- [ ] Style completed blocks with strikethrough title and 60% opacity; keep only the Delete button visible.

**Tests:**

- [ ] `pnpm --filter @cedar/mail typecheck`
- [ ] `pnpm --filter @cedar/mail lint`
- [ ] Manual: edit a task title inline; confirm blur saves and that the agenda-reconcile shimmer fires on the next refetch.
- [ ] Manual: change a task's due date so it moves from Today to Next week; confirm it re-buckets without a full re-render flash.
- [ ] Manual: complete all tasks; confirm the toggle reads `Show 1 completed task` (singular) and that toggling reveals the Completed group.

### Phase 4 — Cleanup, retire the agenda-document Next Steps surface inside the tab

**Goal:** Remove the now-redundant `ConversationAgendaDocument` mount for the in-tab Next Steps surface. The agenda document and its TipTap nodes remain available to other surfaces and other features that still use it (the doc is still seeded/reconciled server-side); only the `nextSteps`-tab mounting is removed.

- [ ] Verify `ConversationAgendaDocument` is no longer mounted by any of the redesigned surfaces (Inbox pinned card, Conversations Agenda view, sidebar tab body). The agenda doc remains seeded/reconciled server-side for any other consumer that still reads from `documents.getDoc({ documentType: 'conversation_agenda' })`; only the conversation-view mount points are gone.
- [ ] In [AgentRow.tsx:311-320](apps/mail/modules/conversations/components/AgentRow.tsx), confirm the `TASK_AGGREGATOR` row body is no longer rendered (the AgentList in `agents` tab filters out `TASK_AGGREGATOR` per overview.md Phase 2 checkbox).
- [ ] Delete the inline `dueTaskCount` filter at [AgentRow.tsx:689-696](apps/mail/modules/conversations/components/AgentRow.tsx) once the row is removed.
- [ ] If no other surface mounts `ConversationAgendaDocument`, mark it for follow-up deletion in a tracking issue (do not delete in this PR — `agendaTaskSyncHook` and the reconciler still service other agent surfaces).
- [ ] Grep `apps/mail` for `ConversationAgendaDocument`, `agendaTaskSyncHook`, `TASK_AGGREGATOR` and document remaining call sites at the top of this doc as follow-ups.
- [ ] Run `pnpm deps:check` to confirm no circular imports introduced.

**Tests:**

- [ ] `pnpm --filter @cedar/mail typecheck`
- [ ] `pnpm --filter @cedar/mail lint`
- [ ] `pnpm deps:check`
- [ ] Manual: open the Next Steps tab on a conversation with > 30 tasks across multiple weeks; confirm group headers render in order, the completed toggle works, "+ Add task" creates an immediately editable block, and Delete + Open draft + Create draft round-trip via the Timeline composer.

## Open questions

- Should the `[ + Add task ]` footer pre-select a `taskChannel`? Today the agent picks the channel; manual tasks default to `email`. Confirm with the existing default at [user-tasks.ts:286](apps/server/src/trpc/routes/user-tasks.ts).