From 00bc2f16382e31d46c7806ec60ec2e614e5e2158 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Wed, 25 Feb 2026 00:12:04 -0600 Subject: [PATCH 1/5] fix(tui): show models in provider section even when in recents/favorites --- .../opencode/src/cli/cmd/tui/component/dialog-model.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index c30b8d12a933..00c028cce73e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -92,14 +92,6 @@ export function DialogModel(props: { providerID?: string }) { local.model.set({ providerID: provider.id, modelID: model }, { recent: true }) }, })), - filter((x) => { - if (!showSections) return true - if (favorites.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID)) - return false - if (recents.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID)) - return false - return true - }), sortBy( (x) => x.footer !== "Free", (x) => x.title, From 421654a0828b0c4d8d7ef08bed426ebb3705125c Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Wed, 25 Feb 2026 00:19:39 -0600 Subject: [PATCH 2/5] test(tui): add unit tests for model picker filtering logic --- .../cli/cmd/tui/component/dialog-model.tsx | 184 ++++++++++++------ .../test/cli/tui/dialog-model.test.ts | 138 +++++++++++++ 2 files changed, 259 insertions(+), 63 deletions(-) create mode 100644 packages/opencode/test/cli/tui/dialog-model.test.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index 00c028cce73e..933947c1243b 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -8,6 +8,90 @@ import { createDialogProviderOptions, DialogProvider } from "./dialog-provider" import { useKeybind } from "../context/keybind" import * as fuzzysort from "fuzzysort" +type ModelRef = { + providerID: string + modelID: string +} + +type ModelInfo = { + id: string + name?: string + providerID?: string + status?: string + cost?: { + input?: number + } +} + +type ProviderInfo = { + id: string + name: string + models: Record +} + +export function buildSectionOptions(input: { + items: ModelRef[] + category: string + providers: ProviderInfo[] + showSections: boolean +}) { + if (!input.showSections) return [] + return input.items.flatMap((item) => { + const provider = input.providers.find((x) => x.id === item.providerID) + if (!provider) return [] + const model = provider.models[item.modelID] + if (!model) return [] + return [ + { + key: item, + value: { providerID: provider.id, modelID: model.id }, + title: model.name ?? item.modelID, + description: provider.name, + category: input.category, + disabled: provider.id === "opencode" && model.id.includes("-nano"), + footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, + }, + ] + }) +} + +export function buildProviderOptions(input: { + providers: ProviderInfo[] + favorites: ModelRef[] + connected: boolean + providerID?: string +}) { + return pipe( + input.providers, + sortBy( + (provider) => provider.id !== "opencode", + (provider) => provider.name, + ), + flatMap((provider) => + pipe( + provider.models, + entries(), + filter(([_, info]) => info.status !== "deprecated"), + filter(([_, info]) => (input.providerID ? info.providerID === input.providerID : true)), + map(([model, info]) => ({ + value: { providerID: provider.id, modelID: model }, + title: info.name ?? model, + description: input.favorites.some((item) => item.providerID === provider.id && item.modelID === model) + ? "(Favorite)" + : undefined, + category: input.connected ? provider.name : undefined, + disabled: provider.id === "opencode" && model.includes("-nano"), + footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, + })), + sortBy( + (x) => x.footer !== "Free", + (x) => x.title, + ), + ), + ), + ) +} + export function useConnected() { const sync = useSync() return createMemo(() => @@ -33,72 +117,46 @@ export function DialogModel(props: { providerID?: string }) { const favorites = connected() ? local.model.favorite() : [] const recents = local.model.recent() - function toOptions(items: typeof favorites, category: string) { - if (!showSections) return [] - return items.flatMap((item) => { - const provider = sync.data.provider.find((x) => x.id === item.providerID) - if (!provider) return [] - const model = provider.models[item.modelID] - if (!model) return [] - return [ - { - key: item, - value: { providerID: provider.id, modelID: model.id }, - title: model.name ?? item.modelID, - description: provider.name, - category, - disabled: provider.id === "opencode" && model.id.includes("-nano"), - footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, - onSelect: () => { - dialog.clear() - local.model.set({ providerID: provider.id, modelID: model.id }, { recent: true }) - }, - }, - ] - }) - } + const favoriteOptions = buildSectionOptions({ + items: favorites, + category: "Favorites", + providers: sync.data.provider, + showSections, + }).map((option) => ({ + ...option, + onSelect: () => { + dialog.clear() + local.model.set(option.value, { recent: true }) + }, + })) - const favoriteOptions = toOptions(favorites, "Favorites") - const recentOptions = toOptions( - recents.filter( + const recentOptions = buildSectionOptions({ + items: recents.filter( (item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID), ), - "Recent", - ) - - const providerOptions = pipe( - sync.data.provider, - sortBy( - (provider) => provider.id !== "opencode", - (provider) => provider.name, - ), - flatMap((provider) => - pipe( - provider.models, - entries(), - filter(([_, info]) => info.status !== "deprecated"), - filter(([_, info]) => (props.providerID ? info.providerID === props.providerID : true)), - map(([model, info]) => ({ - value: { providerID: provider.id, modelID: model }, - title: info.name ?? model, - description: favorites.some((item) => item.providerID === provider.id && item.modelID === model) - ? "(Favorite)" - : undefined, - category: connected() ? provider.name : undefined, - disabled: provider.id === "opencode" && model.includes("-nano"), - footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, - onSelect() { - dialog.clear() - local.model.set({ providerID: provider.id, modelID: model }, { recent: true }) - }, - })), - sortBy( - (x) => x.footer !== "Free", - (x) => x.title, - ), - ), - ), - ) + category: "Recent", + providers: sync.data.provider, + showSections, + }).map((option) => ({ + ...option, + onSelect: () => { + dialog.clear() + local.model.set(option.value, { recent: true }) + }, + })) + + const providerOptions = buildProviderOptions({ + providers: sync.data.provider, + favorites, + connected: connected(), + providerID: props.providerID, + }).map((option) => ({ + ...option, + onSelect() { + dialog.clear() + local.model.set(option.value, { recent: true }) + }, + })) const popularProviders = !connected() ? pipe( diff --git a/packages/opencode/test/cli/tui/dialog-model.test.ts b/packages/opencode/test/cli/tui/dialog-model.test.ts new file mode 100644 index 000000000000..3774379f9336 --- /dev/null +++ b/packages/opencode/test/cli/tui/dialog-model.test.ts @@ -0,0 +1,138 @@ +import { describe, expect, test } from "bun:test" +import { buildProviderOptions, buildSectionOptions } from "../../../src/cli/cmd/tui/component/dialog-model" + +function provider(id: string, name: string, models: Record) { + return { id, name, models } +} + +describe("dialog-model", () => { + test("models in recents still appear in provider groups", () => { + const providers = [ + provider("anthropic", "Anthropic", { + "claude-3-5-sonnet": { id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet", providerID: "anthropic" }, + }), + ] + const recents = [{ providerID: "anthropic", modelID: "claude-3-5-sonnet" }] + + const recentOptions = buildSectionOptions({ + items: recents, + category: "Recent", + providers, + showSections: true, + }) + const providerOptions = buildProviderOptions({ + providers, + favorites: [], + connected: true, + }) + + expect(recentOptions).toHaveLength(1) + expect(providerOptions.map((x) => x.value)).toContainEqual({ providerID: "anthropic", modelID: "claude-3-5-sonnet" }) + }) + + test("models in favorites still appear in provider groups", () => { + const providers = [ + provider("anthropic", "Anthropic", { + "claude-3-5-sonnet": { id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet", providerID: "anthropic" }, + }), + ] + const favorites = [{ providerID: "anthropic", modelID: "claude-3-5-sonnet" }] + + const result = buildProviderOptions({ + providers, + favorites, + connected: true, + }) + + expect(result.map((x) => x.value)).toContainEqual({ providerID: "anthropic", modelID: "claude-3-5-sonnet" }) + expect(result[0]?.description).toBe("(Favorite)") + }) + + test("models in both recents and favorites still appear in provider groups", () => { + const providers = [ + provider("anthropic", "Anthropic", { + "claude-3-5-sonnet": { id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet", providerID: "anthropic" }, + }), + ] + const favorites = [{ providerID: "anthropic", modelID: "claude-3-5-sonnet" }] + const recents = [{ providerID: "anthropic", modelID: "claude-3-5-sonnet" }] + + const recentOptions = buildSectionOptions({ + items: recents, + category: "Recent", + providers, + showSections: true, + }) + const providerOptions = buildProviderOptions({ + providers, + favorites, + connected: true, + }) + + expect(recentOptions).toHaveLength(1) + expect(providerOptions.map((x) => x.value)).toContainEqual({ providerID: "anthropic", modelID: "claude-3-5-sonnet" }) + expect(providerOptions[0]?.description).toBe("(Favorite)") + }) + + test("empty recents and favorites keeps all provider models", () => { + const providers = [ + provider("anthropic", "Anthropic", { + "claude-3-5-sonnet": { id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet", providerID: "anthropic" }, + }), + provider("openai", "OpenAI", { + "gpt-4o": { id: "gpt-4o", name: "GPT-4o", providerID: "openai" }, + }), + ] + + const result = buildProviderOptions({ + providers, + favorites: [], + connected: true, + }) + + expect(result.map((x) => x.value)).toEqual([ + { providerID: "anthropic", modelID: "claude-3-5-sonnet" }, + { providerID: "openai", modelID: "gpt-4o" }, + ]) + }) + + test("deprecated models are excluded from provider groups", () => { + const providers = [ + provider("anthropic", "Anthropic", { + "claude-3-5-sonnet": { id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet", providerID: "anthropic" }, + "claude-2": { id: "claude-2", name: "Claude 2", providerID: "anthropic", status: "deprecated" }, + }), + ] + + const result = buildProviderOptions({ + providers, + favorites: [], + connected: true, + }) + + expect(result.map((x) => x.value)).toEqual([{ providerID: "anthropic", modelID: "claude-3-5-sonnet" }]) + }) + + test("provider ordering keeps opencode first then alphabetical", () => { + const providers = [ + provider("zeta", "Zeta", { + "zeta-model": { id: "zeta-model", providerID: "zeta" }, + }), + provider("opencode", "OpenCode", { + "opencode-base": { id: "opencode-base", providerID: "opencode", cost: { input: 0 } }, + }), + provider("anthropic", "Anthropic", { + "claude-3-5-sonnet": { id: "claude-3-5-sonnet", providerID: "anthropic" }, + }), + ] + + const result = buildProviderOptions({ + providers, + favorites: [], + connected: true, + }) + + expect(result.map((x) => x.category)).toEqual(["OpenCode", "Anthropic", "Zeta"]) + expect(result[0]?.footer).toBe("Free") + }) +}) From 134fa2f71c4d6b5491d7d820a6a1289afc5b1214 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Wed, 25 Feb 2026 00:47:08 -0600 Subject: [PATCH 3/5] refactor(tui): extract pure model picker helpers to dialog-model-utils.ts Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- .../cmd/tui/component/dialog-model-utils.ts | 85 ++++++++++++++++++ .../cli/cmd/tui/component/dialog-model.tsx | 87 +------------------ .../test/cli/tui/dialog-model.test.ts | 2 +- 3 files changed, 88 insertions(+), 86 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/component/dialog-model-utils.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model-utils.ts b/packages/opencode/src/cli/cmd/tui/component/dialog-model-utils.ts new file mode 100644 index 000000000000..704f816fe59a --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model-utils.ts @@ -0,0 +1,85 @@ +import { map, pipe, flatMap, entries, filter, sortBy } from "remeda" + +export type ModelRef = { + providerID: string + modelID: string +} + +export type ModelInfo = { + id: string + name?: string + providerID?: string + status?: string + cost?: { + input?: number + } +} + +export type ProviderInfo = { + id: string + name: string + models: Record +} + +export function buildSectionOptions(input: { + items: ModelRef[] + category: string + providers: ProviderInfo[] + showSections: boolean +}) { + if (!input.showSections) return [] + return input.items.flatMap((item) => { + const provider = input.providers.find((x) => x.id === item.providerID) + if (!provider) return [] + const model = provider.models[item.modelID] + if (!model) return [] + return [ + { + key: item, + value: { providerID: provider.id, modelID: model.id }, + title: model.name ?? item.modelID, + description: provider.name, + category: input.category, + disabled: provider.id === "opencode" && model.id.includes("-nano"), + footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, + }, + ] + }) +} + +export function buildProviderOptions(input: { + providers: ProviderInfo[] + favorites: ModelRef[] + connected: boolean + providerID?: string +}) { + return pipe( + input.providers, + sortBy( + (provider) => provider.id !== "opencode", + (provider) => provider.name, + ), + flatMap((provider) => + pipe( + provider.models, + entries(), + filter(([_, info]) => info.status !== "deprecated"), + filter(([_, info]) => (input.providerID ? info.providerID === input.providerID : true)), + map(([model, info]) => ({ + value: { providerID: provider.id, modelID: model }, + title: info.name ?? model, + description: input.favorites.some((item) => item.providerID === provider.id && item.modelID === model) + ? "(Favorite)" + : undefined, + category: input.connected ? provider.name : undefined, + disabled: provider.id === "opencode" && model.includes("-nano"), + footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, + })), + sortBy( + (x) => x.footer !== "Free", + (x) => x.title, + ), + ), + ), + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index 933947c1243b..579e4d19e47e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -1,97 +1,14 @@ import { createMemo, createSignal } from "solid-js" import { useLocal } from "@tui/context/local" import { useSync } from "@tui/context/sync" -import { map, pipe, flatMap, entries, filter, sortBy, take } from "remeda" +import { map, pipe, take } from "remeda" import { DialogSelect } from "@tui/ui/dialog-select" import { useDialog } from "@tui/ui/dialog" import { createDialogProviderOptions, DialogProvider } from "./dialog-provider" import { useKeybind } from "../context/keybind" +import { buildSectionOptions, buildProviderOptions } from "./dialog-model-utils" import * as fuzzysort from "fuzzysort" -type ModelRef = { - providerID: string - modelID: string -} - -type ModelInfo = { - id: string - name?: string - providerID?: string - status?: string - cost?: { - input?: number - } -} - -type ProviderInfo = { - id: string - name: string - models: Record -} - -export function buildSectionOptions(input: { - items: ModelRef[] - category: string - providers: ProviderInfo[] - showSections: boolean -}) { - if (!input.showSections) return [] - return input.items.flatMap((item) => { - const provider = input.providers.find((x) => x.id === item.providerID) - if (!provider) return [] - const model = provider.models[item.modelID] - if (!model) return [] - return [ - { - key: item, - value: { providerID: provider.id, modelID: model.id }, - title: model.name ?? item.modelID, - description: provider.name, - category: input.category, - disabled: provider.id === "opencode" && model.id.includes("-nano"), - footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, - }, - ] - }) -} - -export function buildProviderOptions(input: { - providers: ProviderInfo[] - favorites: ModelRef[] - connected: boolean - providerID?: string -}) { - return pipe( - input.providers, - sortBy( - (provider) => provider.id !== "opencode", - (provider) => provider.name, - ), - flatMap((provider) => - pipe( - provider.models, - entries(), - filter(([_, info]) => info.status !== "deprecated"), - filter(([_, info]) => (input.providerID ? info.providerID === input.providerID : true)), - map(([model, info]) => ({ - value: { providerID: provider.id, modelID: model }, - title: info.name ?? model, - description: input.favorites.some((item) => item.providerID === provider.id && item.modelID === model) - ? "(Favorite)" - : undefined, - category: input.connected ? provider.name : undefined, - disabled: provider.id === "opencode" && model.includes("-nano"), - footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, - })), - sortBy( - (x) => x.footer !== "Free", - (x) => x.title, - ), - ), - ), - ) -} - export function useConnected() { const sync = useSync() return createMemo(() => diff --git a/packages/opencode/test/cli/tui/dialog-model.test.ts b/packages/opencode/test/cli/tui/dialog-model.test.ts index 3774379f9336..ba3d5994a9e8 100644 --- a/packages/opencode/test/cli/tui/dialog-model.test.ts +++ b/packages/opencode/test/cli/tui/dialog-model.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { buildProviderOptions, buildSectionOptions } from "../../../src/cli/cmd/tui/component/dialog-model" +import { buildProviderOptions, buildSectionOptions } from "../../../src/cli/cmd/tui/component/dialog-model-utils" function provider(id: string, name: string, models: Record) { return { id, name, models } From 7347061fb6c5b98093ee8d3aea749d98295b3abd Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Wed, 25 Feb 2026 01:16:31 -0600 Subject: [PATCH 4/5] fix(tui): give section items unique values to fix navigation jump dialog-select uses JSON.stringify(value) as the DOM id for scroll targeting. When a model appears in both a section (Favorites/Recent) and its provider group, duplicate ids caused moveTo() to always scroll to the section entry instead of the provider entry. Add a `section` discriminator to values returned by buildSectionOptions so each item has a unique identity. onSelect handlers strip the field before calling local.model.set. --- .../cmd/tui/component/dialog-model-utils.ts | 2 +- .../cli/cmd/tui/component/dialog-model.tsx | 7 +++-- .../test/cli/tui/dialog-model.test.ts | 29 +++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model-utils.ts b/packages/opencode/src/cli/cmd/tui/component/dialog-model-utils.ts index 704f816fe59a..5ff5f9bf718a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model-utils.ts +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model-utils.ts @@ -36,7 +36,7 @@ export function buildSectionOptions(input: { return [ { key: item, - value: { providerID: provider.id, modelID: model.id }, + value: { providerID: provider.id, modelID: model.id, section: input.category }, title: model.name ?? item.modelID, description: provider.name, category: input.category, diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index 579e4d19e47e..05e06ea05922 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -43,7 +43,7 @@ export function DialogModel(props: { providerID?: string }) { ...option, onSelect: () => { dialog.clear() - local.model.set(option.value, { recent: true }) + local.model.set({ providerID: option.value.providerID, modelID: option.value.modelID }, { recent: true }) }, })) @@ -58,7 +58,7 @@ export function DialogModel(props: { providerID?: string }) { ...option, onSelect: () => { dialog.clear() - local.model.set(option.value, { recent: true }) + local.model.set({ providerID: option.value.providerID, modelID: option.value.modelID }, { recent: true }) }, })) @@ -118,7 +118,8 @@ export function DialogModel(props: { providerID?: string }) { title: "Favorite", disabled: !connected(), onTrigger: (option) => { - local.model.toggleFavorite(option.value as { providerID: string; modelID: string }) + const value = option.value as { providerID: string; modelID: string } + local.model.toggleFavorite({ providerID: value.providerID, modelID: value.modelID }) }, }, ]} diff --git a/packages/opencode/test/cli/tui/dialog-model.test.ts b/packages/opencode/test/cli/tui/dialog-model.test.ts index ba3d5994a9e8..fe01fcb7c493 100644 --- a/packages/opencode/test/cli/tui/dialog-model.test.ts +++ b/packages/opencode/test/cli/tui/dialog-model.test.ts @@ -30,6 +30,35 @@ describe("dialog-model", () => { expect(providerOptions.map((x) => x.value)).toContainEqual({ providerID: "anthropic", modelID: "claude-3-5-sonnet" }) }) + test("section options have unique values with section discriminator", () => { + const providers = [ + provider("anthropic", "Anthropic", { + "claude-3-5-sonnet": { id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet", providerID: "anthropic" }, + }), + ] + const favorites = [{ providerID: "anthropic", modelID: "claude-3-5-sonnet" }] + const recents = [{ providerID: "anthropic", modelID: "claude-3-5-sonnet" }] + + const favoriteOptions = buildSectionOptions({ items: favorites, category: "Favorites", providers, showSections: true }) + const recentOptions = buildSectionOptions({ items: recents, category: "Recent", providers, showSections: true }) + const providerOptions = buildProviderOptions({ providers, favorites, connected: true }) + + // Section items must not share value identity with provider items + const providerValue = providerOptions[0]?.value + const favoriteValue = favoriteOptions[0]?.value + const recentValue = recentOptions[0]?.value + expect(favoriteValue).not.toEqual(providerValue) + expect(recentValue).not.toEqual(providerValue) + expect(favoriteValue).not.toEqual(recentValue) + + // Section values carry the section discriminator + expect(favoriteValue).toMatchObject({ providerID: "anthropic", modelID: "claude-3-5-sonnet", section: "Favorites" }) + expect(recentValue).toMatchObject({ providerID: "anthropic", modelID: "claude-3-5-sonnet", section: "Recent" }) + + // Provider values remain plain + expect(providerValue).toEqual({ providerID: "anthropic", modelID: "claude-3-5-sonnet" }) + }) + test("models in favorites still appear in provider groups", () => { const providers = [ provider("anthropic", "Anthropic", { From 8be5fcbd723a15da0bb187f09e900d9876233aa1 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Wed, 25 Feb 2026 01:32:01 -0600 Subject: [PATCH 5/5] test(tui): assert all composed option values produce unique JSON ids Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- .../test/cli/tui/dialog-model.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/opencode/test/cli/tui/dialog-model.test.ts b/packages/opencode/test/cli/tui/dialog-model.test.ts index fe01fcb7c493..4b026bee06c1 100644 --- a/packages/opencode/test/cli/tui/dialog-model.test.ts +++ b/packages/opencode/test/cli/tui/dialog-model.test.ts @@ -164,4 +164,34 @@ describe("dialog-model", () => { expect(result.map((x) => x.category)).toEqual(["OpenCode", "Anthropic", "Zeta"]) expect(result[0]?.footer).toBe("Free") }) + + test("all composed option values produce unique JSON.stringify ids (no duplicate DOM ids)", () => { + const providers = [ + provider("anthropic", "Anthropic", { + "claude-3-5-sonnet": { id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet", providerID: "anthropic" }, + "claude-3-opus": { id: "claude-3-opus", name: "Claude 3 Opus", providerID: "anthropic" }, + }), + provider("openai", "OpenAI", { + "gpt-4o": { id: "gpt-4o", name: "GPT-4o", providerID: "openai" }, + }), + ] + const favorites = [{ providerID: "anthropic", modelID: "claude-3-5-sonnet" }] + const recents = [ + { providerID: "anthropic", modelID: "claude-3-5-sonnet" }, + { providerID: "openai", modelID: "gpt-4o" }, + ] + + const favoriteOptions = buildSectionOptions({ items: favorites, category: "Favorites", providers, showSections: true }) + const recentOptions = buildSectionOptions({ + items: recents.filter((r) => !favorites.some((f) => f.providerID === r.providerID && f.modelID === r.modelID)), + category: "Recent", + providers, + showSections: true, + }) + const providerOptions = buildProviderOptions({ providers, favorites, connected: true }) + + const all = [...favoriteOptions, ...recentOptions, ...providerOptions] + const ids = all.map((o) => JSON.stringify(o.value)) + expect(new Set(ids).size).toBe(ids.length) + }) })