feat(desktop): side panel tabs get a right-click menu (Close, Close others, copy transcripts) and pin per plan

Merged
DA
8d ago
+1800-230

What

Dean: "Let's add 'Close' and 'Close others' to a right click menu for all tabs. For agents, we want to support 'Copy concise transcript' and 'Copy full transcript'. … I also want to support pinning individual tabs from within a plan."

Right-click menu on every side panel tab

Right-clicking any side panel tab (Board, Code, Transcript, Work, coding agent, subagent, draft, file) opens the shared ActionMenu that the plan menus use (#1831/#1847): same component and icons, no "…". From the keyboard, the menu key or ⇧F10 on a focused tab opens it under the tab.

GroupItems
1Close, Close others (dimmed when nothing else would close)
2Pin tab / Unpin tab, plus Rename on agent tabs (the existing item)
3Agent and subagent tabs only: Copy concise transcript, Copy full transcript
  • Close does what the × does and never stops an agent. Close others closes every other closable, unpinned tab in this plan's strip and chooses the right-clicked one. Pinned tabs survive it.
  • The plan column's single tab gets no menu. None of these items apply to it: it can't close and there's nothing beside it.
  • ActionMenu items gained disabled (dimmed, skipped by the arrow keys), as a system menu does.

Copy concise / full transcript

Both are built from the rows the tab draws (agentRows / the Work tab's worker session through turn-rows.ts), never from the DOM. They live in the new planning/transcript-copy.ts.

  • Concise: # title, the ## Plan / ## Task brief, each ## Developer message, and each agent turn as one line per tool call (- Ran \pnpm test`, - Edited `src/x.ts`, - Asked the worker: …, (failed)` on an error) followed by the turn's last words. No reasoning, no tool input or output.
  • Full: everything in order: words, reasoning (quoted under Reasoning), and every call with its input and its output or error in fenced blocks. Fences outrun any backticks inside them. Each output is cut at 4,000 characters, ending in …truncated.
  • An agent's tab only holds its transcript while it's shown, so the copy reads the whole transcript from the service. A running agent's held long-poll would make that wait up to 20s, so the read stops at the first page shorter than a full page. That needs the page size on the wire: new CODING_AGENT_BOUNDS.MESSAGES_PAGE = 200, and the coder transcript route now passes it to listMessagesPast. Same value as today, so there's no server behaviour change; the bound just can't drift from the client's assumption anymore.
  • Clipboard goes through the existing WINDOW_COPY_TEXT act with no custom "Copied" UI. #1993 (copy feedback) hadn't merged when this was ready, so there's a TODO(copy-feedback) at the one call site to switch to copyWithToast(words, "Transcript copied").
  • Nothing is copied beyond what the tab already draws.

Pinned tabs (per plan)

  • Pin tab moves the tab to the end of the pinned group at the start of the strip, VS Code style. It keeps its icon and label and wears a small pin glyph where the × was.
  • The pin glyph is a button that unpins. Neither × nor Delete closes a pinned tab.
  • Close in the menu unpins and closes, as VS Code's context-menu Close does on a pinned editor.
  • Pins are stored per plan in #1975's plans[planId] state (pinned, optional, so old storage still reads) and survive restarts.
  • A pinned fixed tab is also "opened" on the plan, so it stands even when its kind holds nothing yet, and stays open after unpinning.
  • Pinned subagent and file tabs are reopened at launch, where they'd otherwise be dropped. A draft's pin is kept nowhere, like the draft itself.
  • Dragging (#1935/#1955) and ⇧⌘←/→ reorder within the pinned group and within the unpinned fixed group, never across. TabStrip/useTabDrag take runs (sizes of consecutive movable groups) instead of a single movable count.

⌘W closes the chosen tab; Close Window moves to ⇧⌘W

  • ⌘W closes the side panel's chosen tab while the panel is showing. It's the same action as the menu's Close, so it never stops an agent, and on a pinned tab it unpins and closes. The next tab is chosen by the existing rule. The chosen tab's Close item shows ⌘W at its end.
  • With nothing closable (panel hidden, panel open with no tab, or only the plan column's tab), the keymap doesn't claim ⌘W and nothing happens, so Luke's one window is never hidden by accident.
  • The menu bar had no Close Window item at all: Electron's windowMenu role on macOS holds Minimize, Zoom and Bring All to Front, and the custom File menu had no close. File now has Close Tab ⌘W and Close Window ⇧⌘W, as in Chrome and VS Code. Close Tab hands the window its command. Close Window closes the window main-side, which hides it as every close does (panel-manager.ts).
  • Both are rows in the shortcut table (#1844), so the menu, the hovers and the Keyboard shortcuts page (Window group) all list them, and the voice hotkey recorder refuses them like any other window chord.
  • ⌘W does nothing in a macOS text field, so it isn't marked editsText and the table's normal handling applies.

Screenshots (headless Chrome, real DesktopPlans + useSidePanel + stylesheet, synthetic data)

Pinned tabs (Work and an agent pinned), dark / light:

Right-click on a pinned agent tab, dark / light:

Tests

  • transcript-copy.test.ts: the exact concise and full markdown for a fixture transcript (reasoning, run, patch, delegation, failed call, developer follow-up); truncation and fence escaping; a worker's copy; whole-transcript paging stopping at a short page; a refused or rejected read copies nothing.
  • desktop-plans.test.ts:
    • menu items per tab kind, and ⇧F10 opening the menu on the focused tab;
    • Close, and Close others keeping pinned + clicked tabs without stopping an agent (dimmed when nothing else would close);
    • pin moves the tab to the start with a pin glyph, Delete doesn't close it, Unpin puts it back, Close unpins and closes;
    • pins persist per plan across a relaunch, including a kind with no content, and not on another plan;
    • drags stay within pinned or unpinned;
    • both copies put the expected markdown on the clipboard for an agent (read from the service) and a planning subagent.
  • desktop-plans.test.ts (⌘W): it closes the chosen tab, unpins and closes a pinned one, stops no agent, and chooses the next tab; with the panel hidden or no tab open, the key goes unclaimed.
  • app-menu.test.ts: File's Close Tab ⌘W sends its command on a click and does nothing on an unclaimed key. Close Window ⇧⌘W closes the window on its key or a click. This uses the file's existing fake BrowserWindow (the vi.mock("electron") double it already uses), not a new Effect Layer, since app-menu.ts is plain functions with no Context.Tag.
  • use-side-panel.test.ts: pinned file and subagent tabs reopen at launch, a draft pin isn't kept, and closing a pinned tab unpins it.

./scripts/check.sh exits 0 (knip, lint, discarded-effect, typecheck, tests).

CI could not verify the Mac UI, and ./scripts/verify.sh was not run (Linux cloud VM). The screenshots are headless Chrome, not Electron. LUKE-159 is the manual Mac pass.

🤖 Generated with Claude Code