From ba1edea0ab092a93772f5ad657809279f872f61e Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sat, 7 Mar 2026 06:57:00 -0600 Subject: [PATCH] fix(app): model sticks to session --- .../components/prompt-input/submit.test.ts | 58 ++++++++++++- .../app/src/components/prompt-input/submit.ts | 1 + packages/app/src/context/sync.tsx | 2 + packages/app/src/pages/session.tsx | 7 +- .../session/session-model-helpers.test.ts | 83 +++++++++++++++++++ .../pages/session/session-model-helpers.ts | 33 ++++++++ 6 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 packages/app/src/pages/session/session-model-helpers.test.ts create mode 100644 packages/app/src/pages/session/session-model-helpers.ts diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index c633525a2..4109417d2 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -6,10 +6,19 @@ let createPromptSubmit: typeof import("./submit").createPromptSubmit const createdClients: string[] = [] const createdSessions: string[] = [] const enabledAutoAccept: Array<{ sessionID: string; directory: string }> = [] +const optimistic: Array<{ + message: { + agent: string + model: { providerID: string; modelID: string } + variant?: string + } +}> = [] const sentShell: string[] = [] const syncedDirectories: string[] = [] +let params: { id?: string } = {} let selected = "/repo/worktree-a" +let variant: string | undefined const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }] @@ -26,6 +35,7 @@ const clientFor = (directory: string) => { return { data: undefined } }, prompt: async () => ({ data: undefined }), + promptAsync: async () => ({ data: undefined }), command: async () => ({ data: undefined }), abort: async () => ({ data: undefined }), }, @@ -40,7 +50,7 @@ beforeAll(async () => { mock.module("@solidjs/router", () => ({ useNavigate: () => () => undefined, - useParams: () => ({}), + useParams: () => params, })) mock.module("@opencode-ai/sdk/v2/client", () => ({ @@ -62,7 +72,7 @@ beforeAll(async () => { useLocal: () => ({ model: { current: () => ({ id: "model", provider: { id: "provider" } }), - variant: { current: () => undefined }, + variant: { current: () => variant }, }, agent: { current: () => ({ name: "agent" }), @@ -118,7 +128,11 @@ beforeAll(async () => { data: { command: [] }, session: { optimistic: { - add: () => undefined, + add: (value: { + message: { agent: string; model: { providerID: string; modelID: string }; variant?: string } + }) => { + optimistic.push(value) + }, remove: () => undefined, }, }, @@ -155,9 +169,12 @@ beforeEach(() => { createdClients.length = 0 createdSessions.length = 0 enabledAutoAccept.length = 0 + optimistic.length = 0 + params = {} sentShell.length = 0 syncedDirectories.length = 0 selected = "/repo/worktree-a" + variant = undefined }) describe("prompt submit worktree selection", () => { @@ -219,4 +236,39 @@ describe("prompt submit worktree selection", () => { expect(enabledAutoAccept).toEqual([{ sessionID: "session-1", directory: "/repo/worktree-a" }]) }) + + test("includes the selected variant on optimistic prompts", async () => { + params = { id: "session-1" } + variant = "high" + + const submit = createPromptSubmit({ + info: () => ({ id: "session-1" }), + imageAttachments: () => [], + commentCount: () => 0, + autoAccept: () => false, + mode: () => "normal", + working: () => false, + editor: () => undefined, + queueScroll: () => undefined, + promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), + addToHistory: () => undefined, + resetHistoryNavigation: () => undefined, + setMode: () => undefined, + setPopover: () => undefined, + onSubmit: () => undefined, + }) + + const event = { preventDefault: () => undefined } as unknown as Event + + await submit.handleSubmit(event) + + expect(optimistic).toHaveLength(1) + expect(optimistic[0]).toMatchObject({ + message: { + agent: "agent", + model: { providerID: "provider", modelID: "model" }, + variant: "high", + }, + }) + }) }) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index db1b5a5ca..fee6b070d 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -316,6 +316,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { time: { created: Date.now() }, agent, model, + variant, } const addOptimisticMessage = () => diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index ed54751c3..562a2d19c 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -199,6 +199,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ parts: Part[] agent: string model: { providerID: string; modelID: string } + variant?: string }) { const message: Message = { id: input.messageID, @@ -207,6 +208,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ time: { created: Date.now() }, agent: input.agent, model: input.model, + variant: input.variant, } const [, setStore] = target() setOptimisticAdd(setStore as (...args: unknown[]) => void, { diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index cba49f5fb..ba9b14c12 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -36,6 +36,7 @@ import { createSessionComposerState, SessionComposerRegion } from "@/pages/sessi import { createOpenReviewFile, createSizing } from "@/pages/session/helpers" import { MessageTimeline } from "@/pages/session/message-timeline" import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab" +import { syncSessionModel } from "@/pages/session/session-model-helpers" import { createScrollSpy } from "@/pages/session/scroll-spy" import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs" import { SessionSidePanel } from "@/pages/session/session-side-panel" @@ -418,11 +419,7 @@ export default function Page() { () => { const msg = lastUserMessage() if (!msg) return - if (msg.agent) { - local.agent.set(msg.agent) - if (local.agent.current()?.model) return - } - if (msg.model) local.model.set(msg.model) + syncSessionModel(local, msg) }, ), ) diff --git a/packages/app/src/pages/session/session-model-helpers.test.ts b/packages/app/src/pages/session/session-model-helpers.test.ts new file mode 100644 index 000000000..ae8700ccd --- /dev/null +++ b/packages/app/src/pages/session/session-model-helpers.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, test } from "bun:test" +import type { UserMessage } from "@opencode-ai/sdk/v2" +import { syncSessionModel } from "./session-model-helpers" + +const message = (input?: Partial>) => + ({ + id: "msg", + sessionID: "session", + role: "user", + time: { created: 1 }, + agent: input?.agent ?? "build", + model: input?.model ?? { providerID: "anthropic", modelID: "claude-sonnet-4" }, + variant: input?.variant, + }) as UserMessage + +describe("syncSessionModel", () => { + test("restores the last message model and variant", () => { + const calls: unknown[] = [] + + syncSessionModel( + { + agent: { + set(value) { + calls.push(["agent", value]) + }, + }, + model: { + set(value) { + calls.push(["model", value]) + }, + current() { + return { id: "claude-sonnet-4", provider: { id: "anthropic" } } + }, + variant: { + set(value) { + calls.push(["variant", value]) + }, + }, + }, + }, + message({ variant: "high" }), + ) + + expect(calls).toEqual([ + ["agent", "build"], + ["model", { providerID: "anthropic", modelID: "claude-sonnet-4" }], + ["variant", "high"], + ]) + }) + + test("skips variant when the model falls back", () => { + const calls: unknown[] = [] + + syncSessionModel( + { + agent: { + set(value) { + calls.push(["agent", value]) + }, + }, + model: { + set(value) { + calls.push(["model", value]) + }, + current() { + return { id: "gpt-5", provider: { id: "openai" } } + }, + variant: { + set(value) { + calls.push(["variant", value]) + }, + }, + }, + }, + message({ variant: "high" }), + ) + + expect(calls).toEqual([ + ["agent", "build"], + ["model", { providerID: "anthropic", modelID: "claude-sonnet-4" }], + ]) + }) +}) diff --git a/packages/app/src/pages/session/session-model-helpers.ts b/packages/app/src/pages/session/session-model-helpers.ts new file mode 100644 index 000000000..ab181df59 --- /dev/null +++ b/packages/app/src/pages/session/session-model-helpers.ts @@ -0,0 +1,33 @@ +import type { UserMessage } from "@opencode-ai/sdk/v2" +import { batch } from "solid-js" + +type Local = { + agent: { + set(name: string | undefined): void + } + model: { + set(model: UserMessage["model"] | undefined): void + current(): + | { + id: string + provider: { id: string } + } + | undefined + variant: { + set(value: string | undefined): void + } + } +} + +export const syncSessionModel = (local: Local, msg: UserMessage) => { + batch(() => { + local.agent.set(msg.agent) + local.model.set(msg.model) + }) + + const model = local.model.current() + if (!model) return + if (model.provider.id !== msg.model.providerID) return + if (model.id !== msg.model.modelID) return + local.model.variant.set(msg.variant) +}