feat(desktop, chat): center pane area for terminals and chats, /mcp picker, edit a sent message, and ACP sessions API + CLI

Merged
SA
10d ago
+10746-1605

This PR consolidates several pieces of work so they ship together:

PartAuthorWhat
A@saddlepaddleCenter pane area holds terminals and chats; smoother right sidebar
B@AviPeltz/mcp picker in the chat composer (was #8578)
CHarshithEdit a sent message (replaces /rewind), tool-run transcript, ACP sessions API + CLI with automation routing, chat history picker

A. Center pane area and right sidebar (@saddlepaddle)

With right-pane-area on, the center pane area only holds terminals and chats. Everything else opens on the right. The right sidebar also animates and resizes smoothly, without rebuilding its panes.

Part 1: Pane placement (flag on)

Why

The right pane area replaces the old files/changes sidebar. The center is for working sessions; viewers belong beside them.

What / How

  • A PaneTarget { store, reveal } replaces raw stores in the openers (useWorkspacePaneOpeners, useWorkspaceFileNavigation, useWorkspaceHotkeys, deep links, activity menu, subagent opens). page.tsx points that viewer target at the right store when the flag is on, else at the center.
  • moveViewerPanesToRight runs on load and on every center change. It moves any non-terminal, non-chat pane into its own right tab. Tabs that mix kinds are split apart.
  • Terminal, page, file and chat link clicks always land on the right when the right area exists, so the "Open in right pane" modifier now matches a plain click.
  • Center "+" menu: Terminal and Chat only. Chat opens with the last-used chat agent (see Part 5); you switch agent in the new chat screen. Desktop moves to the right "+" menu (sandboxes only).
  • Removed: Move to Center, Merge into center.
  • The right sidebar closes itself when its last tab closes.

Keyboard shortcuts

ShortcutAction
⌘⇧J (Ctrl+Shift+Alt+J)New Chat (Chat category, next to ⌘J Focus Chat Input)

⌘⇧T stays free for a future v2 reopen-closed-tab.

Part 2: Right-area opening rules

createWorkspaceStore({ placePane }) in @superset/panes, with openPane({ pane, beside? }). The center keeps its behaviour (split beside the anchor or the active pane); the right store passes placeInTabs. Opening a pane on the right:

  1. If the item is already open, focus that tab (each opener checks this first).
  2. Else replace an unpinned pane of the same kind in any tab (preview behavior; double-click still pins).
  3. Else add a tab.

It never splits by itself. openBesidePane (the Files, Changes, Review and Pages lists) and the "open Changes in a pane" setting follow the same rules. Users can still split with the split button, the right-click menu, or drag and drop.

Part 3: Sidebar motion and performance (all users unless noted)

Separator drags (measured over CDP in a dev build)

Every drag frame used to re-render the whole left workspace list.

  • Right: the width was saved each frame to v2UserPreferences, which about 19 hooks watch. That includes useTagFolderContext in every sidebar row.
  • Left: DashboardLayout subscribed to the whole sidebar store, and its providers handed fresh values to every row.

Fixes:

  • ResizablePanel commitOnRelease tracks the width locally and saves it on release.
  • The left width moved into WorkspaceSidebarPanel, its only subscriber. That is what fixed the left drag: the sidebar is passed in as children, so React skips it when the panel re-renders. DashboardSidebar is also memoized, which only saves re-renders when DashboardLayout re-renders for other reasons.

Result: 75–83 ms → 8.3 ms per frame, no long tasks, on both sides.

Open / close / expand (SidebarSlide), modelled on Codex's split view

  • The panel is anchored to the window's right edge and uses one element tree in every mode, so switching modes never remounts panes. Transitions run 300 ms ease, with contain: strict while moving.
  • Open/close animate the space taken from the center on the same curve, so the center follows the panel edge and the composer never cuts off and then jumps.
  • Expand overlays the center, which stays mounted and inert, instead of moving tabs. Content resizes with the panel. The persisted rightPaneAreaExpansion snapshot and expandPaneArea are deleted.
  • Flag on only: after the first open, the right area stays mounted while closed (invisible and inert). Terminals keep running and browser pages stay loaded. It deliberately doesn't use React <Activity>, which would run effect cleanups and detach terminals.
  • Browser panes: their webviews are hoisted to <body>, so browserRuntimeRegistry.relayout() now clips each one to its clipping ancestors (visibleClipPath) and parks it when hidden. The slide calls it on every frame.

Manual QA (dev app, driven over CDP)

  • Separator drag, both sides: 8.3 ms p50 per frame, no long tasks.
  • Open/close/expand/restore: no long tasks in the clean run. The right-area DOM survives every transition. Filmstrips show no cut-then-jump.
  • Terminal in the right area: exactly 1 SIGWINCH per expand and per restore, 0 for close/reopen.
  • Browser pane in the right area: clipped during the slide, parked while closed, follows expand/restore, same webContentsId throughout.
  • Closing the last right tab auto-closes the sidebar.
  • 7 rapid toggles and 5 rapid expands settle in the right state, with no errors.
  • inert blocks focus into the closed right area and into the covered center.
  • Flag off: the old sidebar opens and closes, renders correctly, no errors, and unmounts when closed.
  • placePane clicking through files in the Files tree (unit-tested only; not driven in the app)
  • Packaged build

Testing

  • bun run typecheck (desktop): clean apart from existing better-auth session-type errors in _authenticated/layout.tsx and friends, which aren't from this PR.
  • bunx tsc --noEmit + bun test in packages/panes: 123/123.
  • bun test src/renderer (desktop): 6846 pass, 1 fail. The failure is in useComposerDictation, a Lexical HISTORY_MERGE_TAG import-order error that also fails on its own and isn't touched here.
  • bunx biome check: clean. bun run check:i18n: 0 missing.
  • New tests: SidebarSlide (modes, no remount, keep mounted, rapid toggles); right-store placement rules; visibleClipPath; moveViewerPanesToRight; subscribePaneAreaEmptied; updated link-action and forwarded-hotkey tests.

Known Limitations

  • With the flag off, the old sidebar has an 80–180 ms long task on each toggle. No baseline was measured, so it may predate this PR.
  • While expanded, center shortcuts (e.g. ⌘T) still target the hidden center store.
  • A browser's "open in new window" still splits, because it uses the same split action as the button.

Rollback

Revert the commit. The removed persisted rightPaneAreaExpansion field is optional, so older builds read rows without it.


B. /mcp picker (@AviPeltz, from #8578, reworked in review)

  • /mcp in the chat composer opens a searchable list of the MCP servers the chat's agent has in this workspace. A pick becomes a chip. Typing /mcp and pressing Enter opens the same picker.
  • On send, the hint naming the picked servers goes to the agent as a hidden content part. The stored and shown message is exactly what the user typed (bubble, Copy, Edit). Hidden parts reach the agent wrapped in <superset-context>, so a replay of the agent's history (session/load) drops them again.
  • Superset plugin servers that aren't installed or have no connected account show Install / Connect. Connect uses the Plugins page's connect-first flow (ConnectConnectorDialog connectOnly → useInstallAfterConnect), so a plugin is installed only after its account is connected.
  • Host query agentTooling.listMcpServers reads Claude Code and Codex config files for a workspace (files only; nothing is started or contacted). It returns { servers, supported }; each server is config only (name, agent, scope, transport, url, configPath, state). The desktop joins it with the plugin catalog: a proxied server is matched to its plugin by its /mcp/plugins/<marketplace>/<plugin> URL, others by name or endpoint.
  • A message replayed from the agent's history has no recorded chip elements, so @mcp/ tokens and a leading /command become chips from the text.

C. Editing, transcript, sessions API (Harshith)

Edit a sent message (replaces /rewind)

  • Hover a user message → Edit. Sending the edit rewinds the conversation only: the chat continues in a new session holding the transcript before that message, and the edited message is sent. Files are never touched (the earlier checkpoint/restore design was removed).
  • Claude, Codex and pdev fork natively at the last reply before the message (session/fork with a cut point), and the fork is resumed quietly.
  • Other agents get the earlier conversation as a hidden part of the same prompt as the edited message, so there is no extra acknowledgement turn. The rewound session is seeded with the earlier events, so nothing is parsed back out of our own prompt text.

Transcript: tool runs

  • A run of tool calls stays folded ("Ran 3 commands") while the agent works, as in #8339. The step still running shows under the fold with its command and a ticking timer: Running bun test 42s. The summary counts only finished steps, so it never says "Ran" about something still running. (Follows the "Why the chat feels slow while the agent works" page.)
  • The agent's thinking between tool calls folds into the run, in order. A run that stays one call is still a plain row.
  • The "Working for…" clock sits under the latest prompt of the turn, so a message steered in mid-turn shows the live indicator below it.

ACP sessions API and superset sessions CLI

  • Host sessions.*: capabilities, create, list, get, read, send, resume, close. No host-side feature-flag check: the desktop UI and automation dispatch keep their acp-chat gates, and no host call depends on an API round trip.
  • CLI: superset sessions create (new chats only), import --agent --native-session (adopt a session the agent started outside Superset), resume, send (resumes a stopped chat first and reports the session the prompt went to), read (prints the cursor for older events), list, close, open.
  • Automation dispatch launches through launchAgentSession: sessions.create for agents the host can chat with, agents.run otherwise or on hosts without sessions. Chat runs are recorded as chat with their session id, and run links carry ?sessionId= so they open the chat. New cloud boxes launch on first boot and report back with reportCloudLaunch.

Other chat changes

  • Provider-scoped chat history picker: saved chats plus the agent's own sessions (discoverSessions); an older host without it shows "not supported".
  • The CLI | Chat surface toggle is removed.
  • New chats (center "+" → Chat, ⌘⇧J) open the last-used chat agent.
  • Agent failures are no longer silent: a turn that ends with no output shows an error notice, and recovery shows a dead session's real error via getSession's lastError.
  • Switching provider in a chat with history offers Start fresh or Fork conversation.

API compatibility

  • New host procedures are unreleased: sessions.*, agentTooling.listMcpServers, chat rewindSession, discoverSessions. Clients fall back on hosts without them (agents.run for dispatch, "not supported" in the history picker).
  • Additive fields only on existing shapes: optional hidden on text content parts, agentMessageId on agent messages and lastError on getSession.

Testing

  • bun test (bun 1.4.3): trpc 674, cli 438, shared 1432, chat 83, chat-runtime 301, chat-ui 34, panes 121, desktop v2-workspace 800, all pass.
  • host-service: 2603 pass, 2 fail on the 5 s timeout under machine load (terminal disposal, which also fails on clean main locally; getGitStatusPartial convergence, in code this PR doesn't change).
  • Typecheck clean for desktop, host-service, cli, chat, chat-runtime, chat-ui, shared, panes, apart from existing better-auth session-type errors. bun run lint:fix clean. bun run check:i18n: 0 missing.

Manual QA (dev app over CDP, Claude and pdev)

  • Folded run with the running step and timer under it ("Ran 1 command" / Running sleep 15 && echo done 5s); finished runs fold; nothing opens or closes while the agent works
  • Thinking folds into runs; "Working for" sits under a steered message
  • /mcp: hint never in the bubble, also after a restart replays the agent's history; replayed @mcp/ tokens render as chips; typed /mcp + Enter opens the picker; proxied plugin servers without an account show Connect
  • Connection notice ("Starting the agent…") aligned with the composer text
  • Edit with the new one-prompt rewind (unit-tested; driven in the app before that change)
  • placePane on the right (unit-tested only)
  • Codex live; packaged build

Follow-ups (not in this PR)

  • Cloud dispatch: provision, wait for the host to come online, then launch through the same launchAgentSession path, replacing reportCloudLaunch.
  • One dispatch pipeline: resolve target → HostCaller + workspace → launchAgentSession → recordDispatchedRun.
  • Chat runtime: one ACP connection per agent per workspace, panes bound to a chat id.