Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
43 changes: 28 additions & 15 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
}));
}
if (composerTrigger.kind === "slash-command") {
// Plan/default only matter when the provider honors interactionMode.
// Grok maps plan → `/plan` text on send; OpenCode and others with
// showInteractionModeToggle=false hide these no-op built-ins.
const builtInSlashCommandItems = [
{
id: "slash:model",
Expand All @@ -1092,20 +1095,24 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
label: "/model",
description: "Switch response model for this thread",
},
{
id: "slash:plan",
type: "slash-command",
command: "plan",
label: "/plan",
description: "Switch this thread into plan mode",
},
{
id: "slash:default",
type: "slash-command",
command: "default",
label: "/default",
description: "Switch this thread back to normal build mode",
},
...(composerProviderControls.showInteractionModeToggle
? ([
{
id: "slash:plan",
type: "slash-command",
command: "plan",
label: "/plan",
description: "Switch this thread into plan mode",
},
{
id: "slash:default",
type: "slash-command",
command: "default",
label: "/default",
description: "Switch this thread back to normal build mode",
},
] as const)
: []),
] satisfies ReadonlyArray<Extract<ComposerCommandItem, { type: "slash-command" }>>;
const providerSlashCommandItems = (selectedProviderStatus?.slashCommands ?? []).map(
(command) => ({
Expand Down Expand Up @@ -1140,7 +1147,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
);
}
return [];
}, [composerTrigger, selectedProvider, selectedProviderStatus, workspaceEntries.entries]);
}, [
composerProviderControls.showInteractionModeToggle,
composerTrigger,
selectedProvider,
selectedProviderStatus,
workspaceEntries.entries,
]);

const composerMenuOpen = Boolean(composerTrigger);
const composerMenuSearchKey = composerTrigger
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/components/settings/providerDriverMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ export const PROVIDER_CLIENT_DEFINITIONS: readonly ProviderClientDefinition[] =
value: ProviderDriverKind.make("grok"),
label: "Grok",
icon: GrokIcon,
badgeLabel: "Early Access",
settingsSchema: GrokSettings,
},
{
Expand Down
223 changes: 223 additions & 0 deletions apps/web/src/composerDraftStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"
import {
COMPOSER_DRAFT_STORAGE_KEY,
clearComposerDraftsEnvironment,
deriveEffectiveComposerModelState,
finalizePromotedDraftThreadByRef,
markPromotedDraftThread,
markPromotedDraftThreadByRef,
Expand Down Expand Up @@ -1155,11 +1156,233 @@ describe("composerDraftStore project draft thread mapping", () => {
describe("composerDraftStore modelSelection", () => {
const threadId = ThreadId.make("thread-model-options");
const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId);
const GROK_INSTANCE = ProviderInstanceId.make("grok");
const GROK_DRIVER = ProviderDriverKind.make("grok");

beforeEach(() => {
resetComposerDraftStore();
});

it("prefers sticky effort options over stale thread modelSelection", () => {
const derived = deriveEffectiveComposerModelState({
draft: {
modelSelectionByProvider: {},
activeProvider: null,
},
providers: [
{
instanceId: GROK_INSTANCE,
driver: GROK_DRIVER,
enabled: true,
isAvailable: true,
models: [
{
slug: "grok-4.5",
name: "Grok 4.5",
isCustom: false,
capabilities: { optionDescriptors: [] },
},
],
} as never,
],
selectedProvider: GROK_DRIVER,
selectedInstanceId: GROK_INSTANCE,
threadModelSelection: modelSelection(GROK_DRIVER, "grok-4.5", {
reasoningEffort: "high",
}),
projectModelSelection: null,
stickyModelSelectionByProvider: {
[GROK_INSTANCE]: modelSelection(GROK_DRIVER, "grok-4.5", {
reasoningEffort: "low",
}),
},
settings: {
providers: {
grok: { customModels: [] },
},
providerInstances: {},
models: {},
} as never,
});
expect(derived.modelOptions?.[String(GROK_INSTANCE)]).toEqual(
toSelections({ reasoningEffort: "low" }),
);
});

it("ignores draft options for other instances when resolving sticky for selected", () => {
// Draft has Codex effort; selected is Grok with sticky low — must not let
// the codex map entry short-circuit sticky Grok options.
const derived = deriveEffectiveComposerModelState({
draft: {
modelSelectionByProvider: {
[CODEX_INSTANCE]: modelSelection(CODEX_DRIVER, "gpt-5.4", {
reasoningEffort: "high",
}),
},
activeProvider: null,
},
providers: [
{
instanceId: GROK_INSTANCE,
driver: GROK_DRIVER,
enabled: true,
isAvailable: true,
models: [
{
slug: "grok-4.5",
name: "Grok 4.5",
isCustom: false,
capabilities: { optionDescriptors: [] },
},
],
} as never,
],
selectedProvider: GROK_DRIVER,
selectedInstanceId: GROK_INSTANCE,
threadModelSelection: modelSelection(GROK_DRIVER, "grok-4.5", {
reasoningEffort: "high",
}),
projectModelSelection: null,
stickyModelSelectionByProvider: {
[GROK_INSTANCE]: modelSelection(GROK_DRIVER, "grok-4.5", {
reasoningEffort: "low",
}),
},
settings: {
providers: {
grok: { customModels: [] },
},
providerInstances: {},
models: {},
} as never,
});
expect(derived.modelOptions?.[String(GROK_INSTANCE)]).toEqual(
toSelections({ reasoningEffort: "low" }),
);
expect(derived.modelOptions?.[String(CODEX_INSTANCE)]).toBeUndefined();
});

it("uses sticky options only and keeps thread model when draft is empty", () => {
const derived = deriveEffectiveComposerModelState({
draft: {
modelSelectionByProvider: {},
activeProvider: null,
},
providers: [
{
instanceId: GROK_INSTANCE,
driver: GROK_DRIVER,
enabled: true,
isAvailable: true,
models: [
{
slug: "grok-4.5",
name: "Grok 4.5",
isCustom: false,
capabilities: { optionDescriptors: [] },
},
{
slug: "grok-4",
name: "Grok 4",
isCustom: false,
capabilities: { optionDescriptors: [] },
},
],
} as never,
],
selectedProvider: GROK_DRIVER,
selectedInstanceId: GROK_INSTANCE,
threadModelSelection: modelSelection(GROK_DRIVER, "grok-4.5", {
reasoningEffort: "high",
}),
projectModelSelection: null,
stickyModelSelectionByProvider: {
[GROK_INSTANCE]: modelSelection(GROK_DRIVER, "grok-4", {
reasoningEffort: "low",
}),
},
settings: {
providers: {
grok: { customModels: [] },
},
providerInstances: {},
models: {},
} as never,
});
expect(derived.selectedModel).toBe("grok-4.5");
expect(derived.modelOptions?.[String(GROK_INSTANCE)]).toEqual(
toSelections({ reasoningEffort: "low" }),
);
});

it("re-keys sticky options under selected custom instance", () => {
const derived = deriveEffectiveComposerModelState({
draft: {
modelSelectionByProvider: {},
activeProvider: null,
},
providers: [
{
instanceId: CODEX_SECONDARY_INSTANCE,
driver: CODEX_DRIVER,
enabled: true,
isAvailable: true,
models: [
{
slug: "gpt-5.4",
name: "GPT-5.4",
isCustom: false,
capabilities: { optionDescriptors: [] },
},
],
} as never,
],
selectedProvider: CODEX_DRIVER,
selectedInstanceId: CODEX_SECONDARY_INSTANCE,
threadModelSelection: modelSelection(CODEX_DRIVER, "gpt-5.4", {
reasoningEffort: "high",
}),
projectModelSelection: null,
stickyModelSelectionByProvider: {
[CODEX_INSTANCE]: modelSelection(CODEX_DRIVER, "gpt-5.3-codex", {
reasoningEffort: "low",
}),
},
settings: {
providers: {
codex: { customModels: [] },
},
providerInstances: {},
models: {},
} as never,
});
expect(derived.selectedModel).toBe("gpt-5.4");
expect(derived.modelOptions?.[String(CODEX_SECONDARY_INSTANCE)]).toEqual(
toSelections({ reasoningEffort: "low" }),
);
expect(derived.modelOptions?.[String(CODEX_INSTANCE)]).toBeUndefined();
});

it("persists grok option selections on the draft and sticky map", () => {
const store = useComposerDraftStore.getState();
store.setProviderModelOptions(
threadRef,
GROK_DRIVER,
toSelections({ reasoningEffort: "low" }),
{
instanceId: GROK_INSTANCE,
model: "grok-4.5",
persistSticky: true,
},
);
expect(
draftFor(threadId, TEST_ENVIRONMENT_ID)?.modelSelectionByProvider[GROK_INSTANCE],
).toEqual(modelSelection(GROK_DRIVER, "grok-4.5", { reasoningEffort: "low" }));
expect(useComposerDraftStore.getState().stickyModelSelectionByProvider[GROK_INSTANCE]).toEqual(
modelSelection(GROK_DRIVER, "grok-4.5", { reasoningEffort: "low" }),
);
});

it("stores a model selection in the draft", () => {
const store = useComposerDraftStore.getState();
store.setModelSelection(
Expand Down
Loading
Loading