diff options
| author | Adam <[email protected]> | 2026-03-13 11:05:08 -0500 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-13 11:05:08 -0500 |
| commit | 4ad8116ce37a0e77e7f3c0e9e4e1002bba05b15e (patch) | |
| tree | b7e5ed2b05aabb5ed5134520c4eb485c52eb5333 /packages/app/src/components | |
| parent | 5c7088338c07ad632834ebd4a87feb23d255fb8a (diff) | |
| download | opencode-4ad8116ce37a0e77e7f3c0e9e4e1002bba05b15e.tar.gz opencode-4ad8116ce37a0e77e7f3c0e9e4e1002bba05b15e.zip | |
fix(app): model selection persist by session (#17348)
Diffstat (limited to 'packages/app/src/components')
5 files changed, 116 insertions, 85 deletions
diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index bcee3f501..2106b3a01 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -13,8 +13,10 @@ import { DialogSelectProvider } from "./dialog-select-provider" import { ModelTooltip } from "./model-tooltip" import { useLanguage } from "@/context/language" -export const DialogSelectModelUnpaid: Component = () => { - const local = useLocal() +type ModelState = ReturnType<typeof useLocal>["model"] + +export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props) => { + const model = props.model ?? useLocal().model const dialog = useDialog() const providers = useProviders() const language = useLanguage() @@ -35,8 +37,8 @@ export const DialogSelectModelUnpaid: Component = () => { <List class="[&_[data-slot=list-scroll]]:overflow-visible" ref={(ref) => (listRef = ref)} - items={local.model.list} - current={local.model.current()} + items={model.list} + current={model.current()} key={(x) => `${x.provider.id}:${x.id}`} itemWrapper={(item, node) => ( <Tooltip @@ -55,7 +57,7 @@ export const DialogSelectModelUnpaid: Component = () => { </Tooltip> )} onSelect={(x) => { - local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { + model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true, }) dialog.close() diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 9f7afb8cd..3654aab85 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -18,19 +18,22 @@ import { useLanguage } from "@/context/language" const isFree = (provider: string, cost: { input: number } | undefined) => provider === "opencode" && (!cost || cost.input === 0) +type ModelState = ReturnType<typeof useLocal>["model"] + const ModelList: Component<{ provider?: string class?: string onSelect: () => void action?: JSX.Element + model?: ModelState }> = (props) => { - const local = useLocal() + const model = props.model ?? useLocal().model const language = useLanguage() const models = createMemo(() => - local.model + model .list() - .filter((m) => local.model.visible({ modelID: m.id, providerID: m.provider.id })) + .filter((m) => model.visible({ modelID: m.id, providerID: m.provider.id })) .filter((m) => (props.provider ? m.provider.id === props.provider : true)), ) @@ -41,7 +44,7 @@ const ModelList: Component<{ emptyMessage={language.t("dialog.model.empty")} key={(x) => `${x.provider.id}:${x.id}`} items={models} - current={local.model.current()} + current={model.current()} filterKeys={["provider.name", "name", "id"]} sortBy={(a, b) => a.name.localeCompare(b.name)} groupBy={(x) => x.provider.name} @@ -63,7 +66,7 @@ const ModelList: Component<{ </Tooltip> )} onSelect={(x) => { - local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { + model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true, }) props.onSelect() @@ -88,6 +91,7 @@ type ModelSelectorTriggerProps = Omit<ComponentProps<typeof Kobalte.Trigger>, "a export function ModelSelectorPopover(props: { provider?: string + model?: ModelState children?: JSX.Element triggerAs?: ValidComponent triggerProps?: ModelSelectorTriggerProps @@ -151,6 +155,7 @@ export function ModelSelectorPopover(props: { <Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title> <ModelList provider={props.provider} + model={props.model} onSelect={() => setStore("open", false)} class="p-1" action={ @@ -184,7 +189,7 @@ export function ModelSelectorPopover(props: { ) } -export const DialogSelectModel: Component<{ provider?: string }> = (props) => { +export const DialogSelectModel: Component<{ provider?: string; model?: ModelState }> = (props) => { const dialog = useDialog() const language = useLanguage() @@ -202,7 +207,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => { </Button> } > - <ModelList provider={props.provider} onSelect={() => dialog.close()} /> + <ModelList provider={props.provider} model={props.model} onSelect={() => dialog.close()} /> <Button variant="ghost" class="ml-3 mt-5 mb-6 text-text-base self-start" diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index fd54de9a0..9048fa895 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1430,39 +1430,76 @@ export const PromptInput: Component<PromptInputProps> = (props) => { <div class="size-4 shrink-0" /> </div> <div class="flex items-center gap-1.5 min-w-0 flex-1"> - <TooltipKeybind - placement="top" - gutter={4} - title={language.t("command.agent.cycle")} - keybind={command.keybind("agent.cycle")} - > - <Select - size="normal" - options={agentNames()} - current={local.agent.current()?.name ?? ""} - onSelect={local.agent.set} - class="capitalize max-w-[160px] text-text-base" - valueClass="truncate text-13-regular text-text-base" - triggerStyle={control()} - variant="ghost" - /> - </TooltipKeybind> - <Show - when={providers.paid().length > 0} - fallback={ + <div data-component="prompt-agent-control"> + <TooltipKeybind + placement="top" + gutter={4} + title={language.t("command.agent.cycle")} + keybind={command.keybind("agent.cycle")} + > + <Select + size="normal" + options={agentNames()} + current={local.agent.current()?.name ?? ""} + onSelect={local.agent.set} + class="capitalize max-w-[160px] text-text-base" + valueClass="truncate text-13-regular text-text-base" + triggerStyle={control()} + triggerProps={{ "data-action": "prompt-agent" }} + variant="ghost" + /> + </TooltipKeybind> + </div> + <div data-component="prompt-model-control"> + <Show + when={providers.paid().length > 0} + fallback={ + <TooltipKeybind + placement="top" + gutter={4} + title={language.t("command.model.choose")} + keybind={command.keybind("model.choose")} + > + <Button + data-action="prompt-model" + as="div" + variant="ghost" + size="normal" + class="min-w-0 max-w-[320px] text-13-regular text-text-base group" + style={control()} + onClick={() => dialog.show(() => <DialogSelectModelUnpaid model={local.model} />)} + > + <Show when={local.model.current()?.provider?.id}> + <ProviderIcon + id={local.model.current()!.provider.id} + class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150" + style={{ "will-change": "opacity", transform: "translateZ(0)" }} + /> + </Show> + <span class="truncate"> + {local.model.current()?.name ?? language.t("dialog.model.select.title")} + </span> + <Icon name="chevron-down" size="small" class="shrink-0" /> + </Button> + </TooltipKeybind> + } + > <TooltipKeybind placement="top" gutter={4} title={language.t("command.model.choose")} keybind={command.keybind("model.choose")} > - <Button - as="div" - variant="ghost" - size="normal" - class="min-w-0 max-w-[320px] text-13-regular text-text-base group" - style={control()} - onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)} + <ModelSelectorPopover + model={local.model} + triggerAs={Button} + triggerProps={{ + variant: "ghost", + size: "normal", + style: control(), + class: "min-w-0 max-w-[320px] text-13-regular text-text-base group", + "data-action": "prompt-model", + }} > <Show when={local.model.current()?.provider?.id}> <ProviderIcon @@ -1475,57 +1512,31 @@ export const PromptInput: Component<PromptInputProps> = (props) => { {local.model.current()?.name ?? language.t("dialog.model.select.title")} </span> <Icon name="chevron-down" size="small" class="shrink-0" /> - </Button> + </ModelSelectorPopover> </TooltipKeybind> - } - > + </Show> + </div> + <div data-component="prompt-variant-control"> <TooltipKeybind placement="top" gutter={4} - title={language.t("command.model.choose")} - keybind={command.keybind("model.choose")} + title={language.t("command.model.variant.cycle")} + keybind={command.keybind("model.variant.cycle")} > - <ModelSelectorPopover - triggerAs={Button} - triggerProps={{ - variant: "ghost", - size: "normal", - style: control(), - class: "min-w-0 max-w-[320px] text-13-regular text-text-base group", - }} - > - <Show when={local.model.current()?.provider?.id}> - <ProviderIcon - id={local.model.current()!.provider.id} - class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150" - style={{ "will-change": "opacity", transform: "translateZ(0)" }} - /> - </Show> - <span class="truncate"> - {local.model.current()?.name ?? language.t("dialog.model.select.title")} - </span> - <Icon name="chevron-down" size="small" class="shrink-0" /> - </ModelSelectorPopover> + <Select + size="normal" + options={variants()} + current={local.model.variant.current() ?? "default"} + label={(x) => (x === "default" ? language.t("common.default") : x)} + onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)} + class="capitalize max-w-[160px] text-text-base" + valueClass="truncate text-13-regular text-text-base" + triggerStyle={control()} + triggerProps={{ "data-action": "prompt-model-variant" }} + variant="ghost" + /> </TooltipKeybind> - </Show> - <TooltipKeybind - placement="top" - gutter={4} - title={language.t("command.model.variant.cycle")} - keybind={command.keybind("model.variant.cycle")} - > - <Select - size="normal" - options={variants()} - current={local.model.variant.current() ?? "default"} - label={(x) => (x === "default" ? language.t("common.default") : x)} - onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)} - class="capitalize max-w-[160px] text-text-base" - valueClass="truncate text-13-regular text-text-base" - triggerStyle={control()} - variant="ghost" - /> - </TooltipKeybind> + </div> <TooltipKeybind placement="top" gutter={8} diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index 9f7fac69d..b0166c43a 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -17,6 +17,7 @@ const optimistic: Array<{ }> = [] const optimisticSeeded: boolean[] = [] const storedSessions: Record<string, Array<{ id: string; title?: string }>> = {} +const promoted: Array<{ directory: string; sessionID: string }> = [] const sentShell: string[] = [] const syncedDirectories: string[] = [] @@ -86,6 +87,11 @@ beforeAll(async () => { agent: { current: () => ({ name: "agent" }), }, + session: { + promote(directory: string, sessionID: string) { + promoted.push({ directory, sessionID }) + }, + }, }), })) @@ -201,6 +207,7 @@ beforeEach(() => { enabledAutoAccept.length = 0 optimistic.length = 0 optimisticSeeded.length = 0 + promoted.length = 0 params = {} sentShell.length = 0 syncedDirectories.length = 0 @@ -240,6 +247,11 @@ describe("prompt submit worktree selection", () => { expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"]) expect(sentShell).toEqual(["/repo/worktree-a", "/repo/worktree-b"]) expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"]) + expect(promoted).toEqual([ + { directory: "/repo/worktree-a", sessionID: "session-1" }, + { directory: "/repo/worktree-b", sessionID: "session-2" }, + ]) + expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"]) }) test("applies auto-accept to newly created sessions", async () => { diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index e8d765cd9..ba299fe36 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -296,6 +296,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { const currentModel = local.model.current() const currentAgent = local.agent.current() + const variant = local.model.variant.current() if (!currentModel || !currentAgent) { showToast({ title: language.t("prompt.toast.modelAgentRequired.title"), @@ -370,6 +371,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { seed(sessionDirectory, created) session = created if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory) + local.session.promote(sessionDirectory, session.id) layout.handoff.setTabs(base64Encode(sessionDirectory), session.id) navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`) } @@ -387,7 +389,6 @@ export function createPromptSubmit(input: PromptSubmitInput) { providerID: currentModel.provider.id, } const agent = currentAgent.name - const variant = local.model.variant.current() const context = prompt.context.items().slice() const draft: FollowupDraft = { sessionID: session.id, |
