Skip to content

fix(clients): keep a checkout terminal off the thread's worktree - #16039

Open
saphid wants to merge 13 commits into
pingdotgg:mainfrom
saphid:stack/03-terminal-panel
Open

saphid wants to merge 13 commits into
pingdotgg:mainfrom
saphid:stack/03-terminal-panel

Conversation

@saphid

@saphid saphid commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Model: Claude Opus 5.5. Narrowed to the checkout-terminal fix by Claude Opus 5.5 on 2026-10-14.

Scope: preserve the project-checkout location for terminals on web and mobile. The panel-host proposal remains excluded. The current diff changes the web resolver and its two call sites in PersistentThreadTerminals.tsx, the mobile resolver, and their focused tests.

Problem

A terminal can run on the project checkout while its thread uses a worktree. Two common ways:

  • A project script was run while the thread was still on the local checkout. Its launch context records worktreePath: null.
  • A terminal was opened before the thread got a worktree. The server summary has worktreePath: null.

null here means "the project checkout". Then the thread gains a worktree, either from its first send in worktree mode or by being moved onto one.

On main, the web right-panel terminal (PersistentThreadTerminalPanel in PersistentThreadTerminals.tsx) resolves launchContext?.worktreePath ?? summary?.worktreePath ?? threadWorktreePath, at two sites. The ?? treats the authoritative null as missing and falls back to the thread's worktree. The terminal keeps its checkout cwd, but it is described to the server with the thread's worktree:

  • projectScriptRuntimeEnv (packages/shared/src/projectScripts.ts) sets T3CODE_WORKTREE_PATH to the unrelated worktree.
  • When the server opens or restarts that session, it records the wrong worktree, takes its workspace lease on the worktree, and starts the shell with the mismatched context.

The bottom terminal drawer already checks launchContext !== null and is not affected. Mobile has the same ?? fallback in resolveTerminalOpenLocation (apps/mobile/src/features/terminal/terminalLaunchContext.ts). Attaching to a running session ignores the location, so mobile only goes wrong when the server has to open a new session for that terminal.

Why this qualifies

A small, focused fix for an obvious bug: a terminal on the checkout is told it is in a worktree. It changes no contract; TerminalSummary.worktreePath is already a required nullable field where null means the checkout. It includes each client that has the same defect.

Fix

  • Web: a pure resolveTerminalWorktreePath in ChatView.logic.ts uses the launch context if there is one, then the summary, and only then the thread's worktree. Both right-panel call sites in PersistentThreadTerminals.tsx use it.
  • Mobile: resolveTerminalOpenLocation uses the session's location as a whole whenever a session exists, and the thread's worktree only when there is none. The cwd result is unchanged.

Verification

At 4d3a85ec8c, integrated with upstream 70d60e03a7 on macOS, 2026-10-17:

  • Focused selection: vp test run apps/mobile/src/features/terminal/terminalLaunchContext.test.ts apps/web/src/components/ChatView.logic.test.ts: 144 passed (144).
  • Targeted lint and formatting pass; package typechecks pass for apps/mobile, apps/web.
  • Fresh read-only integration review by GPT-6.1 Sol (high), Codex through T3 Code: no remaining actionable findings for this exact tree. Claude quota prevented a new cross-model review.
  • Current-head CI passes, including Macroscope Correctness and Approvability; Macroscope approved 4d3a85e. Macroscope UI Consistency and Effect Service Conventions skipped because their required Check run was not discovered within the one-minute window.
  • CodeRabbit’s summary and title/description warnings cover the superseded panel-host refactor at c568d16. That 23-file scope is absent from the current five-file contribution, which contains only the web and mobile checkout-terminal resolvers, two web call sites, and tests. The request to rename or describe a panel-host refactor therefore does not apply. Its aggregate docstring warning also measures the superseded scope; the new web resolver has a doc comment. The current-head status says Review rate limited, so no fresh CodeRabbit review is claimed.
  • No new UI/native capture or live-provider verification was performed in this integration pass. Earlier fail-before results below retain their recorded revisions and limitations; they do not certify this head. Live client terminal opening/restart remains unverified as described below.

Earlier verification

Run on Linux (lxso2):

  • vp test run apps/web/src/components/ChatView.logic.test.ts -t resolveTerminalWorktreePath: 3 new cases pass. With the resolver set to main's ?? expression, 2 fail (expected '/repo/.worktrees/feature' to be null).
  • vp test run apps/mobile/src/features/terminal/terminalLaunchContext.test.ts: 7/7 pass. On main's file, the new case fails (worktreePath was "/repo/worktrees/feature", expected null).
  • ChatView.logic.test.ts, ThreadTerminalDrawer.test.ts, ThreadTerminalDrawer.permissions.test.tsx and the mobile test: all pass.
  • tsc --noEmit for apps/web and apps/mobile, plus vp lint and vp fmt --check on the changed files: pass. Lint has no warnings on changed lines.
  • Independent review by GPT-6.1 Sol (high) of the web commit, then of the full web + mobile diff: no actionable findings. Sol corrected one claim, that attach does not respawn a running shell, and the commit messages reflect that.

