feat(web): continue handoffs in a new thread - #8755
timothyachumba wants to merge 1 commit into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Warning Your free Security trial is over. An organization admin can activate Security or dismiss this notice. Comment |
|
|
||
| if (failure === null) { | ||
| const startedResult = await settlePromise(() => waitForStartedServerThread(nextThreadRef)); | ||
| failure = startedResult._tag === "Failure" ? startedResult : null; |
There was a problem hiding this comment.
🟡 Medium components/ChatView.tsx:6787
waitForStartedServerThread returns false on timeout, but settlePromise wraps that false as AsyncResult.Success(false), which passes the _tag === "Failure" check. The handler then proceeds to settle the source thread and navigate to the new thread even though it was never observed to start. On a slow server the user lands on a thread that may not exist yet.
The startedResult.value boolean needs to be checked so a timeout is treated as a failure.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatView.tsx around line 6787:
`waitForStartedServerThread` returns `false` on timeout, but `settlePromise` wraps that `false` as `AsyncResult.Success(false)`, which passes the `_tag === "Failure"` check. The handler then proceeds to settle the source thread and navigate to the new thread even though it was never observed to start. On a slow server the user lands on a thread that may not exist yet.
The `startedResult.value` boolean needs to be checked so a timeout is treated as a failure.
| failure = navigateResult._tag === "Failure" ? navigateResult : null; | ||
| } | ||
|
|
||
| if (failure !== null && !isAtomCommandInterrupted(failure)) { |
There was a problem hiding this comment.
🟠 High components/ChatView.tsx:6817
When startThreadTurn succeeds (creating the thread via bootstrap) but waitForStartedServerThread or navigation subsequently fails, the newly created thread is left running with no cleanup. The adjacent onImplementPlanInNewThread explicitly calls deleteThread to remove the orphan; this path only toasts and calls finish(), leaving an unreachable thread actively executing the handoff task.
Wrap the failure path with thread cleanup guarded on startResult._tag === "Success", mirroring the implementation-thread flow. Also add deleteThread to the dependency array.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatView.tsx around line 6817:
When `startThreadTurn` succeeds (creating the thread via bootstrap) but `waitForStartedServerThread` or navigation subsequently fails, the newly created thread is left running with no cleanup. The adjacent `onImplementPlanInNewThread` explicitly calls `deleteThread` to remove the orphan; this path only toasts and calls `finish()`, leaving an unreachable thread actively executing the handoff task.
Wrap the failure path with thread cleanup guarded on `startResult._tag === "Success"`, mirroring the implementation-thread flow. Also add `deleteThread` to the dependency array.
| const nextThreadId = newThreadId(); | ||
| const nextThreadRef = scopeThreadRef(activeThread.environmentId, nextThreadId); | ||
| const modelSelection = sendCtx.selectedModelSelection; | ||
| const interactionMode = activeThread.interactionMode; |
There was a problem hiding this comment.
🟠 High components/ChatView.tsx:6744
This local interactionMode shadows the outer resolved value and reads activeThread.interactionMode directly. When settings.planModeEnabled is false, the outer interactionMode (line ~1643) is forced to "default", but this line bypasses that gate — so a source thread that stored "plan" will start the continuation thread in plan mode even though the feature is disabled.
Remove this local declaration so the callback uses the outer, already-resolved interactionMode, and add interactionMode to the dependency array.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatView.tsx around line 6744:
This local `interactionMode` shadows the outer resolved value and reads `activeThread.interactionMode` directly. When `settings.planModeEnabled` is `false`, the outer `interactionMode` (line ~1643) is forced to `"default"`, but this line bypasses that gate — so a source thread that stored `"plan"` will start the continuation thread in plan mode even though the feature is disabled.
Remove this local declaration so the callback uses the outer, already-resolved `interactionMode`, and add `interactionMode` to the dependency array.
There was a problem hiding this comment.
One consistency finding on the new "Continue in new thread" action. Everything else in the change (Tooltip/Button composition mirroring MessageCopyButton, aria-label, disabled/loading handling, icon sizing, and the onContinueInNewThread default of null that suppresses the action instead of falling back to the active environment) looks consistent with the shared primitives.
Posted via Macroscope — UI Consistency
| variant="ghost" | ||
| className="text-muted-foreground hover:text-foreground" |
There was a problem hiding this comment.
This new call site reconstructs the existing ghost-muted button variant (text-muted-foreground + hover-to-foreground) with call-site classes on variant="ghost". ghost-muted already owns that treatment, including the pressed state ([:hover,[data-pressed]]:text-foreground) that the local classes drop, so the pressed tone here will diverge from every other muted ghost action. Consider using the named variant and letting the primitive own the base state colors.
| variant="ghost" | |
| className="text-muted-foreground hover:text-foreground" | |
| variant="ghost-muted" |
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 35ce8d6. Configure here.
| sendInFlightRef.current | ||
| ) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Continue starts second live worktree session
Medium Severity
onContinueInNewThread does not treat a live source session as a hard stop. Historical handoff rows stay clickable while a later turn is running, so Continue can start another agent on the same worktreePath and only then try settleThread, which canSettle rejects. The new thread still launches and navigation proceeds after a warning.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 35ce8d6. Configure here.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This adds a new production workflow that parses handoff responses, creates and starts an agent thread on the existing checkout, optionally settles the source, and navigates to it. The lifecycle orchestration has unresolved startup, cleanup, mode-selection, and shared-worktree risks, so it warrants human review. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |


A completed task can leave a compact handoff for the next task, but today the user has to create the next thread and paste that context manually. This adds an explicit action for versioned handoff packets.
When a terminal assistant response contains a
t3-handoff:1packet, web and desktop show Continue in new thread beside Copy. The action atomically creates and starts a thread in the same project and checkout, sends the packet with a backlink to the source thread, then settles the source thread when supported. Ordinary assistant responses are unchanged.Discussed first in #8754.
Checks:
git diff --checkThe focused Vite+ tests could not run locally because Corepack could not reach the npm registry while installing pnpm. CI is the first full type and test pass.
Implemented with OpenAI Codex in T3 Code.
Note
Medium Risk
The flow creates threads, sends turns, navigates routes, and optionally settles the prior thread, but it is gated on server-thread/send-busy state and treats settlement errors as non-fatal.
Overview
Adds Continue in new thread for assistant messages that include a versioned
t3-handoff:1packet. A newsessionHandoffhelper parses the marked block (requiring a Next task section), derives a thread title, and builds the first user message with a link back to the source thread.ChatView wires the action: it reuses the current composer provider/model, bootstraps a new thread in the same project, branch, and worktree, starts the turn with the handoff prompt, navigates to the new route, and attempts to settle the previous thread when supported (settlement failures surface as a warning toast only). The timeline shows the control next to copy only when parsing succeeds; ordinary replies are unchanged.
User docs in
composer.mddescribe the flow; unit tests cover parsing/prompt building and timeline visibility.Reviewed by Cursor Bugbot for commit 35ce8d6. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add "Continue in new thread" action for session handoffs in
ChatViewparseSessionHandoffandbuildSessionHandoffPromptin sessionHandoff.ts to detect handoff packets in assistant text and build a prompt linking back to the source threadonContinueInNewThreadin ChatView.tsx: it validates runtime preconditions, creates a new thread in the same project/checkout, sends the handoff prompt as the first message, optionally settles the previous thread, and navigates to the new threadForwardIconbutton in MessagesTimeline.tsx on assistant messages that contain a valid handoff packet, disabled while the async continuation is in flight📊 Macroscope summarized 35ce8d6. 4 files reviewed, 4 issues evaluated, 1 issue filtered, 3 comments posted
🗂️ Filtered Issues
apps/web/src/lib/sessionHandoff.ts — 0 comments posted, 1 evaluated, 1 filtered
titleFromNextTasktruncates with UTF-16 code-unitlength/slice, so a Next task whose 69th code unit is the first half of an astral character (for example, 68 ASCII characters followed by an emoji) produces a title containing an unpaired surrogate followed by.... The new thread then displays/stores a replacement character rather than the task text at the truncation boundary. [ Out of scope (post-validation triage) ]Status update
Closing this after finding the prior work I should have checked before opening it. Discussion #8433 tracks agent-controlled T3 threads. PRs #7487, #8452, and #8492 already explored agent-requested or fresh-thread handoffs. In particular, #8452 was closed on August 28, 2026 with an explicit decision not to add a separate Continue in new thread action.
The implementation remains available on the branch, but this PR should not compete with that existing history.