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).