Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
3e6b938
Create a new thread from the follow-up composer when another provider…
brsbl Sep 10, 2026
b7d1a0a
Remove the handoff footer action now that the composer creates the th…
brsbl Sep 10, 2026
b3d04fb
Seed the handoff reference into the draft instead of prepending it on…
brsbl Sep 11, 2026
5db1451
Recognize the handoff reference after the editor collapses its blank …
brsbl Sep 11, 2026
ac67895
Merge origin/main into bb/follow-up-handoff-model-thr_rmmwartzsr
brsbl Sep 11, 2026
b9b50fc
Guide the handoff from the picker footer without leaving the thread
brsbl Sep 11, 2026
8db143f
Keep the handoff step header readable inside the picker width
brsbl Sep 11, 2026
f82ad3f
Make the handoff a picker mode with a composer cap instead of steps a…
brsbl Sep 11, 2026
e57c282
Let the labeled submit button grow on the compact composer
brsbl Sep 11, 2026
35abb69
Exempt the labeled submit button from the compact composer's square s…
brsbl Sep 11, 2026
c84e7db
Restore provider-tab switching and align composer tests with mount cl…
brsbl Sep 11, 2026
e3830ad
Drop the hint line from the picker's handoff header
brsbl Sep 11, 2026
831586a
Assert on the handoff back control instead of the removed hint
brsbl Sep 11, 2026
25a6401
Remove the handoff cap above the follow-up composer
brsbl Sep 11, 2026
0f04be9
Use the new-thread icon on the handoff submit button
brsbl Sep 11, 2026
b6ed4a2
Let the current-thread tab leave handoff mode
brsbl Sep 11, 2026
5726250
Assert on the submit label in the picker-flow handoff test
brsbl Sep 11, 2026
c4a84e7
Move the handoff entry to the top of the picker
brsbl Sep 11, 2026
0ed179b
Revert "Move the handoff entry to the top of the picker"
brsbl Sep 11, 2026
8b9ec60
Tint the handoff-mode header with the selection grey
brsbl Sep 11, 2026
0ec01cb
Continue the handoff tint across the mobile drawer's top
brsbl Sep 11, 2026
5d3c8f2
Merge remote-tracking branch 'origin/main' into bb/get-the-follow-up-…
brsbl Sep 11, 2026
a7db16d
Merge commit '77eeb1be591f62613ecaedffe1f2a69e0cd2cddc' into bb/get-t…
brsbl Sep 11, 2026
412e9de
Merge remote-tracking branch 'origin/main' into bb/get-the-follow-up-…
brsbl Sep 11, 2026
28fce72
Merge remote-tracking branch 'origin/main' into bb/get-the-follow-up-…
brsbl Sep 11, 2026
1388a9c
Keep handoff destination state independent of the source thread
brsbl Sep 11, 2026
b368d79
Merge commit 'b3c5434da' into bb/get-the-follow-up-handoff-pr-merge-r…
brsbl Sep 11, 2026
d1a5364
Update composer fixtures for independent provider catalogs
brsbl Sep 11, 2026
23e3b05
Merge commit '232a5e256' into bb/get-the-follow-up-handoff-pr-merge-r…
brsbl Sep 11, 2026
b5185c6
Merge commit 'd4c3b454e' into bb/get-the-follow-up-handoff-pr-merge-r…
brsbl Sep 11, 2026
8a253e0
Consolidate handoff regression coverage and reduce diff churn
brsbl Sep 11, 2026
2ebccde
Merge remote-tracking branch 'origin/main' into bb/get-the-follow-up-…
brsbl Sep 11, 2026
35229a6
Merge remote-tracking branch 'origin/main' into bb/get-the-follow-up-…
brsbl Sep 12, 2026
3ec95da
Close model picker after selecting a model
brsbl Sep 12, 2026
2159dba
Reopen picker before checking model capabilities
brsbl Sep 12, 2026
9f07797
Restore model picker dismissal behavior
brsbl Sep 12, 2026
7c3a50a
Enter handoff mode from follow-up provider tabs
brsbl Sep 12, 2026
6b06a05
Keep source provider tab styled normally during handoff
brsbl Sep 12, 2026
922819f
Merge remote-tracking branch 'origin/main' into bb/get-the-follow-up-…
brsbl Sep 12, 2026
fdc1378
Keep handoff picker open after destination selection
brsbl Sep 12, 2026
5407064
Merge remote-tracking branch 'origin/main' into bb/get-the-follow-up-…
brsbl Sep 12, 2026
c7e1c4d
Use readable thread mentions in handoff titles
brsbl Sep 12, 2026
da64b58
Revert "Use readable thread mentions in handoff titles"
brsbl Sep 12, 2026
eda926f
Resolve title mentions inside composer thread chips
brsbl Sep 12, 2026
65bc859
Place submit icon before its label
brsbl Sep 12, 2026
7429699
Match handoff header to sort menu label styling
brsbl Sep 12, 2026
47f8d33
Use picker background for handoff header and provider tabs
brsbl Sep 12, 2026
1ab588b
Remove redundant handoff provider-tab routing
brsbl Sep 12, 2026
9a32f8d
Merge remote-tracking branch 'origin/main' into bb/get-the-follow-up-…
brsbl Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/app/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,10 @@
padding: 0;
}

[data-follow-up-composer] [data-promptbox-submit-label] {
display: none;
}

[data-follow-up-composer] [data-follow-up-composer-footer] {
min-height: 0;
max-height: 0;
Expand Down
175 changes: 175 additions & 0 deletions apps/app/src/components/pickers/ModelReasoningPicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import {
buildModelNavRows,
ModelReasoningPicker,
type ModelReasoningPickerHandoff,
} from "./ModelReasoningPicker";
import type { PickerOption } from "./OptionPicker";
import type { ProviderPickerOption } from "./model-brand-prefix";
Expand Down Expand Up @@ -160,6 +161,7 @@ function renderPicker({
compact = false,
splitPane = false,
muted = false,
handoff,
}: {
onSelectedProviderChange?: ((value: string) => void) | null;
onModelChange?: (value: string) => void;
Expand All @@ -178,6 +180,7 @@ function renderPicker({
compact?: boolean;
splitPane?: boolean;
muted?: boolean;
handoff?: ModelReasoningPickerHandoff;
} = {}) {
const { queryClient, wrapper } = createQueryClientTestHarness();
queryClient.setQueryData(
Expand Down Expand Up @@ -219,6 +222,7 @@ function renderPicker({
showFastModeToggle={false}
muted={muted}
modal={false}
handoff={handoff}
/>
<button type="button">Composer action</button>
</div>
Expand Down Expand Up @@ -667,6 +671,177 @@ describe("ModelReasoningPicker", () => {
expect(onModelChange).toHaveBeenCalledWith("claude-opus-4-7");
});

it("opens the same handoff flow from provider tabs and the footer", async () => {
const onSelect = vi.fn();
const { onSelectedProviderChange, onModelChange, onReasoningChange } =
renderPicker({ handoff: { sourceProviderId: "codex", onSelect } });
const trigger = screen.getByRole("button", {
name: "Provider, model and reasoning",
});

fireEvent.click(trigger);
fireEvent.click(screen.getByTitle("Claude Code"));

expect(
screen.getByRole("button", { name: "Back to model picker" }),
).not.toBeNull();
fireEvent.click(screen.getByTitle("Codex (current thread)"));
expect(
screen.queryByRole("button", { name: "Back to model picker" }),
).toBeNull();
expect(onSelectedProviderChange).not.toHaveBeenCalled();

fireEvent.click(
screen.getByRole("button", { name: "Handoff to new thread" }),
);
expect(screen.getByTitle("Codex (current thread)")).not.toBeNull();
expect(await screen.findByText("Opus 4.7")).not.toBeNull();
expect(screen.getAllByText("5.5")).toHaveLength(1);
expect(onSelectedProviderChange).not.toHaveBeenCalled();
expect(
screen.queryByRole("button", { name: "Handoff to new thread" }),
).toBeNull();

fireEvent.click(
screen.getByRole("button", { name: "Back to model picker" }),
);
expect(
screen.queryByRole("button", { name: "Back to model picker" }),
).toBeNull();
expect(screen.getByTitle("Codex")).toHaveProperty("disabled", false);
expect(screen.getAllByText("5.5")).toHaveLength(2);

fireEvent.click(
screen.getByRole("button", { name: "Handoff to new thread" }),
);
fireEvent.click(await screen.findByText("Opus 4.7"));

expect(onSelect).toHaveBeenCalledExactlyOnceWith({
providerId: "claude-code",
model: "claude-opus-4-7",
reasoningLevel: "medium",
});
expect(onSelectedProviderChange).not.toHaveBeenCalled();
expect(onModelChange).not.toHaveBeenCalled();
expect(onReasoningChange).not.toHaveBeenCalled();
expect(trigger.getAttribute("aria-expanded")).toBe("true");
expect(
screen.getByRole("button", { name: "Back to model picker" }),
).not.toBeNull();
});

it.each([
["modelPicker.cycleModel", "claude-sonnet-4-6"],
["modelPicker.cycleModelBackward", "claude-haiku-4-5"],
])(
"%s selects from the handoff provider without changing the source",
async (command, expectedModel) => {
const onSelect = vi.fn();
const { onModelChange, onReasoningChange, onSelectedProviderChange } =
renderPicker({
handoff: { sourceProviderId: "codex", onSelect },
providerRouting: { environmentId: "env-source" },
alternateProviderModels: [
availableModel({
value: "claude-opus-4-7",
label: "Claude Opus 4.7",
isDefault: true,
}),
availableModel({
value: "claude-sonnet-4-6",
label: "Claude Sonnet 4.6",
}),
availableModel({
value: "claude-haiku-4-5",
label: "Claude Haiku 4.5",
}),
],
});
fireEvent.click(
screen.getByRole("button", { name: "Provider, model and reasoning" }),
);
fireEvent.click(
screen.getByRole("button", { name: "Handoff to new thread" }),
);
await screen.findByText("Opus 4.7");
act(() => {
commandHandlers.get(command)?.({ target: document.body });
});
expect(onSelect).toHaveBeenCalledExactlyOnceWith({
providerId: "claude-code",
model: expectedModel,
reasoningLevel: "medium",
});
expect(onModelChange).not.toHaveBeenCalled();
expect(onReasoningChange).not.toHaveBeenCalled();
expect(onSelectedProviderChange).not.toHaveBeenCalled();
},
);

it.each(["modelPicker.cycleReasoning", "modelPicker.cycleReasoningBackward"])(
"%s keeps the handoff preview and applies its reasoning to selection",
async (command) => {
const onSelect = vi.fn();
const { onModelChange, onReasoningChange } = renderPicker({
handoff: { sourceProviderId: "codex", onSelect },
alternateProviderModels: [
{
...availableModel({
value: "claude-opus-4-7",
label: "Claude Opus 4.7",
isDefault: true,
}),
supportedReasoningEfforts: [
{ reasoningEffort: "medium", description: "Medium" },
{ reasoningEffort: "high", description: "High" },
],
},
],
});
fireEvent.click(
screen.getByRole("button", { name: "Provider, model and reasoning" }),
);
fireEvent.click(
screen.getByRole("button", { name: "Handoff to new thread" }),
);
await screen.findByText("Opus 4.7");
act(() => {
commandHandlers.get(command)?.({ target: document.body });
});
expect(screen.getByTitle("Codex (current thread)")).not.toBeNull();
fireEvent.click(screen.getByText("Opus 4.7"));
expect(onSelect).toHaveBeenCalledExactlyOnceWith({
providerId: "claude-code",
model: "claude-opus-4-7",
reasoningLevel: "high",
});
expect(onModelChange).not.toHaveBeenCalled();
expect(onReasoningChange).not.toHaveBeenCalled();
},
);

it("returns to the thread's provider from the current-thread tab", () => {
const onSelect = vi.fn();
const { onSelectedProviderChange } = renderPicker({
selectedProviderId: "claude-code",
handoff: { sourceProviderId: "codex", onSelect },
});

fireEvent.click(
screen.getByRole("button", { name: "Provider, model and reasoning" }),
);
fireEvent.click(
screen.getByRole("button", { name: "Handoff to new thread" }),
);
fireEvent.click(screen.getByTitle("Codex (current thread)"));

expect(onSelectedProviderChange).toHaveBeenCalledExactlyOnceWith("codex");
expect(onSelect).not.toHaveBeenCalled();
expect(
screen.queryByRole("button", { name: "Back to model picker" }),
).toBeNull();
});

it.each([
{
label: "fetches every sibling",
Expand Down
Loading
Loading