TASK_EXECUTION_MODE_DESIGN.md15.7 KBView on GitHub
# Task Execution Mode

When a specific task is open (`?task=<id>` set, left rail showing the task-group accordion), the app
enters **execution mode**: the center column becomes the task's *output surface*, the chat empty-state
speaks to the task, and keyboard nav walks the task list. This doc covers that mode plus a card
redesign that makes each card read as `conversation → due` / `output-type → description`.

Most of the machinery already exists and is reused rather than rebuilt:

- **Thread-per-invocation** — `useInvokeTaskInChat` (`hooks/use-invoke-task-in-chat.ts`) + server
  `agentExecutions.invokeTaskInChat` already create a `chat_threads` row, persist it on
  `user_tasks.chat_thread_id`, and reuse it if the task was already invoked. This *is* the agenda
  structure. Execute just calls it.
- **Executing border animation** — `TabBotIcon` (`components/icons/animated/bot-thinking.tsx`) traces
  a glow around the bot outline while `isThreadProcessing(threadId)`. Reused on the card.
- **Empty-chat surface registry** — `EmptyChatSurface` union + exhaustive `EMPTY_CHAT_LAYOUTS` record.
  We add one surface.
- **Center overlay** — `/tasks`, `/mail`, `/conversations` overlay `<ActiveViewDisplay/>` when
  `overlayActive`, switching on `getActiveView()`. We add one view.

## Current state

- **Card** (`components/TaskKanbanCard.tsx`): eyebrow row (avatar + company, channel icon top-right),
  a "deal state" footer with **Last contact** + **Next step** rows, and a bottom task row with a
  **done checkbox** + description. Due date is used only for column placement/sort, never rendered.
- **Selection** is the `?task` URL param (nuqs). Today it drives *only* the active-card highlight
  (`TaskKanbanBoard.tsx:235`, `TaskGroupsAccordion.tsx:140`). `?group` drives the left-rail drill-in
  (`LeftSidebarContent.tsx:724`, gated on `expanded && groupParam`).
- **Opening a task** (`utils/open-task.ts` via `openTaskInExecutionMode`, `TaskKanbanBoard.tsx:105`):
  sets `?group` + `?task`, then — email task **with** a draft → `?threadOpen` → `ThreadDisplay`;
  otherwise → `openConversationFromAgenda` → `ConversationView`; no conversation → nothing. **There is
  no task-output rendering path.**
- **Center column** on `/tasks` = list, with `<ActiveViewDisplay/>` overlaid when `overlayActive`
  (`tasks/layout.tsx:44`). `getActiveView()` (`uxSlice.ts:413`) returns
  `globalCanvas | conversation | thread | null`; the branch switch is `active-view-display.tsx:20`.
- **Empty chat surface** = pure projection `computeEmptyChatSurface(context, page)`
  (`selectEmptyChatSurface.ts`) → `getEmptyChatLayout(surface)` (`emptyChatLayout.ts`, exhaustive
  `Record`). `EmbeddedCedarChat.tsx:543` already pathname-overrides `/tasks/kanban → tasksKanban`.
- **Context items** = per-thread committed set. `ContextKind = 'conversation' | 'email_thread' |
  'slack_thread' | 'file'` (`MessageTypes.ts:245`); `addContextItem(threadId, item)`
  (`messagesSlice.ts:676`); per-kind color/icon/label in `contextKinds.ts` (exhaustive records).
- **Hotkeys**: `useTaskListHotkeys` (`hooks/use-task-list-hotkeys.ts`) binds `e/s/w` in scope
  `task-list`; `HotkeyBar` surfaces the active scope's `important` shortcuts. **No j/k / arrow nav.**

## Proposed changes

### #6 — Card redesign (`TaskKanbanCard.tsx`)

```
┌───────────────────────────────────────┐
│ ⌂ Acme Corp                 [ due 3d ] │  row 1: conversation ⟷ due-date badge
│ ▧ Send pricing follow-up              │  row 2: output-type icon + description
└───────────────────────────────────────┘
```

- **Row 1**: avatar + company/conversation name (opens conversation, as today) on the left; a
  **due-date badge** (`RelativeDateBadge date={task.dueDate} colorType="scheduled"` — overdue red,
  upcoming green) pinned right. Remove the top-right `ChannelIcon`.
