Skip to content

Commit 0f57090

Browse files
Merge live model threading for the picker marker
2 parents 1e617b1 + 1ce2839 commit 0f57090

5 files changed

Lines changed: 64 additions & 9 deletions

File tree

‎src/tui-opentui/product-host.test.ts‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
permissionChoices,
1414
type ProductHostConfig,
1515
} from "./product-host.js"
16-
import { buildModelsFirstCatalog } from "./model-catalog.js"
16+
import { buildModelsFirstCatalog, modelOptionId } from "./model-catalog.js"
1717

1818
function makeFakeSessionPort(): {
1919
readonly sends: string[]
@@ -383,6 +383,7 @@ describe("provider-first model picker", () => {
383383
interrupt: port.interrupt,
384384
createRenderer: async () => harness.renderer,
385385
models: catalog,
386+
activeModelId: () => modelOptionId("xai/thegreataxios", "grok-4.5"),
386387
onModelSelect: () => {},
387388
})
388389
try {
@@ -396,6 +397,46 @@ describe("provider-first model picker", () => {
396397
}
397398
})
398399

400+
test("stale recents pointing at a different model do not steal the (current) marker", async () => {
401+
// Recents still name the model a *previous* session last switched to;
402+
// this session has run codex/abk-labs / gpt-5.5 all along without ever
403+
// touching the picker. The live model, not the recents list, decides
404+
// which row reads "(current)".
405+
const harness = await createHarness({ width: 80, height: 24 })
406+
const port = makeFakeSessionPort()
407+
const catalog = buildModelsFirstCatalog({
408+
providers,
409+
recent: [{ provider: "xai/thegreataxios", model: "grok-4.5" }],
410+
})
411+
const host = await mountProductHost({
412+
title: "test-session",
413+
eventEmitter: new EventEmitter(),
414+
send: port.send,
415+
interrupt: port.interrupt,
416+
createRenderer: async () => harness.renderer,
417+
models: catalog,
418+
activeModelId: () => modelOptionId("codex/abk-labs", "gpt-5.5"),
419+
onModelSelect: () => {},
420+
})
421+
try {
422+
host.openModels?.()
423+
await harness.renderOnce()
424+
const frame = harness.captureCharFrame()
425+
expect(frame).not.toContain("xai/thegreataxios / grok-4.5 (current)")
426+
const items = host.shell.overlayItems
427+
const codexIndex = items.findIndex((label) => label.includes("codex/abk-labs"))
428+
expect(codexIndex).toBeGreaterThanOrEqual(0)
429+
moveOverlaySelection(host.shell, codexIndex)
430+
acceptOverlaySelection(host.shell)
431+
await harness.renderOnce()
432+
const modelFrame = harness.captureCharFrame()
433+
expect(modelFrame).toContain("gpt-5.5 (current)")
434+
} finally {
435+
host.dispose()
436+
harness.destroy()
437+
}
438+
})
439+
399440
test("fits and scrolls within a short terminal instead of overflowing it", async () => {
400441
const port = makeFakeSessionPort()
401442
const harness = await createHarness({ width: 80, height: 10 })

‎src/tui-opentui/product-host.ts‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@ export type ProductHostConfig = {
160160
readonly deliver?: ProductHostDeliver
161161
/** Model/provider rows for the picker (id applied on select). */
162162
readonly models?: readonly ProductHostModelOption[]
163+
/**
164+
* Row id (`provider:model`) of the model the session is actually running,
165+
* read live on every picker open so it tracks selections made outside the
166+
* picker (e.g. `defaultProvider` at startup). Marks that row "(current)"
167+
* instead of guessing from the recents list.
168+
*/
169+
readonly activeModelId?: () => string | undefined
163170
readonly onModelSelect?: (id: string) => void
164171
/** Description-zone source for the model picker, keyed by row id. */
165172
readonly describeModel?: (itemId: string) => ItemDescription | null
@@ -543,7 +550,7 @@ export async function mountProductHost(
543550
const { groups } = groupModelsForPicker(currentModels)
544551
const group = groups.get(groupProvider)
545552
if (group !== undefined) {
546-
openLevel(annotateCurrent(group.rows, activeModelId()), openModels)
553+
openLevel(annotateCurrent(group.rows, config.activeModelId?.()), openModels)
547554
}
548555
return
549556
}
@@ -567,15 +574,9 @@ export async function mountProductHost(
567574
})
568575
}
569576

570-
// Recent's first row (if any) is the model just switched to — the closest
571-
// thing to a live "current model" id without threading one through from
572-
// the runner. Used only to mark that row "(current)" wherever it appears.
573-
const activeModelId = (): string | undefined =>
574-
currentModels.find((r) => r.section === "recent")?.id
575-
576577
openModels = (): void => {
577578
const { top, groups } = groupModelsForPicker(currentModels)
578-
const activeId = activeModelId()
579+
const activeId = config.activeModelId?.()
579580
// The active model's own row already reads "(current)" via annotateCurrent
580581
// below; when it lives inside a provider group, mark the group row too
581582
// so the pick is visible without descending into it.

‎src/tui-opentui/runner-host.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ describe("mountRunnerHost model picker", () => {
208208
send: () => {},
209209
interrupt: () => {},
210210
providers: { xai: { models: ["grok-4", "grok-3"] } },
211+
activeModel: () => ({ provider: "xai", model: "grok-4" }),
211212
onModelSelect: () => {},
212213
commands: [],
213214
onCommand: () => {},

‎src/tui-opentui/runner-host.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { chromeFromSession, type ChromeSessionInput } from "./chrome-state.js"
2121
import {
2222
buildModelsFirstCatalog,
2323
describeModelCatalogOption,
24+
modelOptionId,
2425
type ModelCatalogOption,
2526
type ModelCatalogProvidersInput,
2627
type ModelCatalogRef,
@@ -67,6 +68,12 @@ export type RunnerHostDeps = {
6768
readonly favoriteModels?: readonly ModelCatalogRef[]
6869
/** Known providers with no stored credentials yet — rendered as "connect →" rows. */
6970
readonly unconnectedProviders?: readonly ModelCatalogUnconnectedProvider[]
71+
/**
72+
* Provider+model the session is actually running, read live on every
73+
* picker open. Marks that row "(current)" — independent of recents, which
74+
* only move on an explicit `/model` pick and can go stale.
75+
*/
76+
readonly activeModel?: () => ModelCatalogRef | undefined
7077
readonly onModelSelect: (id: string) => void
7178
/** Selecting a "connect →" row; runner owns the actual connect flow. */
7279
readonly onConnectProvider?: (providerName: string) => void
@@ -233,6 +240,10 @@ export async function mountRunnerHost(deps: RunnerHostDeps): Promise<RunnerHost>
233240
? { onFavoriteToggle: deps.onFavoriteToggle }
234241
: {}),
235242
models: catalog,
243+
activeModelId: () => {
244+
const active = deps.activeModel?.()
245+
return active ? modelOptionId(active.provider, active.model) : undefined
246+
},
236247
onModelSelect,
237248
describeModel,
238249
commands: buildCommandCatalog(deps.commands),

‎src/tui/runner.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1932,6 +1932,7 @@ export async function runTUI(initialConfig: Config): Promise<number> {
19321932
model: config.model,
19331933
...(config.reasoningEffort !== undefined ? { effort: config.reasoningEffort } : {}),
19341934
}),
1935+
activeModel: () => ({ provider: config.providerName, model: config.model }),
19351936
readCostSummary: () => commandContext.getCostSummary?.(),
19361937
showPromptCost: () => liveShowPromptCost,
19371938
onModelSelect: (id) => {

0 commit comments

Comments
 (0)