Skip to content

Continue a thread on another provider from the follow-up composer - #3434

Open
brsbl wants to merge 5 commits into
mainfrom
bb/follow-up-handoff-model-thr_rmmwartzsr
Open

Continue a thread on another provider from the follow-up composer#3434
brsbl wants to merge 5 commits into
mainfrom
bb/follow-up-handoff-model-thr_rmmwartzsr

Conversation

@brsbl

@brsbl brsbl commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Human comments

What was wrong

  • The follow-up composer's model picker was locked to the thread's provider, so the only way to continue a thread on another model was the "Handoff to new thread" footer action in the picker.
  • That action navigated to root compose with a seeded "Continue from @thread" draft and discarded whatever was already typed in the follow-up composer.
  • Example: typing "Refactor the tests" in a Codex thread and then choosing the footer action to hand off to Claude Code lost the typed text and required retyping it on the new-thread page.

What changed

  • apps/app/src/views/thread-detail/ThreadDetailPromptArea.tsx: the bottom follow-up composer now passes provider.onChange, so the existing ModelReasoningPicker shows the same provider tabs as the new-thread picker.
  • Selecting a provider other than the thread's own (while the thread's provider is still available) enters handoff mode. A "Continue from @thread:" reference is inserted at the top of the draft with a blank line after it and the composer is focused, so the user can edit or delete it before submitting. One appToast.message says the reference was added and that submitting will create a new thread. The submit control reads "Create new thread (Enter)". The submit mode is ready once the selected provider's model resolves, and blocked while it loads or if that catalog fails.
  • Submitting in handoff mode sends the draft exactly as typed in a create-thread request on the selected provider and model, reusing the source thread's environment (or project-default when the thread has none), then navigates to the new thread. Scheduled submits pass sendAt.
  • Switching back to the thread's provider removes the reference again, but only while its mention is intact and it still sits on its own line; the editor may collapse the blank line after it, which is tolerated. Re-entering handoff mode never inserts a second reference.
  • Same-provider model changes still send follow-ups to the current thread. The queued-message inline editor and the sent-message editor keep a provider-locked picker. Beginning a sent-message edit during a pending cross-provider selection reverts that selection so the edit stays on the thread. Side chats use EmbeddedThreadChat and are untouched.
  • packages/client-core/src/prompt/thread-handoff-request.ts: buildThreadHandoffFollowUpDraft inserts the reference and shifts the draft's mention offsets, stripThreadHandoffPrefix reverses it, and buildThreadHandoffCreateRequest builds the AppCreateThreadRequest with an explicit provider source and provider-gated serviceTier.
  • The "Handoff to new thread" footer action is removed together with everything that existed only for it: the picker's footer action slot and menu action button, the ExecutionControls prop that forwarded it, root compose's handoff-seed location-state handling, and client-core's location-state helpers. ThreadHandoffCreateSeed and the "Continue from" draft builder remain for the inline flow.
  • No wire changes and no new CLI or SDK surface: the UI uses the create-thread endpoint that bb thread spawn --provider --model --prompt and sdk.threads.spawn already expose.

How you verified

  • Remote CI on the PR head ac67895ab (merge of main into 5db1451da): passed. Lint/typecheck, both package smoke jobs, and the app, packages, server, and integration test shards are green; the iOS simulator and Node compatibility jobs were skipped by their own path filters. An earlier head's integration shard failed once on a provisioning-timing smoke test unrelated to this change and passed on rerun.
  • packages/client-core/test/thread-handoff-request.test.ts: inserting the reference keeps later mentions anchored and does not duplicate an existing reference; stripping removes it and re-anchors mentions, tolerates a collapsed blank line, and refuses once the reference was edited; the request is built from the draft as typed with environment reuse, explicit sources, project-default fallback, service-tier gating, and sendAt; null for an empty draft or an unresolved model.
  • apps/app/src/views/thread-detail/ThreadDetailPromptArea.test.tsx: only the bottom composer is provider-switchable while the inline queued editor stays locked; selecting another provider seeds the reference, fires the toast once, retitles submit, and reports ready; switching back restores the typed draft; submitting creates the thread from the draft as typed with the reused environment and navigates without sending or queueing on the current thread. Tests for the removed footer action in this file, ExecutionControls.test.tsx, RootComposeView.test.ts, and PluginNewThreadComposer.test.tsx were deleted.
  • Browser QA in Chrome for Testing 151.0.7922.71 against the branch web app launched with scripts/bb-dev-app current from an isolated worktree, on the thread detail follow-up composer of an idle Codex thread.
  • Desktop 1280×900: the picker shows provider tabs and no footer action. Switching to Claude Code seeds "Continue from [Handoff QA source]" into the composer with the toast. After typing below it, switching back to Codex leaves only the typed text with "Submit (Enter)". Switching to Claude Code again and picking Sonnet 5 re-inserts a single reference with "Create new thread (Enter)". Submitting navigated to the new thread; GET /api/v1/threads/<new> shows providerId: claude-code, the source environmentId, model claude-sonnet-5, and a first message identical to the composer contents including the thread mention.
  • Mobile 390×844 (compact layout, the (max-width: 767px) breakpoint): the picker opens as a drawer with the provider tabs; switching to Claude Code seeds the reference and shows the toast; the collapsed composer reads "Create new thread (Enter)".
  • A plain follow-up with the thread's own provider stayed on the same thread, and the side chat composer is unchanged.
  • Not run: the Electron desktop shell and Safari, since nothing here depends on the native shell or is browser-specific.

Before: merge base 459e9f1d9. After: 5db1451da, the last code commit on this branch; the later ac67895ab only merges main (resolving a test-file conflict) and changes no product code. Same thread, route, fixture data, and picker state.

Before — desktop 1280×900 After — desktop 1280×900 After — mobile 390×844
Before: follow-up picker shows only Codex models and the handoff footer action After: follow-up picker shows provider tabs above the model list and no footer action After on mobile: the picker drawer shows provider tabs above the model list
After — desktop: Claude Code selected, reference seeded, notice shown After — desktop: switched back to Codex, typed text restored
After: composer shows Continue from the source thread and the toast saying the reference was added After: composer shows only the typed follow-up with the Codex model selected
After — desktop: Sonnet 5 selected, ready to create the thread After — desktop: created thread starts with the message as typed
After: composer with the reference, the typed follow-up, and Sonnet 5 selected After: new Claude Code thread whose first message matches the composer contents
After — mobile 390×844: Claude Code selected in the drawer, notice shown After — mobile 390×844: composer with the seeded reference
After on mobile: drawer lists Claude Code models and the toast says the reference was added After on mobile: collapsed composer shows Continue from the source thread

BB-Thread-ID: thr_rmmwartzsr

AGENT GENERATED

🤖 Generated with Claude Code

brsbl and others added 4 commits September 10, 2026 17:00
… is selected

The follow-up composer locked its model picker to the thread's provider, so
handing work to a different model meant leaving the thread through the
"Handoff to new thread" footer action and re-typing the prompt in root compose.

The bottom follow-up composer now exposes the same provider tabs as the
new-thread picker. Selecting a provider other than the thread's own provider
shows a toast explaining that submitting will create a new thread, retitles
the submit control to "Create new thread", and on submit creates a thread on
the selected provider and model that reuses the source environment and starts
with "Continue from @thread:<source>" followed by the typed follow-up, then
navigates to it. Same-provider model changes still send follow-ups to the
current thread. Queued-message inline editors, sent-message edits, and side
chats keep the provider-locked picker; beginning a sent-message edit reverts a
pending cross-provider selection so that edit stays on the thread's provider.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…read

Selecting another provider in the follow-up composer now creates and opens
the new thread directly, so the "Handoff to new thread" footer action in the
model picker duplicated that flow with a worse one: it left the thread and
seeded root compose with a draft that dropped anything already typed.

Remove the footer action and everything that existed only to serve it: the
model picker's footer action slot and its menu action button, the
ExecutionControls prop that forwarded it, root compose's handoff seed
handling, and client-core's handoff location-state helpers. The
ThreadHandoffCreateSeed type and the "Continue from" draft builder stay
because the inline handoff still uses them.

Also fix the CI failures from the previous commit: the provider-switch test
queried a test id that the nested inline editor duplicated, the create-thread
test fixture omitted a required runtime field, and the keystrokes test's
mutation mock lacked useCreateThread.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… submit

Submitting a cross-provider follow-up silently prepended "Continue from
@thread:<source>" to whatever the user typed, so the message that started the
new thread was never the message they saw in the composer.

Selecting another provider now inserts that reference into the draft itself,
followed by a blank line, and focuses the composer so the user can edit or
delete it before submitting. Switching back to the thread's provider removes
the reference again, but only while it is still exactly as inserted. The
create-thread request sends the draft as typed, and the toast now says the
reference was added and can be edited.

client-core gains stripThreadHandoffPrefix as the counterpart of
buildThreadHandoffFollowUpDraft, which also leaves a draft alone when the
reference is already present so re-entering handoff mode never duplicates it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…line

The composer editor stores the seeded "Continue from @thread:<source>" line
followed by a single newline once the user types below it, so the exact
two-newline match never fired: switching back to the thread's provider left
the reference in place and re-entering handoff mode inserted a second copy.

Detect the reference by its intact mention and its own line instead, and
treat any run of newlines after it as the separator when stripping it or
deciding not to insert it again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@brsbl brsbl changed the title Create a new thread from the follow-up composer when another provider is selected Continue a thread on another provider from the follow-up composer Sep 11, 2026
Resolves the conflict in PluginNewThreadComposer.test.tsx by keeping the
upstream tests and dropping the root-compose handoff test again, since the
location-state helpers it used were removed on this branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

1 participant