From a751c3b313c9dcd1560106d3e92c855ca99112b9 Mon Sep 17 00:00:00 2001 From: titan-ron <30556071+titan-ron@users.noreply.github.com> Date: Thu, 24 Sep 2026 23:50:23 +0300 Subject: [PATCH] feat(chat): open agents' plans, to-dos and edits in a Work panel What an agent hands the person to look at - a plan to approve, a to-do list, an edit - reached the chat as 400 characters of JSON behind a collapsed tool row. Each tool call's own input is now read in main into a bounded SessionMessage.artifact, for Claude (ExitPlanMode, TodoWrite, TaskCreate/TaskUpdate, Edit, MultiEdit, Write), Codex (update_plan, apply_patch, the FileChange items code-mode patches complete with, the exec stream's todo_list and file_change) and Copilot (exit_plan_mode, edit, create, apply_patch), plus ACP plan updates and diff content. The renderer folds them into a Work panel beside the transcript: Plan (every version, flagged while it waits for approval), To-dos (where the list stands now) and Edits (per file, each change as its call described it, with a link to Changes for what is on disk). A row that carries one opens the panel at itself in one click; the header's Work key and cmd+J open it on what matters now. Under 720px of deck the panel covers the conversation instead. A plan gate's approval card now shows the plan. A call whose result failed is marked on its own row, matched by tool-use id, so an edit that never landed reads as one. Codex's streamed file_change rows are named as their logged FileChange rows are, so a rejoined turn matches the two. --- AGENTS.md | 1 + design-system/cockpit/MASTER.md | 3 +- design-system/cockpit/pages/chat.md | 72 ++++- docs/guide/chat.md | 43 +++ scripts/ui-tour/tour.mts | 35 ++- scripts/ui-tour/world.mts | 74 ++++- src/main/acp-core.ts | 30 +- src/main/chat.ts | 28 +- src/main/parsers/artifacts.ts | 368 +++++++++++++++++++++++ src/main/parsers/claude.ts | 34 ++- src/main/parsers/codex.ts | 80 +++++ src/main/parsers/copilot.ts | 10 + src/main/parsers/util.ts | 15 + src/renderer/src/App.tsx | 4 +- src/renderer/src/AskPicker.tsx | 22 +- src/renderer/src/ChatView.tsx | 433 +++++++++++++++++++-------- src/renderer/src/InstructionDiff.tsx | 5 +- src/renderer/src/WorkPanel.tsx | 393 ++++++++++++++++++++++++ src/renderer/src/logos.tsx | 48 ++- src/renderer/src/style.css | 77 +++++ src/renderer/src/work.ts | 208 +++++++++++++ src/shared/types.ts | 56 ++++ tests/acp-core.test.ts | 54 ++++ tests/artifacts.test.ts | 399 ++++++++++++++++++++++++ tests/chat.test.ts | 37 ++- tests/component/work-panel.test.tsx | 187 ++++++++++++ tests/parsers.test.ts | 156 ++++++++++ tests/work.test.ts | 156 ++++++++++ 28 files changed, 2865 insertions(+), 163 deletions(-) create mode 100644 src/main/parsers/artifacts.ts create mode 100644 src/renderer/src/WorkPanel.tsx create mode 100644 src/renderer/src/work.ts create mode 100644 tests/artifacts.test.ts create mode 100644 tests/component/work-panel.test.tsx create mode 100644 tests/work.test.ts diff --git a/AGENTS.md b/AGENTS.md index 7cd25bcb..9a545ce0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,6 +60,7 @@ Performance invariants — all deliberate, keep them: - The full index is never shipped to or rendered by the UI. Always paginate. - Meta parsing reads at most 256KB per file. Parsers are failure-tolerant: session log formats are provider-internal and drift between releases, so skip anything unreadable rather than fail the scan. +- What a tool call hands the person — a plan, a to-do list, an edit — rides its transcript row as `SessionMessage.artifact`, read off the call's own input in main by `src/main/parsers/artifacts.ts` (shared by the three parsers and the stream parsers in `chat.ts` and `acp-core.ts`) and bounded there (files per call, lines per file, line width), because it crosses IPC on every transcript read. A call that failed is marked on its own row (`failed`), matched to its result by id where the log has one. The renderer only folds these for the Work panel (`work.ts`); it never parses tool JSON. - A parser reports only what its log states (`SessionMeta.logBranch`), never what the disk says. Anything derived from the checkout — `repo`, `isWorktree`, `gitBranch` — is the indexer's `annotate()`, is recomputed on every scan, and is stripped before the stat-cache is written so a renamed remote or a moved worktree can't freeze into it. Branches are the live example: Copilot stopped writing `context.branch` after CLI 1.0.80 and most Codex rollouts carry no `git` block, so for those the checkout's HEAD is the only source. - A session can span several files: Codex paginates a long thread into a new rollout whose `session_meta.history_base` names where the previous one's history ends. The parser reports that (`SessionMeta.historyBase`), `foldThread` in the indexer folds every file carrying one id into a session on the newest file with the earlier ones as `segments`, and anything that reads or removes a session's log — transcript, search, profile, cleanup — reads every file (`sessionLogFiles`; an earlier page only up to its `endByte` where the thread's history is what counts), never `sourcePath` alone. Only liveness reads just the newest file, the one being written. - The stat-cache (mtime+size, persisted to userData) means restarts only re-parse changed files; scans yield to the event loop so IPC never blocks. diff --git a/design-system/cockpit/MASTER.md b/design-system/cockpit/MASTER.md index c300e705..cb0aedcc 100644 --- a/design-system/cockpit/MASTER.md +++ b/design-system/cockpit/MASTER.md @@ -127,7 +127,8 @@ Reuse these; don't invent parallel variants: - **`.rail-resizer`** (`RailResizer.tsx`) — the sash on the rail's right edge: the sidebar's width is the person's, dragged between 200px and what the deck can spare (`rail.ts`, remembered per machine in localStorage, never in config), invisible at rest and an accent hairline on hover, focus and while dragging. A `role=separator` whose value is the rail's *measured* width; ← → move it 16px (64 with ⇧), Home/End reach the bounds, double-click resets. Because the rail is dragged, anything that sheds *inside* it asks the rail (`@container rail`), never the window. See `pages/sidebar.md`. - **Cards:** `.ns-card` — ONE width (`min(760px, 94%)`) shared by every card view (Settings, Agents, Profile, Cleanup, New session); navigating between them must never make the dialog jump sizes. `.composer-card` for the home prompt, in home's own `--home-col`. - **Card tabs** (`TabList` + `TabPanel`, `Tabs.tsx`) — the one way a card view pages its content: a `.pnl-tabs.ns-tabs` row of `.pnl-pill` tabs (hairline under it) over **one** mounted panel. Settings, Agents, Profile and Cleanup all use it. **A tab replaces the panel; nothing in a card scrolls to a heading further down** — a jump row that scrolled took the title, the row and Close off screen with it, and a card of stacked sections read as one long page. Switching mounts the panel fresh (two panels built alike must never share internal state) and puts the card's scroller back at `scrollTop 0`. The row is one tab stop (roving `tabIndex`; ←/→ wrap, Home/End), only the selected tab carries `aria-controls`, and the panel is named by its tab — so a panel holding one group carries no heading that repeats the pill, and a panel holding several names each group with an `h3` that doesn't either. A tab may carry its count (`.pnl-pill-n`, left off at zero), a warn tone or an amber dot. Controls that govern every tab (Cleanup's threshold, Agents' scope, Profile's headline numbers) sit above the row; the pill row must hold in two rows at the 560px floor. A new card view that grows past a screen gets tabs, not a jump row. -- **Chat:** user bubbles right (accent tint), assistant left with avatar; `.tool-row` = collapsed `
` one-liners; `.sys-row` = dotted-left-border annotations; streaming = accent left border. +- **Chat:** user bubbles right (accent tint), assistant left with avatar; `.tool-row` = collapsed `
` one-liners, or a `.tool-open` button when the call carries a plan, to-dos or an edit; `.sys-row` = dotted-left-border annotations; streaming = accent left border. +- **`.work-panel`** (`WorkPanel.tsx`) — the agent's plan, to-dos and edits beside the transcript (chat only, `.btn-work` / ⌘J, or one click on a `.tool-row.tool-open` row): card tabs over one scroller, the review's file blocks for edits, and under 720px of `@container chat-deck` it covers the conversation instead. See `pages/chat.md`. - **`.review`** — the worktree's changes in the transcript's place (chat only, `.btn-review` / ⌘D): the instructions review's `.idiff-*` line grammar with line numbers, a scope switch, and line notes that go back to the agent through the composer. With an open PR it leads with `.review-pr` (what the PR waits on + "Fix with ") and shows reviewers' unresolved threads (`.review-thread`) under their lines. See `pages/chat.md`. - **Semantic count pills:** bordered pill = "session count on a repo"; org counts are plain text. - **`Select`** — the one dropdown (see Native Controls); never a raw `