feat(desktop, chat): center pane area for terminals and chats, /mcp picker, edit a sent message, and ACP sessions API + CLI
This PR consolidates several pieces of work so they ship together:
| Part | Author | What |
|---|---|---|
| A | @saddlepaddle | Center pane area holds terminals and chats; smoother right sidebar |
| B | @AviPeltz | /mcp picker in the chat composer (was #8578) |
| C | Harshith | Edit 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.tsxpoints that viewer target at the right store when the flag is on, else at the center. moveViewerPanesToRightruns 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
| Shortcut | Action |
|---|---|
| ⌘⇧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:
- If the item is already open, focus that tab (each opener checks this first).
- Else replace an unpinned pane of the same kind in any tab (preview behavior; double-click still pins).
- 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 includesuseTagFolderContextin every sidebar row. - Left:
DashboardLayoutsubscribed to the whole sidebar store, and its providers handed fresh values to every row.
Fixes:
ResizablePanelcommitOnReleasetracks 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 aschildren, so React skips it when the panel re-renders.DashboardSidebaris also memoized, which only saves re-renders whenDashboardLayoutre-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, withcontain: strictwhile 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 persistedrightPaneAreaExpansionsnapshot andexpandPaneAreaare 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>, sobrowserRuntimeRegistry.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
webContentsIdthroughout. - Closing the last right tab auto-closes the sidebar.
- 7 rapid toggles and 5 rapid expands settle in the right state, with no errors.
-
inertblocks 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.
-
placePaneclicking 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.tsxand friends, which aren't from this PR.bunx tsc --noEmit+bun testinpackages/panes: 123/123.bun test src/renderer(desktop): 6846 pass, 1 fail. The failure is inuseComposerDictation, a LexicalHISTORY_MERGE_TAGimport-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)
/mcpin 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/mcpand 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.listMcpServersreads 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/commandbecome 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/forkwith 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 theiracp-chatgates, 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.createfor agents the host can chat with,agents.runotherwise or on hosts withoutsessions. Chat runs are recorded aschatwith their session id, and run links carry?sessionId=so they open the chat. New cloud boxes launch on first boot and report back withreportCloudLaunch.
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'slastError. - Switching provider in a chat with history offers Start fresh or Fork conversation.
API compatibility
- New host procedures are unreleased:
sessions.*,agentTooling.listMcpServers, chatrewindSession,discoverSessions. Clients fall back on hosts without them (agents.runfor dispatch, "not supported" in the history picker). - Additive fields only on existing shapes: optional
hiddenon text content parts,agentMessageIdon agent messages andlastErrorongetSession.
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
mainlocally;getGitStatusPartialconvergence, 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:fixclean.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)
-
placePaneon 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
launchAgentSessionpath, replacingreportCloudLaunch. - One dispatch pipeline: resolve target →
HostCaller+ workspace →launchAgentSession→recordDispatchedRun. - Chat runtime: one ACP connection per agent per workspace, panes bound to a chat id.