No screenshots: nothing changes visually. The fix changes the worktree path and environment a checkout terminal is opened with.

Not verified

  • No live run in a client. The tests cover the resolvers, not a rendered terminal panel or a device.
  • How often a client reopens or restarts such a session, which is when the wrong context reaches the server, was inferred from the server code, not observed.

Surfaces

  • Web and desktop: fixed (right-panel terminal; desktop wraps the same bundle).
  • Mobile: fixed.
  • Providers, contracts: unchanged.
  • Connection modes: client-side resolution only; local, remote and tunnel behave the same.

Claude Opus 5.5 (build) and GPT-6.1 Sol (review) via T3 Code
🤖 Generated with Claude Code

Integration and publication: GPT-6.1 Sol (high), Codex harness through T3 Code.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Oct 5, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 4d3a85e

Macroscope's review found this PR approvable — This is a small, tested bug fix that keeps checkout terminals aligned with their actual cwd instead of inheriting a thread worktree after the thread changes. It does not alter APIs, product configuration defaults, deployment behavior, or static-analysis settings.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: pingdotgg/t3code/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 985a3cc7-dd7e-4d4c-88aa-515558fee749
📥 Commits

Reviewing files that changed from the base of the PR and between 7812230 and c568d16.

📒 Files selected for processing (23)
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/RightPanelTabs.browserProfile.test.tsx
  • apps/web/src/components/RightPanelTabs.terminal.test.tsx
  • apps/web/src/components/RightPanelTabs.test.tsx
  • apps/web/src/components/RightPanelTabs.tsx
  • apps/web/src/components/diffs/DiffFileLoadingBoundary.tsx
  • apps/web/src/components/diffs/DiffLoadingState.tsx
  • apps/web/src/components/preview/PreviewPanel.tsx
  • apps/web/src/components/pullRequest/PullRequestCodeTab.tsx
  • apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
  • apps/web/src/panels/bundledPanels.test.tsx
  • apps/web/src/panels/bundledPanels.tsx
  • apps/web/src/panels/diff/DiffSidePanel.tsx
  • apps/web/src/panels/panelHost.ts
  • apps/web/src/panels/panelRegistry.test.tsx
  • apps/web/src/panels/panelRegistry.ts
  • apps/web/src/panels/preview/PreviewSidePanel.test.tsx
  • apps/web/src/panels/preview/PreviewSidePanel.tsx
  • apps/web/src/panels/terminal/PersistentThreadTerminalDrawer.tsx
  • apps/web/src/panels/terminal/TerminalSidePanel.attach.test.tsx
  • apps/web/src/panels/terminal/TerminalSidePanel.test.tsx
  • apps/web/src/panels/terminal/TerminalSidePanel.tsx
  • apps/web/src/routes/_chat.pull-requests.tsx
💤 Files with no reviewable changes (1)
  • apps/web/src/components/preview/PreviewPanel.tsx

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

📝 Walkthrough

The change adds a typed registry for lazy-loaded side panels. ChatView hosts the Preview, Diff, and Terminal panels through shared context, and RightPanelTabs uses panel metadata and launcher configurations to display and open them.

Changes: Registered side panels

Layer / File(s) Summary
Panel registry and host contracts
apps/web/src/panels/panelRegistry.ts, apps/web/src/panels/panelHost.ts, apps/web/src/panels/bundledPanels.tsx, apps/web/src/panels/*test.tsx
Adds typed panel definitions, lazy loading, metadata lookup, and shared host context. Tests cover loading behavior, component identity, mounting, and panel-specific prop types.
Preview and Diff panel bodies
apps/web/src/panels/preview/*, apps/web/src/panels/diff/DiffSidePanel.tsx, apps/web/src/components/diffs/*, apps/web/src/components/pullRequest/PullRequestCodeTab.tsx, apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx, apps/web/src/components/preview/PreviewPanel.tsx
Preview and Diff use the panel host for shared state. Diff loading components and imports move to DiffLoadingState; the previous PreviewPanel and DiffPanelShell implementations are removed.
Terminal panel and drawer
apps/web/src/panels/terminal/*, apps/web/src/components/ChatView.tsx
Moves terminal panel and drawer behavior into panel modules. TerminalSidePanel resolves thread and launch context; the drawer manages terminal sessions and actions. Tests cover host updates, attachment, and launcher behavior.
Right-panel launcher actions
apps/web/src/components/RightPanelTabs.tsx, apps/web/src/components/RightPanelTabs*.test.tsx
Builds panel launcher actions from registry metadata and availability. Tests cover action details, Browser profile selection, and Terminal availability and opening.
ChatView panel hosting and wiring
apps/web/src/components/ChatView.tsx, apps/web/src/routes/_chat.pull-requests.tsx
ChatView provides shared host context and renders registered panels. Inline and sheet launchers receive panel configurations; the pull-request route marks Preview, Diff, and Terminal unavailable.

Priority: ⬇️ Low

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant ChatView
  participant PanelHostContext
  participant RegisteredSidePanel
  participant PreviewSidePanel
  ChatView->>PanelHostContext: Provides thread and panel state
  ChatView->>RegisteredSidePanel: Renders selected panel with panel props
  RegisteredSidePanel->>PreviewSidePanel: Loads selected Preview body
  PreviewSidePanel->>PanelHostContext: Reads shared host state
Loading

Suggested reviewers: juliusmarminge

Merge Risk: ⚪ Minimal · up to c568d

This refactor moves the Preview, Diff, and Terminal panels into a shared registered-panel host. Launcher labels, shortcuts, and availability are unchanged. No concrete regression has been identified, and the change appears ready to merge after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to c568d

The inspected paths preserve existing thread-scoped terminal inputs, annotation callbacks, and availability checks. No introduced security regression was established. Risk remains low rather than minimal because server authorization and some asynchronous recovery behavior remain unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The security-sensitive outcomes remain terminal operations in the selected environment/thread and mutations through the existing composer callbacks. The inspected client wiring does not establish wider exposure; maximum cross-user, environment, or credential exposure depends on server enforcement that was not verified.

Trust Boundaries and Controls

  • observed — Shared context makes annotation and composer capabilities accessible to all hosted panel code, rather than only their former direct recipients. The current registry is fixed and bundled: Preview consumes annotation sending, Diff consumes composer/workspace values, and Terminal consumes thread identity and visibility. This is broader internal capability visibility, not evidence of an attacker-controlled extension boundary.

Resilience and Maintainability Implications

  • observed — Session-exit handling uses a boolean-gated deferred callback without a captured identity generation. That implementation, and an unkeyed persistent terminal parent, were present at base. The inspection did not establish that hosted rendering introduces or worsens a stale-close failure.

Hardening Proposals

  • proposed — As pre-existing lifecycle hardening, bind delayed terminal-exit callbacks to the initiating environment/thread/terminal generation and cancel pending callbacks during teardown. This proposal is not an introduced PR finding.
Pre-merge checks: Passed 2 · Failed 3

Failed Failed checks: 3 warnings

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 31.11% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 45 functions across 22 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check Warning The title describes a checkout-terminal worktree fix, but the provided changeset primarily refactors the web right-panel terminal into the panel-host and registry architecture. The title does not summ… Rename the PR with a concise title that reflects the panel-host refactor, such as "refactor(web): open the right-panel terminal through the panel host".
Description check Warning The description is detailed but describes a checkout-terminal resolver fix that is absent from the provided changeset and conflicts with the stated PR objectives. It also does not provide the required… Rewrite the description to document the panel-host refactor, its problem and expected behavior, the affected components, the related issue or maintainer approval, and verification results for the actual changeset.
Passed Passed checks: 2 passed
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.
Full details: Title check

Explanation

The title describes a checkout-terminal worktree fix, but the provided changeset primarily refactors the web right-panel terminal into the panel-host and registry architecture. The title does not summarize the main change.

Resolution

Rename the PR with a concise title that reflects the panel-host refactor, such as "refactor(web): open the right-panel terminal through the panel host".

Full details: Description check

Explanation

The description is detailed but describes a checkout-terminal resolver fix that is absent from the provided changeset and conflicts with the stated PR objectives. It also does not provide the required scope and approval information for the panel-host refactor.

Resolution

Rewrite the description to document the panel-host refactor, its problem and expected behavior, the affected components, the related issue or maintainer approval, and verification results for the actual changeset.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@saphid
saphid force-pushed the stack/03-terminal-panel branch 3 times, most recently from bdb5405 to fe0a090 Compare October 6, 2026 16:03
@saphid

saphid commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Review requested

Date (UTC) Reviewer Where
2026-10-06 Maria Discord DM

Logged so this PR shows when a maintainer was asked to review it.

@saphid
saphid force-pushed the stack/03-terminal-panel branch 5 times, most recently from 8701762 to cdb4f58 Compare October 10, 2026 02:13
@saphid
saphid force-pushed the stack/03-terminal-panel branch 2 times, most recently from a77d211 to e34c8a4 Compare October 10, 2026 07:06
saphid and others added 9 commits October 12, 2026 10:54
Preview becomes the second panel on the side-panel registry that Diff
started. Each definition now also carries the panel's title, icon, launcher
letter, client support and unavailable copy, so the tabs, the empty
launcher and the add menu read one ordered list instead of three
hand-kept ones. Labels, letters, order and copy are unchanged.

Panel props are inferred from each lazily loaded body, and the caller is a
closed union, so another panel's props, unknown ids and widened ids do not
compile. ChatView lends the rendered panel a small host (thread, right
panel visibility, composer draft target, workspace mutation id and the
annotation send) instead of drilling the same props into each body; the
annotation send keeps the per-render closure it had before, and PreviewView
still drops a pick that settles after a thread switch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ChatView built a new PanelHost on every render, so every usePanelHost
consumer re-rendered even when no host field changed. Memoize it on its
fields and send annotations through onSendRef so the sender stays stable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Plain server threads reuse one ChatView, so the memoized panel host's
sender could resolve to the next thread's composer when a pick settled
after a switch. The latest sender now carries its thread key, and each
host forwards only to a sender for its own thread.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ChatView replaced the panel host's annotation sender while rendering. If
React threw that render away, an in-flight preview pick could still call
its onSend, for example one that edits a queued message instead of sending
a turn. Update the sender in a layout effect so only committed renders
lend it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PersistentThreadTerminalPanel, PersistentThreadTerminalDrawer, their two
reconciliation helpers and the terminal launch-context types now live in
apps/web/src/panels/terminal. The moved code is unchanged apart from the
added export keywords; ChatView imports them and its call sites, props,
memo boundaries and callbacks are untouched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The right-panel terminal is now a registered side panel. Its body reads the
thread and visibility from the panel host and keybindings from the server
keybindings atom, then hands them to the unchanged memoized terminal, so
ChatView renders that leave its inputs alone still skip it. ChatView passes
only the terminal surface, launch context, focus request, callbacks and
shortcut labels. Launcher copy, letter, order and availability are
unchanged; the bottom drawer stays mounted by ChatView.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tree

A terminal launch context with a null worktree path means the terminal was
launched on the local checkout. The right-panel terminal treated that null as
missing and fell back to the thread's worktree, so a thread that gained a
worktree after the launch gave the drawer a worktree path and runtime env that
did not match its cwd. Use the launch context whenever one exists, as the
persistent drawer already does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hread's worktree

A terminal summary's null worktree path means the server opened the terminal
on the project checkout. Without a launch context the right-panel terminal
treated that null as missing and fell back to the thread's worktree, so a
thread that gained a worktree later gave the drawer a checkout cwd with a
worktree path and runtime env. Fall back to the thread's worktree only when
there is neither a launch context nor a summary.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@saphid
saphid force-pushed the stack/03-terminal-panel branch from e34c8a4 to ca03345 Compare October 12, 2026 00:11
github-actions Bot and others added 3 commits October 14, 2026 14:24
This PR now carries only the checkout-terminal worktree fix. The panel-host
layers it was stacked on (pingdotgg#15010, pingdotgg#16038 and the terminal panel move) wait on
Ideas discussion #14938; the fix does not need them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s worktree

A null worktree path on a terminal launch context or server summary means
the terminal runs on the project checkout. The right-panel terminal read
both with `??`, so it treated that null as missing and fell back to the
thread's worktree. Once a thread gained a worktree (first worktree-mode
send, or switching the thread onto a worktree), a terminal still running on
the checkout got the worktree as its worktreePath and T3CODE_WORKTREE_PATH,
and the server tracked and re-spawned it with that mismatched context
whenever it reopened the session.

Use the launch context whenever there is one, then the summary, and fall
back to the thread's worktree only when neither exists, as the bottom
drawer already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mobile terminal screen resolves its location from the session summary,
where a null worktree path means the terminal runs on the project checkout.
resolveTerminalOpenLocation read that field with `??`, so once the thread
had a worktree it resolved a checkout terminal to the checkout cwd with the
thread's worktree as its worktreePath. Attaching to a running session
ignores the location, but when the server has to open a new session for it,
that session records the wrong worktree and takes its workspace lease on the
worktree instead of the checkout.

Use the session's location whenever there is a session, and fall back to the
thread's worktree only when there is none, matching the web fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@saphid saphid changed the title refactor(web): open the right-panel terminal through the panel host fix(clients): keep a checkout terminal off the thread's worktree Oct 14, 2026
@github-actions github-actions Bot added size:M 30-99 changed lines (additions + deletions). and removed size:XXL 1,000+ changed lines (additions + deletions). labels Oct 14, 2026
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Oct 14, 2026
…inal off the thread's worktree

Preserve the scoped contribution against upstream 70d60e0.
Focused checks and read-only integration review recorded in the PR.

GPT-6.1 Sol (high), Codex harness through T3 Code.
@macroscopeapp
macroscopeapp Bot dismissed their stale review October 17, 2026 03:04

Dismissing prior approval to re-evaluate 4d3a85e

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

size:M 30-99 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant