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 bcbbe69287b..2eb66e850c6 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -39,10 +39,13 @@ export function DialogModel(props: { providerID?: string }) { const favorites = connected() ? local.model.favorite() : [] const recents = local.model.recent() + const limit = sync.data.config.tui?.recent_models_count ?? 10 const recentList = showSections - ? recents.filter( - (item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID), - ) + ? recents + .filter( + (item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID), + ) + .slice(0, limit) : [] const favoriteOptions = showSections diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index d058ce54fb3..b003a50ec74 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -264,7 +264,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ if (!next) return setModelStore("model", agent.current().name, { ...next }) const uniq = uniqueBy([next, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`) - if (uniq.length > 10) uniq.pop() + const limit = sync.data.config.tui?.recent_models_count ?? 10 + if (uniq.length > limit) uniq.pop() setModelStore( "recent", uniq.map((x) => ({ providerID: x.providerID, modelID: x.modelID })), @@ -284,7 +285,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ setModelStore("model", agent.current().name, model) if (options?.recent) { const uniq = uniqueBy([model, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`) - if (uniq.length > 10) uniq.pop() + const limit = sync.data.config.tui?.recent_models_count ?? 10 + if (uniq.length > limit) uniq.pop() setModelStore( "recent", uniq.map((x) => ({ providerID: x.providerID, modelID: x.modelID })), diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 0edc911344c..ae58b727a80 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -268,6 +268,32 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ } const result = Binary.search(parts, event.properties.part.id, (p) => p.id) if (result.found) { + // For TextParts during streaming, use produce to merge deltas safely + // This prevents race conditions where text-end overwrites accumulated text + const currentPart = parts[result.index] + if (currentPart.type === "text" && event.properties.part.type === "text") { + const incomingTextPart = event.properties.part as Extract + const currentTextPart = currentPart as Extract + + // If this is a streaming update (has text field), use produce for safe merge + if (incomingTextPart.text !== undefined) { + setStore( + "part", + event.properties.part.messageID, + produce((draft) => { + const part = draft[result.index] as Extract + // Update text content but preserve other metadata + part.text = incomingTextPart.text + // Update timing only if end time is provided (text-end event) + if (incomingTextPart.time?.end) { + part.time = incomingTextPart.time + } + }) + ) + break + } + } + // Fall back to reconcile for non-text parts or non-streaming updates setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part)) break } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ddb3af4b0a8..f63dec11ec0 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -796,7 +796,16 @@ export namespace Config { .enum(["auto", "stacked"]) .optional() .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"), + recent_models_count: z + .number() + .int() + .min(1) + .max(50) + .optional() + .default(10) + .describe("Number of recent models to store and display in model selection"), }) + export type TUI = z.infer export const Server = z .object({ diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 04e7144eb72..b230ad1948a 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1608,6 +1608,10 @@ export type Config = { * Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column */ diff_style?: "auto" | "stacked" + /** + * Number of recent models to store and display in model selection + */ + recent_models_count?: number } server?: ServerConfig /**