- **Row 2**: **output-type icon** + description. Icon reflects the *output* (`taskActionData`):
  email draft → `GmailColor`; slack → `MessageSquare`; none (`taskActionData == null`) → agent icon
  (`Bot`). This replaces the done checkbox — completion stays available via hover action + `e` hotkey.
- **Remove** the `hasMeta` footer entirely (Last contact + Next step rows).
- While the task's thread is processing, the output-type icon is swapped for `TabBotIcon`
  (`processing`) — the executing border animation (see #4).

Card data already carries `dueDate` + `taskActionData` (both board and accordion pass the full
enriched `listUserTasks` row). `TaskCardConversation`'s `lastContactedAt/nextStepDate/nextSteps`
become unused by the card but stay on the type (still fetched); no query change.

### #3 — Task output panel (center column)

New `components/TaskOutputPanel.tsx`, shown when a task is open **without** a draft:

- **Top**: task description (and conversation/company eyebrow).
- **Body**: empty state — "No output for this task".
- **Actions**: big **Execute task** (→ #4 invoke) and big **Open conversation**
  (→ `openConversationFromAgenda`, disabled if no `conversationId`).

Wiring (mirrors the existing `threadOpen` pattern, store-driven — not reading `?task` at render):

1. `uxSlice.ts` — add `isTaskOutputOpen: boolean` + `activeTaskId: string | null` with setters;
   extend `getActiveView()` precedence: `globalCanvas → conversation → thread → taskOutput → null`.
2. `LayoutUrlSync.tsx` — add a `?task` handler: on set → `setActiveTaskId(id)`; open the task-output
   view only when there's no `threadOpen`/`conversationId` (draft tasks still use `ThreadDisplay`).
   On clear → reset. Also set/clear `selectedArtifact = { kind: 'task', id }` (feeds #2).
3. `active-view-display.tsx` — add `if (activeView === 'taskOutput') return <TaskOutputPanel/>` branch.
4. `tasks/layout.tsx:44` (+ `mail.tsx`, `ConversationInboxLayout.tsx` if desired) — include
   `isTaskOutputOpen` in `overlayActive`.
5. `utils/open-task.ts` — when no draft: set the task-output view instead of auto-opening the
   conversation. (`openTaskInExecutionMode` already sets `?task`/`?group`.)

### #2 — `task` empty-chat surface

- `layoutTypes.ts` — add `'task'` to `EmptyChatSurface`; add `{ kind: 'task'; id }` to
  `ContextContent` and the `DisplayArtifact` kind chain.
- `resolveContext.ts` — map `{kind:'task'}` artifact → `{kind:'task'}` context.
- `selectEmptyChatSurface.ts` — `case 'task': return 'task'`.
- `emptyChatLayout.ts` — add a `task` builder (heading + a compact prompts/`taskShortcuts`-style
  section aimed at "execute / refine this task"). No new section `kind` unless a heavy node is needed.
- `DisplayArtifactPanel.tsx` — handle `selected.kind === 'task'` by rendering `TaskOutputPanel`
  (keeps the `/agent` route consistent; `/tasks` uses the overlay).
- Tests: extend `selectEmptyChatSurface.test.ts` (totality) + `emptyChatLayout.surfaces.test.ts`.

### #4 — Execute → per-task chat thread + animation

- **Execute task** (panel button + a card affordance) → `useInvokeTaskInChat({ taskId, description,
  conversationId })`. Reuses the agenda flow: `invokeTaskInChat` mutation → `createThread →
  switchThread → setShowChat → setChatInputContent(seedPrompt) → sendMessage`. Thread id persists on
  `user_tasks.chat_thread_id`; multiple tasks stream independently (`isThreadProcessing` is per-thread),
  so you can fire several and come back — clicking the card's processing icon reopens its thread.
- **Card animation**: `TaskKanbanCard` reads `isThreadProcessing(task.chatThreadId)` and renders
  `TabBotIcon` (border glow) in place of the output-type icon while processing. `chatThreadId` must be
  threaded onto `TaskCardTask` (already on `HydratedUserTask`; add to the card type + board/accordion
  mapping — it's a pass-through).

### #1 — j/k / ↑↓ task navigation in execution mode

- New `hooks/use-task-execution-nav.ts` (or extend `useTaskListHotkeys`): when a task is open, bind
  `j`/`ArrowDown` → next, `k`/`ArrowUp` → prev over the accordion's **flat ordered task id list**
  (groups concatenated in render order), then re-run `openTaskInExecutionMode(nextTask)` so the center
  + highlight follow. Mounted where the ordered list lives (`TaskGroupsAccordion`).
- `config/shortcuts.ts` — add `taskExecNext`/`taskExecPrev` (`j`/`k`, `important: true`) in a scope
  active only in execution mode (new `task-execution` scope, enabled on mount of the nav hook) so the
  **HotkeyBar** shows them in active task mode.

### #5 — Task as a context item

- `MessageTypes.ts` — add `'task'` to `ContextKind`.
- `contextKinds.ts` — add `task` entries to the `CONTEXT_KIND_COLORS/ICONS/LABELS` records
  (icon `Bot`/`ListChecks`, label "Task").
- **Server** — `chat.addContextItem` route + backend `ContextKind` must accept `'task'`; backend
  resolves the task (description/status/conversation) into chat context. *(Investigate exact server
  types during this phase; if strict validation rejects unknown kinds, extend there.)*
- **Auto-attach on invoke** — in `useInvokeTaskInChat`, after `createThread`, call
  `addContextItem(threadId, { kind:'task', id:taskId })` and attach the conversation
  (`addContextItem` `conversation` or `attachPrimaryConversation`). Task also becomes available via the
  `@`/mention + `[` file-style pickers (register a `task` mention provider with
  `contextKind: 'task'`).

## Critical files

- `apps/mail/modules/userTasks/components/TaskKanbanCard.tsx` — card redesign, processing animation.
- `apps/mail/modules/userTasks/components/TaskOutputPanel.tsx` — **new** center panel.
- `apps/mail/modules/userTasks/utils/open-task.ts`, `components/TaskKanbanBoard.tsx` — open flow.
- `apps/mail/modules/userTasks/components/TaskGroupsAccordion.tsx`,
  `hooks/use-task-execution-nav.ts` (**new**) — j/k nav.
- `apps/mail/modules/ux/uxSlice.ts`, `modules/ux/layout/LayoutUrlSync.tsx`,
  `components/ui/active-view-display.tsx`, `app/(routes)/tasks/layout.tsx` — center-column view.
- `apps/mail/modules/ux/layout/{layoutTypes,selectEmptyChatSurface,resolveContext}.ts`,
  `cedar-os/.../chatComponents/{emptyChatLayout,EmptyStateSuggestions}.tsx`,
  `modules/home/components/DisplayArtifactPanel.tsx` — empty-chat + `/agent` surface.
- `cedar-os/.../store/messages/MessageTypes.ts`, `.../chatComponents/contextKinds.ts`,
  `hooks/use-invoke-task-in-chat.ts`, server `chat`/context types — context item.
- `apps/mail/config/shortcuts.ts` — nav shortcuts.

## Phased implementation plan

**Foundation (shared by #2/#3/#5/#6):** `task` became a first-class `ContextKind` rather than a
display-only artifact kind — it is genuinely attachable (#5), so unifying the display artifact, the
empty-chat surface, and the context item on one `task` kind avoids two parallel vocabularies. The
task id therefore rides in `selectedArtifact.id`; `isTaskOutputOpen` is derived in `setSelectedArtifact`
exactly like `isThreadOpen`/`isConversationOpen`. This pulled the server context vocabulary + task
hydration (originally Phase 6) forward into the foundation, since widening `ContextKind` compiles
against the server mutation types.

- [x] **Phase 1 — Card redesign (#6-card).** Row1 conversation ⟷ due badge; Row2 output-type icon +
  description; removed checkbox + last-contact/next-step; card self-derives the processing glyph.
- [x] **Phase 2 — Task output panel (#3).** `TaskOutputPanel` + `threadSlice.isTaskOutputOpen` +
  uxSlice `taskOutput` view + `active-view-display` branch + `tasks/layout` overlay + shared
  `useOpenTaskInExecutionMode` (open-task no-draft → `setSelectedArtifact({kind:'task'})`) +
  `DisplayArtifactPanel` task branch + Escape-to-close.
- [x] **Phase 3 — Execute + animation (#4).** Panel **Execute** → `useInvokeTaskInChat` (+ query
  invalidation so the card picks up `chatThreadId`); card renders `TabBotIcon` while
  `isThreadProcessing(chatThreadId)`. Invoke-flow ordering test still green.
- [x] **Phase 4 — `task` empty-chat surface (#2).** `EmptyChatSurface`/`ContextContent` `task`;
  resolver + selector (task wins over page-anchoring) + `emptyChatLayout` builder (`TASK_PROMPTS`).
- [x] **Phase 5 — j/k nav (#1).** `use-task-execution-nav` + `task-execution` scope +
  `taskExecutionShortcuts` (spread before `taskListShortcuts` so j/k win the HotkeyBar's first-3 in
  execution mode); mounted in the accordion over its flat ordered task list. Stops at ends, no wrap.
- [x] **Phase 6 — Task context item (#5).** `ContextKind` `task` (client + server `CONTEXT_KINDS`) +
  `contextKinds` records + server `hydrateTask`; `useInvokeTaskInChat` auto-attaches the task
  (`addContextItem`) + its conversation (`attachPrimaryConversation`) on invoke.

### Follow-ups landed

- **`?task` refresh-persistence** — a guarded URL→store handler in `LayoutUrlSync` re-opens the task
  artifact on a cold load / direct link (only when no `threadOpen`/`conversationId` rides along;
  kind-guarded clear). So `/tasks/kanban?group=…&task=…` restores the open task on reload.
- **Ambient chat follows the selected task** — `useOpenTaskInExecutionMode` (and the panel, for the
  cold-load path) `attachPrimaryConversation` on the active thread, so the chat's primary
  conversation tracks the selected task.
- **Panel header** shows the conversation badge + title as a clickable pill (opens the conversation),
  using the slice-enriched company name (`getTaskById` returns only the raw conversation row).
- **List-surface optimistic actions** — the board/accordion hydrate `cedar.tasks`
  (`useHydrateTasksSlice`) so complete/snooze/delete resolve the task; `optimisticDeleteTask` and the
  snooze path now `invalidateQueries(listUserTasks)` (prefix filter) so the card actually leaves the
  list (a stale `setQueryData` on the no-input key was replaced).

### Round 3 (UX polish)

- **All groups expanded on enter**; clicking a group header opens its first (soonest-due) task, the
  chevron alone collapses (`TaskGroupsAccordion`).
- **Empty chat is context-driven**: the `/tasks/kanban` sub-route override now only applies to the
  resting board — an open task/thread/… (`ARTIFACT_EMPTY_CHAT_SURFACES`) wins, so a task no longer
  shows the tasks-list surface.
- **Email-thread surface** = the conversation's name + a Status Overview card + a Next steps card
  (`ThreadContextCards`, injected as the `conversationCards` section node; reads the thread's primary
  conversation), over the deal-scoped Suggestions.
- **No-output panel** redesigned: the conversation identity row (company badge + name, like the
  conversation view's first row) on top, and just the task as one centered card whose in-card
  **Execute** (`TaskKanbanCard` `onExecute`) spawns the thread — no empty-state text or extra buttons.
- Task surface trimmed to a single "What does this need?" prompt.

### Deferred / notes

- **Task mention provider (`@`/`[` picker) deferred** — the task is auto-attached on invoke and is a
  valid `ContextKind`, so the primary "available context item" need is met; a picker provider
  (`registerMentionProvider` with `contextKind:'task'`) is a follow-up.
- **Task-with-draft** still opens the draft's thread (unchanged); the panel is the no-output surface.

## Verification steps

- Types clean for all touched files (mail + server); pre-existing `threadSlice.ts:2061` unrelated.
- Unit: `invoke-task-flow`, `selectEmptyChatSurface`, `emptyChatLayout.surfaces` green (46 tests). The
  9 pre-existing failing cedar-os/ux suites (removed shortcut-mastery, route-set drift, message-find,
  "BUG—" docs) are unrelated — none reference `task`/`isTaskOutputOpen`/`addContextItem`.
- Manual: open a draftless task → panel (description + Execute + Open conversation); Execute → chat
  thread + card border animation, fire two and return; j/k walk the list; card reads conversation→due
  / icon→description; empty chat shows the task surface.