Skip to content

feat(web): continue handoffs in a new thread - #8755

Closed
timothyachumba wants to merge 1 commit into
pingdotgg:mainfrom
timothyachumba:feat/handoff-session
Closed

timothyachumba wants to merge 1 commit into
pingdotgg:mainfrom
timothyachumba:feat/handoff-session

Conversation

@timothyachumba

@timothyachumba timothyachumba commented Aug 30, 2026 •

Copy link
Copy Markdown

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:1 packet, 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:

  • parser smoke test with Node type stripping
  • Bun syntax build for the changed TypeScript and TSX
  • targeted oxlint
  • oxfmt check
  • git diff --check

The 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:1 packet. A new sessionHandoff helper 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.md describe 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 ChatView

  • Adds parseSessionHandoff and buildSessionHandoffPrompt in sessionHandoff.ts to detect handoff packets in assistant text and build a prompt linking back to the source thread
  • Wires onContinueInNewThread in 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 thread
  • Renders a ForwardIcon button in MessagesTimeline.tsx on assistant messages that contain a valid handoff packet, disabled while the async continuation is in flight
  • Behavioral Change: previous thread settlement is best-effort; settlement failures show a warning toast but do not block navigation to the new thread
📊 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
  • line 16: titleFromNextTask truncates with UTF-16 code-unit length/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.

@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3cddd285-6169-479f-80f8-abc93615a40a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

Your free Security trial is over. An organization admin can activate Security or dismiss this notice.


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

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 30, 2026

if (failure === null) {
const startedResult = await settlePromise(() => waitForStartedServerThread(nextThreadRef));
failure = startedResult._tag === "Failure" ? startedResult : null;

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.

🟡 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)) {

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.

🟠 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;

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.

🟠 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.

@macroscopeapp macroscopeapp 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.

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

Comment on lines +1283 to +1284
variant="ghost"
className="text-muted-foreground hover:text-foreground"

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.

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.

Suggested change
variant="ghost"
className="text-muted-foreground hover:text-foreground"
variant="ghost-muted"

Posted via Macroscope — UI Consistency

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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;
}

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.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 35ce8d6. Configure here.

@macroscopeapp

macroscopeapp Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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:

  • 3 blocking correctness issues found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

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

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant