Skip to content

Switch chats between Chat UI and a terminal without forking - #31595

Open
brennanb2025 wants to merge 383 commits into
brennanb2025/canonical-workspace-surfacesfrom
brennanb2025/flip-codex-switch
Open

brennanb2025 wants to merge 383 commits into
brennanb2025/canonical-workspace-surfacesfrom
brennanb2025/flip-codex-switch

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Brennan requested an ad hoc build from the finished branch to try it. Do not merge this PR. The separate proposal to keep the reply going across the switch is on hold.

Files Added Deleted Net
Test 193 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​13855 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1362 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​12493
Prod 445 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​14831 $\color{#cf222e}{\Huge{\mathbf{−}}}$​3687 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​11144

The problem

A conversation with an agent could live in Chat UI or in the agent's own terminal, but moving it meant opening a copy in a second tab ("Continue in Chat UI" / "Continue with Terminal Agent"). That left two views of one conversation, sometimes two agents writing to it, and tabs that piled up.

What changes for users

Switch between Chat UI and the agent's terminal in the same tab, continuing the same conversation with the same agent. The destination takes the source's place, position and pin. Switching one pane of a split keeps its siblings and opens the chat right beside the split.

The switch appears in the pane header, the tab hover card, the tab menu and Session History while the Chat UI setting is on. Claude, Codex, Grok, OpenCode, OpenCode 2 and Pi switch in place when their host supports it. omp, and any agent on a host that can't switch, keep main's "Continue in Chat UI" / "Continue with Terminal Agent", which open a copy beside the source.

The switch never asks. It stops the agent the ordinary way, even mid-reply, waits for proof that it exited, brings in any conversation the terminal wrote, and then shows the destination. A stopped turn reads "Interrupted". Switching from a terminal closes that whole terminal, as "Continue in Chat UI" does on main: an agent paused with Ctrl+Z, exec claude, or a job running beside the agent ends with it. In a split, only the switched pane closes.

If a switch fails after the agent was stopped, the source tab stays where it was with " stopped" and its last screen, plus Restart for a fresh shell and Switch to Chat UI to try again. Paired desktops show the same. Failures name the agent and say what to do, once.

How it works

The host runs the switch as one operation for local and paired windows alike: ordinary Stop, proof of process exit, import of what the terminal wrote, then one layout write that replaces the source tab. A retry after a lost reply reuses the first attempt's terminal instead of starting another. Placement and pin travel as optional hints on the existing tab messages, so nothing new is saved and mixed client and server versions keep working; there is no minimum server version.

Compatibility and differences

Intended:

  • Same slot, one write. The destination takes the source's place and pin in one write by the layout owner, the common pattern. If the tab moved or closed during the switch, the destination opens as an ordinary tab ("The original tab was kept") rather than refusing, because the agent is already stopped.
  • Stop with proof first. Orca won't move a conversation whose agent it can't prove has exited (for example over SSH or after a lost connection) and says so, naming the agent. Losing contact is never treated as proof.
  • Retry reuses the first attempt. Where no reuse key exists (a chat never saved, SSH, an older host), a real connection loss can leave one extra terminal process; per Brennan (10-30) a lost connection may be a little bad but never breaks the app or loses the session.
  • Pending state on the source. The control disables at once and shows a spinner after 200 ms; the old view stays until the new one is ready.
  • "Interrupted". A user's Stop, the CLI's own stop record (Claude, Codex, Pi) and a stop the switch proved all read "Interrupted", on the right turn, in transcript order.
  • Teardown text hidden. The CLI's exit text is hidden from the pane being replaced. Older paired clients without the newer stream notice can still see it, because hiding it from them could leave a silent gap if the stop isn't proven.
  • Stopped source everywhere. The host publishes a failed switch's stopped terminal to every viewer, so paired desktops show the same stopped state and retry in place. On the phone, that terminal is not shown, as on main when a terminal exits; the switch is retried from the desktop or the chat list.
  • Header control as on main. "Switch to Chat UI" appears on every pane main offered "Continue in Chat UI" on, including a pane whose agent exited. A pane Orca can't capture opens the chat selected, right beside it. Session History does the same when a stopped paired pane can't be captured.
  • Another live writer. The switch refuses only while Orca's agent status shows the conversation running somewhere other than the pane being switched, the same rule Close the terminal before continuing its conversation in chat #36732 gives main's "Continue in Chat UI". If that status is stale the switch goes ahead: letting the user continue beats refusing wrongly (Brennan 10-31).
  • Codex's stopped reply text. When Codex is stopped mid-reply in its terminal, the chat shows the prompt and "Interrupted" but not the partial reply, because Codex doesn't save that text. Claude does, so its partial reply appears. This is the same as main's "Continue in Chat UI".
  • In-place agents lose the copy entry (Brennan 10-19). For in-place agents the switch replaces "Continue with Terminal Agent" and "Resume in New Terminal".

Temporary:

  • Phone teardown text. The phone app's terminal view can still show the CLI's exit text briefly during a desktop switch. Tracked in STA-13131.
  • Orchestration workers. Worker follow-ups don't yet follow a conversation switched between Chat UI and terminal. Tracked in STA-13132.
  • Lost-answer message. When the connection drops mid-switch, the message can be missing or generic; the conversation is kept and a retry works. Tracked in STA-13133.
  • Leftover Stop note. In a narrow case, a "could not confirm Stop" note keyed by the Stop's operation can remain after the switch proved the exit. Tracked in STA-13136.
  • Agent paused with Ctrl+Z. Switching from an agent paused mid-reply closes it and opens the chat as usual, but that turn shows the "stopped while this response was in progress" notice instead of "Interrupted", because Orca drops a paused agent's status at the shell prompt. Per Brennan (10-31) this is handled for everyone in a follow-up.
  • Grok and OpenCode. A Stop made in their CLI before switching reads as an interruption rather than "Interrupted" until each agent's stop record is read. Tracked in STA-13146.

Verification

CI is green on 7f5e913. Running-app QA used isolated test copies, with agents signed in through the load balancers.

  • macOS, on 7f5e913 (M4Air):
    • Round trip with Claude and Codex: chat → terminal → chat, no dialog even mid-reply, the same tab index, one CLI at a time, prompts in order, and "Interrupted" on the stopped turn only.
    • Another agent holding the conversation: refused in both directions with one "Claude is open in another chat or terminal…" message, and nothing stopped. It switches once that agent exits.
    • A chat → terminal switch whose answer was lost (a paired client's connection cut mid-switch): it recovered in the same tab with exactly one CLI on the conversation.
    • A switch that fails after the stop: the tab stays as a stopped terminal with one message. Retrying puts the chat in place.
  • macOS, on 94db3a9 (flows unchanged since):
    • Paired-client failure, reload and History's in-place switch.
    • Ctrl+Z and sleep 600 & beside Claude: the terminal closes and the chat works.
    • No "Terminal exited" or CLI exit text in any frame, on the host or a paired client.
  • Windows:
    • Round trip with Codex twice and Claude once on 13b8381, including a graceful restart. 7f5e913 reverts only how a Ctrl+Z-paused turn is labelled, and the macOS run re-proved that labelling on the final head.
    • A child process started beside Claude ends with the terminal (4d27b2e).
  • Not verified in the running app: Grok, OpenCode and Pi switches. They share the code above and are covered by recorded-transcript tests.

Details, screenshots and what wasn't verified are in the review status comment. The ownership prerequisite (#30930) is merged; this PR targets main.

Author: @BrennanKB5

@brennanb2025

brennanb2025 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Not ready — 2026-10-27. Targeted QA found two Codex return failures. The review-completed marker is withheld.

This PR keeps a Codex conversation in the same tab when moving between chat and CLI, brings terminal turns back into chat, and makes switch failures actionable. The correction uses one bounded host-owned operation, shared renderer dispatch and cheap live availability facts. Readable history appears before provider startup completes. Older peers and other agents retain their supported existing paths.

Published head: e8c26d523b4825b03157ced8fa1952bdb6945677. The reviewed correction, current main integration and inert foundation conservation checks are committed. Later changes consolidate an equivalent admission call, repair test fixtures and counted baseline entries, regenerate two catalog imports, and handle a generated-only commit in the normal hook. Independent narrow reviews agreed; the normal hook passed. No runtime patch has been made from the latest QA failures.

CI is not green. On the preceding head, the four root compilers, layout-writer checks, E2E type ratchet and mobile checks passed; a stale generated catalog failed the static job and dependent aggregate. The catalog is corrected in this head. New main conflicts now suppress the source workflows: the single current-head watch saw only the successful branch-clean check and skipped bot review. Conflict resolution and full CI remain owed.

Targeted Mac QA ran on b7b716b1ddc45b4338ec14d56435f87f524fc377, tree 98a396b886b99cff9f77a64030fabe51f6a3ea14, after a same-profile graceful upgrade from ac154dc6a3ea0d6b0cf82f66127f36c894ab4d4c. The subsequent equivalent admission and support-only changes carry that evidence.

Flow Result
Saved Codex chat → CLI Passed: same slot, source retired, inherited history and a real CLI reply.
Empty Codex chat → fresh CLI Passed: immediate fresh CLI, source retired, real reply and learned conversation ID.
Learned-ID CLI → chat after ordinary exit Failed: transcript admission refused the return; terminal retained. The exact rejection predicate is still under audit.
Saved cold CLI → chat Failed: history appeared and source cleanup completed, but the selected Codex start was then refused as a running terminal job. The later writer association is unresolved; the destination also moved from slot 1 to slot 3.
Shared Claude continuation Functional pass: inherited history and a distinct real CLI reply through its existing continuation action. Callback-observation limits remain.

Windows is still completing its originally assigned targeted flows. No completed cross-platform verdict is claimed. Paired slow/lost-answer actions, the selected committed start-failure/unused-destination outcomes, cleanup with newer work, and the suspended-tooltip screenshot remain incomplete. A Mac failure attempt recovered automatically with a real reply, which does not prove the once-only committed-failure case. Earlier accepted History, recovery, Chat UI-off and WSL evidence covers only unchanged flows and retains its original limits; it does not fill these gaps.

Both return responses were captured from one real click with matching source identity. Missing guard/holder evidence is not grounds to remove safety checks. A later prompt in the saved-return rig was sent after the refusal and earns no successful return or reply. Mac cleanup is verified: owned apps, observers and recorded roots are gone, admitted ports are free, and all five real-config hashes are unchanged.

For Brennan’s review

Learned-ID return: the terminal remains after the refused switch. This frame shows the ordinary exited shell and tooltip, not a captured refusal toast.

Learned-ID return retained terminal

Saved cold return: actual inherited history is readable, but the selected start is refused. This is a failure screenshot, not a passing return.

Readable history with refused Codex start

Claude’s existing continuation completed a real inherited-context reply.

Claude continuation reply

Layout-writer ratchet

The window is the shipped layout owner. These counted writes are the switch's fenced retirement/restoration and launch-verdict producers; they will be ported when the main-owned layout path replaces the window path. The additional main call is post-dispatch recovery with distinct preconditions from fresh/replacement binding. The coordinator authorized exactly these three counted rows; scanner rules and allowed paths are unchanged.

The base dependency #30930 and inert foundation #34384 are merged. No merge or readiness approval is requested while the failures, incomplete flows and source CI remain outstanding.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 21, 2026 21:44
@brennanb2025
brennanb2025 changed the base branch from brennanb2025/flip-ownership-admission to main October 25, 2026 21:39
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 69bd7fff-a458-486d-b408-e5491373d777
Commits

Reviewing files that changed from the base of the PR and between d24a5a8 and b5e75c7.

Files selected for processing: 1
  • src/main/native-chat/agent-session-wire/structured-agent-session-switch-to-chat.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Walkthrough

The pull request adds Codex in-place switching between native chat and CLI sessions, with capability checks, switch admission, RPC methods, and renderer controls. It adds incarnation and original-surface validation to terminal-close paths, with persistence and renderer-retirement changes. It also adds captured tab-placement handling and pane-conversation state retention. Tests cover switching, stale terminal identity, surface retirement, and placement behavior.

Merge Risk: Minimal · previous review b5e75

The two previously reported issues do not require fixes at this head. No actionable merge-blocking defect was established in this review scope.

Pre-merge checks: Passed 4
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly summarizes the main change: switching chats between Chat UI and a terminal while keeping the conversation intact without forking.
Description check Passed PASS: The description is detailed and covers the problem, user-visible behavior, implementation approach, compatibility limits, known issues, testing results, platforms tested, and unverified areas. I…
Full details: Description check

Explanation

PASS: The description is detailed and covers the problem, user-visible behavior, implementation approach, compatibility limits, known issues, testing results, platforms tested, and unverified areas. It references visual proof in the review comment and identifies linked issues. Several template headings and checklist items are not reproduced verbatim, and AI disclosure is absent, but these omissions do not prevent the description from being mostly complete.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Resolve merge conflicts
  • Resolve merge conflict in branch brennanb2025/flip-codex-switch
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments: 1
src/renderer/src/components/native-chat/NativeChatTopRightControls.tsx: 1

34-42: Trivial · Performance & Scalability · Quick win

Cache the identity-derived host scope.

useAppStore evaluates this selector on store updates. resolveTabSessionHistorySubject then builds the full session-list request, including worktree indexing and project/scope derivation, although this component only needs executionHostScope.

The primitive result limits component re-renders, but it does not avoid the selector work. NativeChatResumeInCliButton.tsx already uses the relevant memoized selector pattern. Reuse it here, but cache the result of resolveNativeChatCliForkIdentity; caching the full subject with only the identity selector’s inputs would omit dependencies used by resolveAiVaultPanelSessionListRequest.


Review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f57fe104-a0e2-4c98-83d0-f8f79f117491
Commits

Reviewing files that changed from the base of the PR and between 883b768 and c653f4c.

Files ignored due to path filters: 1
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*
Files selected for processing: 232
  • config/main-layout-writer-baseline.txt
  • config/scripts/check-layout-writer-ratchets.mjs
  • config/scripts/check-layout-writer-ratchets.test.mjs
  • mobile/src/session/use-mobile-session-tab-application-launch.test.ts
  • src/main/agent-hooks/managed-hook-stdin-lifecycle.test.ts
  • src/main/agent-hooks/server-launch-resume-identity.test.ts
  • src/main/ai-vault/structured-chat-cli-fork.ts
  • src/main/ai-vault/structured-session-ownership.test.ts
  • src/main/ai-vault/structured-session-ownership.ts
  • src/main/daemon/daemon-pty-session-control.ts
  • src/main/daemon/daemon-shutdown-final-checkpoint-caller-deadline.test.ts
  • src/main/ipc/pty/host-env/saved-resume-admission.ts
  • src/main/ipc/pty/ipc/renderer-kill.ts
  • src/main/ipc/pty/provider/shutdown-detect.ts
  • src/main/ipc/pty/runtime/controller-deps.ts
  • src/main/ipc/pty/runtime/kill-same-id-respawn.test.ts
  • src/main/ipc/pty/runtime/kill.ts
  • src/main/ipc/pty/runtime/spawn-commit-response.ts
  • src/main/ipc/pty/runtime/spawn-state.ts
  • src/main/ipc/pty/session.ts
  • src/main/ipc/pty/spawn-commit-assembly.ts
  • src/main/ipc/pty/spawn-commit-publication.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-conversation-lifetime.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-create-adoption.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-types.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-idle-sweep.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-switch-admission.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-switch-to-chat.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-terminal-writer-acquire.test.ts
  • src/main/native-chat/agent-session-wire/structured-provider-session-ownership.ts
  • src/main/persistence/loading-store/profile-state-delayed-authority-fixture.ts
  • src/main/persistence/loading-store/pty-spawn-commit-dependencies-fixture.ts
  • src/main/persistence/terminal-topology/layout-command-persistence.ts
  • src/main/persistence/terminal-topology/original-terminal-close-store.test.ts
  • src/main/providers/local-pty-provider-shutdown.test.ts
  • src/main/providers/local-pty-provider.ts
  • src/main/providers/local-pty-termination.ts
  • src/main/runtime/__fixtures__/orca-runtime-terminal-close-continuity-fixtures.ts
  • src/main/runtime/__fixtures__/terminal-close-runtime.test-fixture.ts
  • src/main/runtime/mobile-session-layout-projection.ts
  • src/main/runtime/mobile-session-switch-placement.test.ts
  • src/main/runtime/mobile-session-tab-close-outcome.ts
  • src/main/runtime/orca-runtime-original-terminal-close-store.test.ts
  • src/main/runtime/orca-runtime-original-terminal-close.test.ts
  • src/main/runtime/orca-runtime-terminal-close-continuity.test.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/orchestration/chat-assignee.test.ts
  • src/main/runtime/orchestration/chat-assignee.ts
  • src/main/runtime/rpc/methods/session-tabs.ts
  • src/main/runtime/rpc/methods/structured-agent-session-gate-classification.test-fixture.ts
  • src/main/runtime/rpc/methods/structured-agent-session-switch-to-chat.ts
  • src/main/runtime/rpc/methods/structured-agent-session-switch-to-terminal.ts
  • src/main/runtime/rpc/methods/structured-agent-session.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session.ts
  • src/main/runtime/rpc/methods/terminal/terminal-lifecycle-methods.ts
  • src/main/runtime/rpc/runtime-close-attribution.test.ts
  • src/main/runtime/runtime-layout-apply.test.ts
  • src/main/runtime/runtime-layout-apply.ts
  • src/main/runtime/runtime-notifier-contract.ts
  • src/main/runtime/runtime-pty-controller-contract.ts
  • src/main/runtime/runtime-terminal-contracts.ts
  • src/main/runtime/session-graph/session-window-graph.ts
  • src/main/runtime/session-lifecycle/close/close-mobile-row-in-main-layout.ts
  • src/main/runtime/session-lifecycle/close/close-mobile-session-tab.ts
  • src/main/runtime/session-lifecycle/projection/build-headless-mobile-session-browser-tabs.ts
  • src/main/runtime/session-lifecycle/projection/prune-mobile-session-tab-group-layout.ts
  • src/main/runtime/session-tabs/explicit-terminal-close-pty-stops.ts
  • src/main/runtime/session-tabs/explicit-terminal-close-stops.ts
  • src/main/runtime/session-tabs/explicit-terminal-tab-close.ts
  • src/main/runtime/session-tabs/paired-host-terminal-close.ts
  • src/main/runtime/session-tabs/structured-session-terminal-launch-retirement.ts
  • src/main/runtime/session-tabs/terminal-surface-close-commit.ts
  • src/main/runtime/structured-agent-session-runtime.ts
  • src/main/runtime/structured-agent-session-windowless-layout-host.test.ts
  • src/main/runtime/structured-chat-coordinator-mail-rig.test-fixture.ts
  • src/main/runtime/structured-chat-dispatch-worker-lifecycle.test.ts
  • src/main/runtime/terminal-intentional-stops.ts
  • src/main/runtime/terminal-surface-close.ts
  • src/main/runtime/terminal/identity/terminal-handle-bindings.ts
  • src/main/runtime/terminal/launch/failed-start-pane.ts
  • src/main/runtime/terminal/launch/mobile-terminal-create.ts
  • src/main/runtime/terminal/launch/mobile-terminal-owned-create.ts
  • src/main/runtime/terminal/launch/mobile-terminal-renderer-create.ts
  • src/main/runtime/terminal/lifecycle/floating-session-liveness.ts
  • src/main/runtime/terminal/output/provider-buffer-capture.ts
  • src/main/runtime/terminal/query/pty-terminal-summary.ts
  • src/main/runtime/terminal/recovery/resolve-terminal-pane.ts
  • src/main/runtime/workspace-session-terminal-tab-retirement-identity.ts
  • src/main/runtime/workspaces/workspace-process-listing.ts
  • src/main/window/terminal-tab-close-request-relay.ts
  • src/preload/api/pty-api.ts
  • src/preload/api/pty-bridge-session-control.ts
  • src/renderer/src/components/native-chat/NativeChatPaneHeader.test.tsx
  • src/renderer/src/components/native-chat/NativeChatPaneHeader.tsx
  • src/renderer/src/components/native-chat/NativeChatResumeInCliButton.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.history-read-state.test.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
  • src/renderer/src/components/native-chat/NativeChatTopRightControls.tsx
  • src/renderer/src/components/native-chat/native-chat-header-ownership.test.tsx
  • src/renderer/src/components/native-chat/use-native-chat-context-menu.test.tsx
  • src/renderer/src/components/native-chat/use-native-chat-context-menu.tsx
  • src/renderer/src/components/right-sidebar/AiVaultPanel.legacy-filter.test.tsx
  • src/renderer/src/components/right-sidebar/AiVaultSessionActionMenuItems.tsx
  • src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx
  • src/renderer/src/components/right-sidebar/AiVaultSessionDetailsSwitchButtons.tsx
  • src/renderer/src/components/right-sidebar/AiVaultSessionRow.structured-title.test.tsx
  • src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx
  • src/renderer/src/components/right-sidebar/AiVaultSessionSurfaceSwitchMenuItems.tsx
  • src/renderer/src/components/right-sidebar/AiVaultVirtualRow.test.tsx
  • src/renderer/src/components/right-sidebar/SessionRowTrailingActions.tsx
  • src/renderer/src/components/right-sidebar/ai-vault-history-native-return.test.tsx
  • src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork-copy.tsx
  • src/renderer/src/components/right-sidebar/ai-vault-session-in-place-switch.ts
  • src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts
  • src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-workspace.test.ts
  • src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-workspace.ts
  • src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat.ts
  • src/renderer/src/components/right-sidebar/use-ai-vault-panel-session-resume.ts
  • src/renderer/src/components/tab-bar/CliChatResumeMenuItems.tsx
  • src/renderer/src/components/tab-bar/NativeChatCliForkMenuItems.tsx
  • src/renderer/src/components/tab-bar/SortableTab.rename-shortcut.test.tsx
  • src/renderer/src/components/tab-bar/SortableTab.tsx
  • src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx
  • src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx
  • src/renderer/src/components/tab-bar/TabHoverCard.tsx
  • src/renderer/src/components/tab-bar/TabSessionSurfaceSwitchMenuItems.test.tsx
  • src/renderer/src/components/tab-bar/TabSessionSwitchHoverAction.tsx
  • src/renderer/src/components/tab-bar/cli-chat-resume-exit-return.test-fixture.ts
  • src/renderer/src/components/tab-bar/cli-chat-resume-feedback.ts
  • src/renderer/src/components/tab-bar/cli-chat-resume-mocks.test-fixture.ts
  • src/renderer/src/components/tab-bar/cli-chat-resume-parked-retirement.test-fixture.ts
  • src/renderer/src/components/tab-bar/cli-chat-resume-run.test.ts
  • src/renderer/src/components/tab-bar/cli-chat-resume-run.ts
  • src/renderer/src/components/tab-bar/cli-chat-resume-source.ts
  • src/renderer/src/components/tab-bar/cli-chat-resume-terminal-capture.ts
  • src/renderer/src/components/tab-bar/cli-chat-resume-transport.test-fixture.ts
  • src/renderer/src/components/tab-bar/native-chat-cli-switch-run.test.ts
  • src/renderer/src/components/tab-bar/native-chat-cli-switch-run.ts
  • src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts
  • src/renderer/src/components/tab-bar/tab-session-history-switch.ts
  • src/renderer/src/components/tab-bar/tab-title-tooltip.test.tsx
  • src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx
  • src/renderer/src/components/terminal-pane/TerminalPaneResumeInChatButton.test.tsx
  • src/renderer/src/components/terminal-pane/TerminalPaneResumeInChatButton.tsx
  • src/renderer/src/components/terminal-pane/pty-connection-types.ts
  • src/renderer/src/components/terminal-pane/pty-transport-types.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-close.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-mount-cleanup.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-mount-context.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-mount-preparation.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-mount-registration.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-pane-closed.ts
  • src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.ts
  • src/renderer/src/components/terminal-pane/terminal-presentation-retirement.test-fixture.ts
  • src/renderer/src/components/terminal-pane/terminal-presentation-retirement.test.ts
  • src/renderer/src/components/terminal-pane/terminal-presentation-retirement.ts
  • src/renderer/src/components/terminal-pane/use-terminal-pane-mount-lifecycle.ts
  • src/renderer/src/components/ui/hover-card.tsx
  • src/renderer/src/hooks/ipc-events/agent-status-partial-updates.ts
  • src/renderer/src/hooks/ipc-events/mobile-terminal-close-ipc-bridge.ts
  • src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge-placement.test.ts
  • src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge.ts
  • src/renderer/src/hooks/useIpcEvents-agent-conversation-cap.test.ts
  • src/renderer/src/hooks/useIpcEvents-agent-status-batch-projection.test.ts
  • src/renderer/src/hooks/useIpcEvents-agent-status-owner-ordering.test.ts
  • src/renderer/src/i18n/en-runtime-required.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/lib/activate-ai-vault-structured-session.ts
  • src/renderer/src/lib/ai-vault-tab-title-requests.ts
  • src/renderer/src/lib/in-place-switch-support.test.tsx
  • src/renderer/src/lib/in-place-switch-support.ts
  • src/renderer/src/lib/launch-ai-vault-session-placement.test.ts
  • src/renderer/src/lib/launch-ai-vault-session.test.ts
  • src/renderer/src/lib/launch-ai-vault-session.ts
  • src/renderer/src/lib/pane-manager/pane-inline-images.ts
  • src/renderer/src/lib/pane-manager/pane-lifecycle.ts
  • src/renderer/src/lib/pane-manager/pane-manager-tree-mutations.ts
  • src/renderer/src/lib/pane-manager/pane-manager-types.ts
  • src/renderer/src/lib/pane-manager/pane-manager.ts
  • src/renderer/src/lib/pane-manager/pane-split-close.ts
  • src/renderer/src/lib/pane-manager/pane-webgl-renderer.ts
  • src/renderer/src/lib/pending-conversation-surface-switches.ts
  • src/renderer/src/runtime/mounted-terminal-tab-registry.ts
  • src/renderer/src/runtime/structured-agent-session-client.ts
  • src/renderer/src/runtime/web-runtime-session-test-harness.ts
  • src/renderer/src/runtime/web-runtime-session-types.ts
  • src/renderer/src/runtime/web-runtime-terminal-create-operation.ts
  • src/renderer/src/runtime/web-runtime-terminal-placement-settlement.ts
  • src/renderer/src/runtime/web-runtime-terminal-switch-placement.test.ts
  • src/renderer/src/runtime/web-session-tabs-sync/agent-status-primitives.ts
  • src/renderer/src/runtime/web-session-tabs-sync/state.ts
  • src/renderer/src/store/agent-conversation-retention.ts
  • src/renderer/src/store/index.ts
  • src/renderer/src/store/slices/agent-pane-conversation.ts
  • src/renderer/src/store/slices/agent-status-authority-actions.ts
  • src/renderer/src/store/slices/agent-status-drop-actions.ts
  • src/renderer/src/store/slices/agent-status-drop-reducer.ts
  • src/renderer/src/store/slices/agent-status-live-actions.ts
  • src/renderer/src/store/slices/agent-status-live-reducer.ts
  • src/renderer/src/store/slices/agent-status-provider-session-actions.ts
  • src/renderer/src/store/slices/agent-status-provider-session.test.ts
  • src/renderer/src/store/slices/agent-status-runtime.ts
  • src/renderer/src/store/slices/agent-status-slice-contract.ts
  • src/renderer/src/store/slices/agent-status.ts
  • src/renderer/src/store/slices/store-test-helpers.ts
  • src/renderer/src/store/slices/worktrees/teardown/worktree-purge-surface-patch.ts
  • src/renderer/src/store/terminals/terminal-actions.ts
  • src/renderer/src/store/terminals/terminal-state.ts
  • src/shared/protocol-version.ts
  • src/shared/rpc-contract/session-tabs-schemas-params.ts
  • src/shared/rpc-contract/terminal-unary-params.ts
  • src/shared/runtime-terminal-contracts.ts
  • src/shared/session-tab-placement-work.test-fixture.ts
  • src/shared/session-tab-placement.ts
  • src/shared/structured-agent-cli-conversation-adoption.ts
  • src/shared/terminal-layout-leaf-retirement.ts
  • src/shared/terminal-tab-close.ts
  • tests/e2e/cli-chat-resume-terminal-capture.unit.test.ts
  • tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts
  • tests/e2e/cross-version-wire/structured-agent-session-host-fixture.ts
  • tests/e2e/cross-version-wire/structured-agent-session-surface-calls.ts
  • tests/e2e/cross-version-wire/structured-agent-session-surface-execution.ts
  • tests/e2e/cross-version-wire/structured-agent-session-surface-manifest.ts
  • tests/e2e/terminal-presentation-retirement.unit.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


Review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 872da579-cac4-4025-a060-9f4b64c88389
Commits

Reviewing files that changed from the base of the PR and between 5906f37 and 4ec1e2f.

Files ignored due to path filters: 1
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*
Files selected for processing: 8
  • src/main/native-chat/agent-session-wire/structured-agent-session-conversation-lifetime.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
Files skipped from review as they are similar to previous changes: 5
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/locales/ja.json

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

Comment thread src/renderer/src/i18n/locales/fr.json Resolved
@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 1/5

[High impact] The PR changes conversation switching across shared chat, terminal, and process-lifecycle paths.

The PR is not ready to merge because four earlier blocking findings remain unresolved.

Findings

  1. P1 Failed launches block recovery ▶
  2. P1 Paginated chats cannot return ▶
  3. P1 Cross-workspace returns close the terminal ▶
  4. P1 Switching reuses a closed terminal ▶
  5. P2 Empty daemon never retires ▶

Summary

This PR lets users move the same conversation between Chat UI and a terminal without making a copy.

  • A conversation switches between Chat UI and terminal in the same tab.
  • Supported agents can switch surfaces while keeping selection and placement tied to the current operation.

The PR description explicitly says not to merge.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[User requests switch] --> B[Stop source]
  B --> C[Confirm process exited]
  C --> D[Import conversation if needed]
  D --> E[Open and publish destination]
  E --> F[Retire source presentation]
  F --> G[Observe provider startup separately]
Loading

Reviews (12) · Last reviewed commit: "Align switch recovery and UI test owner ..." · Reviewed by Greptile

Comment on lines +169 to +178
const result = await this.client.request<CreateOrAttachResult>(
'createOrAttach',
{
sessionId: dispatchedSessionId,
cols: context.effectiveCols,
rows: context.effectiveRows,
attachOnly: true
},
5000
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Failed launches block recovery

When a switch uses an older daemon’s ordinary creation path, a create rejected before any child starts leaves its completion promise unresolved. The later attachOnly request throws SessionNotFoundError, but this branch never settles or removes the retained operation. Later native starts keep waiting, and Restart Terminal Daemon fails at settleSwitchWorkForReplacement.

Settle explicit creation failures through the original request’s outcome. Do not treat an uncertain missing session as proof of exit.

Comment on lines +30 to +35
const prepared = await prepareLegacyTranscriptImport({
agent: source.agent,
piAfterEntry,
sessionId: source.sessionId,
options: { filePath: source.filePath }
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Paginated chats cannot return

This call passes only filePath, so a paginated Codex conversation with history_base cannot return to chat. prepareLegacyTranscriptImport follows that base through readCodexLocalHistory, but the resolver throws without account roots. Every Codex return calls this helper, even after the source shell has stopped.

Forward the source’s account storage so the importer can read the earlier history.

Comment on lines 73 to 83
@@ -66,14 +82,14 @@ export class DaemonPtySpawnPreparations {
this.finish(sessionId, preparation)
}, 720_000)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Empty daemon never retires

If a prepare reply is lost, the daemon keeps an entry in retained that nobody can release. The client only records recovery after receiving the anchor. The daemon's expiry callback converts the entry into a completed record but does not delete it. Since hasOwedOutcomes() counts every retained record, idle shutdown stays blocked even after all terminals and clients leave.

This leaves an empty daemon running indefinitely and adds retained records on each repeat. Keep enough request identity to release an unanswered prepare, or discard abandoned preparations that never started a child.

Comment on lines +140 to +142
// Shutdown already accepted must finish even when the observing request expires.
try {
await host.trackSwitchPhysical(sessionId, close.stop())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Cross-workspace returns close the terminal

When the conversation already has a native chat in another workspace, this path still stops the source terminal and later calls close.retire(). switchRetainedOwner can select that other workspace, but neither close checks the destination workspace. The source terminal disappears instead of staying open as the PR describes.

Compare the source and destination workspaces before stopping the source, and skip its retirement for this case.

@brennanb2025 brennanb2025 changed the title Switch a Codex chat to its terminal and back on the same conversation Switch chats between Chat UI and a terminal without forking Oct 29, 2026
brennanb2025 and others added 27 commits November 1, 2026 23:40
…tched tab moves by its terminal, never by an id it no longer holds
…ner first says which tab, the chat-tab record commits, then the tab changes; a late or lost answer never undoes a committed switch
…st authoritative inventory, and no other frame changes a tab
…ch on the host changes what that tab holds, keeping its place, pin and selection, also after a restart
…rminal, by the tab or by its terminal; split focus and orphan recovery read the terminal a tab shows
…t tab, also when the chat was open in another tab, which closes as a duplicate
…or Session History: a whole tab keeps its selection because the same tab holds the chat
… row by the conversation, and keys the stop record the way Pi's import keys its rows
…itting anything to it, so that terminal keeps its size and screen
…b already shows, asked of the window that holds the tab, so a late tab graph never starts a second CLI
…e chat reopened under its own row, and a chat's lifetimes are told apart by the row each publishes
…place, pin and id through a host switch and a restart whatever it first showed, and two hosts' tabs never take each other's place
…nswer is asked again of that tab instead of opening the chat or terminal elsewhere, a tab the user changed keeps their change with the chat opening in a free tab beside it, and a retry naming no tab finds the switched tab through its owner
…nal that ran this run, even one whose process since exited, and retires the recovered terminals' records in one write
…b the record names takes the chat in place when it still holds just the stopped terminal, and a tab the user changed keeps their change with the chat opening in a free tab
…e lookup, by the tab's saved host tab and what it holds, so a tab first seen as a chat or one whose id another host took first is closed, pinned, focused, moved and anchored on its own host tab, also before the first frame after a restart
…ndow publishes for it, so a chat reopened under its own row is never moved by its retired session row
…rmation only while the tab still holds what the close named, so a tab a switch changed meanwhile keeps its new content
…n owns acts only while each row it named still names that tab when main writes it, so a switch queued ahead of it, or an earlier step of the same reorder, never moves or pins the new content, and a row naming a tab's current terminal pins that tab

The window's rename of a terminal main names now renames only a terminal it holds, never a chat a switch left in the tab that once had that terminal's id.
…rors in both drawing modes, so a main-owned host's switch keeps its place, selection and recent order too, and the switched chat's status, notifications, close and focus name that chat rather than what its tab id once held

The main-owned frame codec now finds each desktop tab through the same saved host-tab index as the snapshot mirror and saves the association; new tabs take their terminal's own id in both modes. A retired terminal's closed marker never refuses the chat a switch left in a tab with that id; a container id is never read as a terminal's (status retraction, sidebar attribution, orphan group cleanup, split focus, launch completion); a paired chat's phone notification carries the host's pane key; a chat's close and close intent name the row it publishes; and this window's own chat takes the tab main recorded for it.
…es: the window applies it at most once, a lost answer is asked about without changing anything again, a reloaded page never answers for the old one, and a chat the user closed or switched meanwhile stays closed rather than reopening, while a switch the window refused opens the chat in the tab the window itself picks
… tab for it, also beside a tab that already has that id, and a paired host's chat takes over the window's tab holding that same conversation, as before
…s before terminals reconnect puts the chat back in the tab it was switching, only when that tab still holds just the stopped terminal and nothing runs in it; otherwise the terminal keeps its tab and the chat opens beside it, a window that reloads keeps a switch it hadn't saved yet, and opening a chat never replaces what another tab holds
…n the host is an older Orca, a chat opened where its usual tab id is taken records the tab it actually opens in, and a chat's status counts under the workspace of the tab showing it whatever that tab's id looks like
…he user hasn't changed them since it opened, so nothing the user did in the first seconds is replaced; a changed one keeps its stopped terminal with the chat opening beside it

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@brennanb2025
brennanb2025 changed the base branch from main to brennanb2025/canonical-workspace-surfaces November 4, 2026 06:48
@FC-Zer0x1

Copy link
Copy Markdown

A use case for this PR. We're considering moving long-lived Claude agents from terminals into native chat, and found this while planning the way back (from reading 1.4.247, not from doing it). When an agent has to go from a chat back to a terminal, "Resume in New Terminal" runs claude --resume <id> --fork-session (1.4.247). The terminal gets a new conversation ID while the chat keeps the original, so anything keyed to the original ID (orchestration Runs, mailboxes, Session History) points at the wrong conversation. Switching in place on the same conversation, as described here, avoids that.

Two things we would need on top: a CLI form of the switch, so a script can do it without the UI, and the closed chat staying refused while the terminal owns the conversation, as Orca already does today.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants