Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
17 changes: 13 additions & 4 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -727,9 +727,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
model: selectedModel,
models: selectedProviderModels,
prompt,
modelOptions: composerModelOptions?.[selectedProvider],
modelOptions: composerModelOptions?.[selectedInstanceId],
}),
[composerModelOptions, prompt, selectedModel, selectedProvider, selectedProviderModels],
[
composerModelOptions,
prompt,
selectedInstanceId,
selectedModel,
selectedProvider,
selectedProviderModels,
],
);

const selectedPromptEffort = composerProviderState.promptEffort;
Expand Down Expand Up @@ -1018,21 +1025,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)

const providerTraitsMenuContent = renderProviderTraitsMenuContent({
provider: selectedProvider,
instanceId: selectedInstanceId,
...(routeKind === "server" ? { threadRef: routeThreadRef } : {}),
...(routeKind === "draft" && draftId ? { draftId } : {}),
model: selectedModel,
models: selectedProviderModels,
modelOptions: composerModelOptions?.[selectedProvider],
modelOptions: composerModelOptions?.[selectedInstanceId],
prompt,
onPromptChange: setPromptFromTraits,
});
const providerTraitsPicker = renderProviderTraitsPicker({
provider: selectedProvider,
instanceId: selectedInstanceId,
...(routeKind === "server" ? { threadRef: routeThreadRef } : {}),
...(routeKind === "draft" && draftId ? { draftId } : {}),
model: selectedModel,
models: selectedProviderModels,
modelOptions: composerModelOptions?.[selectedProvider],
modelOptions: composerModelOptions?.[selectedInstanceId],
prompt,
onPromptChange: setPromptFromTraits,
});
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/components/chat/TraitsPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
type ProviderDriverKind,
type ProviderInstanceId,
type ProviderOptionDescriptor,
type ProviderOptionSelection,
type ScopedThreadRef,
Expand Down Expand Up @@ -196,6 +197,7 @@ export function shouldRenderTraitsControls(input: {

export interface TraitsMenuContentProps {
provider: ProviderDriverKind;
instanceId?: ProviderInstanceId;
models: ReadonlyArray<ServerProviderModel>;
model: string | null | undefined;
prompt: string;
Expand All @@ -208,6 +210,7 @@ export interface TraitsMenuContentProps {

export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
provider,
instanceId,
models,
model,
prompt,
Expand All @@ -228,11 +231,12 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
return;
}
setProviderModelOptions(threadTarget, provider, nextOptions, {
...(instanceId ? { instanceId } : {}),
model,
persistSticky: true,
});
},
[model, persistence, provider, setProviderModelOptions],
[instanceId, model, persistence, provider, setProviderModelOptions],
);
const {
descriptors,
Expand Down Expand Up @@ -343,6 +347,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({

export const TraitsPicker = memo(function TraitsPicker({
provider,
instanceId,
models,
model,
prompt,
Expand Down Expand Up @@ -431,6 +436,7 @@ export const TraitsPicker = memo(function TraitsPicker({
<MenuPopup align="start">
<TraitsMenuContent
provider={provider}
{...(instanceId ? { instanceId } : {})}
models={models}
model={model}
prompt={prompt}
Expand Down
16 changes: 14 additions & 2 deletions apps/web/src/components/chat/composerProviderState.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
type ProviderDriverKind,
type ProviderInstanceId,
type ProviderOptionSelection,
type ScopedThreadRef,
type ServerProviderModel,
Expand Down Expand Up @@ -35,6 +36,7 @@ export type ComposerProviderState = {

type TraitsRenderInput = {
provider: ProviderDriverKind;
instanceId?: ProviderInstanceId;
threadRef?: ScopedThreadRef;
draftId?: DraftId;
model: string;
Expand Down Expand Up @@ -76,8 +78,17 @@ function renderTraitsControl(
Component: typeof TraitsMenuContent | typeof TraitsPicker,
input: TraitsRenderInput,
): ReactNode {
const { provider, threadRef, draftId, model, models, modelOptions, prompt, onPromptChange } =
input;
const {
provider,
instanceId,
threadRef,
draftId,
model,
models,
modelOptions,
prompt,
onPromptChange,
} = input;
const hasTarget = threadRef !== undefined || draftId !== undefined;
if (
!hasTarget ||
Expand All @@ -88,6 +99,7 @@ function renderTraitsControl(
return (
<Component
provider={provider}
{...(instanceId ? { instanceId } : {})}
models={models}
{...(threadRef ? { threadRef } : {})}
{...(draftId ? { draftId } : {})}
Expand Down
38 changes: 38 additions & 0 deletions apps/web/src/composerDraftStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { createModelSelection } from "@t3tools/shared/model";
// `stickyModelSelectionByProvider` maps are keyed by `ProviderInstanceId`
// in production; these aliases keep the legacy-key migration tests concise.
const CODEX_INSTANCE = ProviderInstanceId.make("codex");
const CODEX_SECONDARY_INSTANCE = ProviderInstanceId.make("codex_secondary");
const CLAUDE_AGENT_INSTANCE = ProviderInstanceId.make("claudeAgent");
const CURSOR_INSTANCE = ProviderInstanceId.make("cursor");
const CODEX_DRIVER = ProviderDriverKind.make("codex");
Expand Down Expand Up @@ -1201,6 +1202,43 @@ describe("composerDraftStore modelSelection", () => {
);
});

it("stores provider option changes on a selected custom instance", () => {
const store = useComposerDraftStore.getState();

store.setProviderModelOptions(
threadRef,
CODEX_DRIVER,
toSelections({ reasoningEffort: "low" }),
{
instanceId: CODEX_SECONDARY_INSTANCE,
model: "gpt-5-codex",
persistSticky: true,
},
);

expect(
draftFor(threadId, TEST_ENVIRONMENT_ID)?.modelSelectionByProvider[CODEX_SECONDARY_INSTANCE],
).toEqual(
expect.objectContaining({
instanceId: CODEX_SECONDARY_INSTANCE,
options: [{ id: "reasoningEffort", value: "low" }],
}),
);
expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.activeProvider).toBe(CODEX_SECONDARY_INSTANCE);
expect(useComposerDraftStore.getState().stickyActiveProvider).toBe(CODEX_SECONDARY_INSTANCE);
expect(useComposerDraftStore.getState().stickyModelSelectionByProvider[CODEX_INSTANCE]).toBe(
undefined,
);
expect(
useComposerDraftStore.getState().stickyModelSelectionByProvider[CODEX_SECONDARY_INSTANCE],
).toEqual(
expect.objectContaining({
instanceId: CODEX_SECONDARY_INSTANCE,
options: [{ id: "reasoningEffort", value: "low" }],
}),
);
});

it("updates only the draft when sticky persistence is disabled", () => {
const store = useComposerDraftStore.getState();

Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/composerDraftStore.ts
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ interface ComposerDraftStoreState {
provider: ProviderDriverKind,
nextProviderOptions: ReadonlyArray<ProviderOptionSelection> | null | undefined,
options?: {
instanceId?: ProviderInstanceId | null | undefined;
model?: string | null | undefined;
persistSticky?: boolean;
},
Expand Down Expand Up @@ -2456,7 +2457,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
if (normalizedProvider === null) {
return;
}
const instanceKey = defaultInstanceIdForDriver(normalizedProvider);
const instanceKey = options?.instanceId ?? defaultInstanceIdForDriver(normalizedProvider);
const fallbackModel =
normalizeModelSlug(options?.model, normalizedProvider) ??
DEFAULT_MODEL_BY_PROVIDER[normalizedProvider] ??
Expand Down Expand Up @@ -2501,7 +2502,9 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
const { options: _, ...rest } = stickyBase;
nextStickyMap[instanceKey] = rest as ModelSelection;
}
nextStickyActiveProvider = base.activeProvider ?? instanceKey;
nextStickyActiveProvider = options.instanceId
? instanceKey
: (base.activeProvider ?? instanceKey);
}

if (
Expand All @@ -2514,6 +2517,7 @@ const composerDraftStore = create<ComposerDraftStoreState>()(

const nextDraft: ComposerThreadDraftState = {
...base,
...(options?.instanceId ? { activeProvider: instanceKey } : {}),
modelSelectionByProvider: nextMap,
};
const nextDraftsByThreadKey = { ...state.draftsByThreadKey };
Expand Down
Loading