Compare commits

...
Author SHA1 Message Date
rekram1-node 13e9cbaca9 fix(tui): keep session model stable when recents sync 2026-10-01 16:40:13 +00:00
2 changed files with 40 additions and 1 deletions

No files matched your search

+10 -1
View File
@@ -153,6 +153,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const repository = createModelPreferenceRepository(path.join(paths.state, "model.json"))
const pendingSelectionCommits = new Map<string, { agentID: string; selection: string }>()
// Recents are shared, but an open session's fallback is local until a durable selection replaces it.
const fallbackBySessionAgent = new Map<string, Map<string, ModelSelection>>()
const selectionKey = (value: ModelSelection) =>
`${modelPreferenceKey(value)}:${normalizeModelVariant(value.variant) ?? "default"}`
@@ -267,6 +269,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const selected = [
selectionState.selectionBySessionAgent[sessionID]?.[current.id],
!session?.agent || session.agent === current.id ? durableSelection(sessionID) : undefined,
fallbackBySessionAgent.get(sessionID)?.get(current.id),
].find((selection) => selection && isModelValid(selection))
if (selected) {
const info = models()?.find((item) => item.providerID === selected.providerID && item.id === selected.modelID)
@@ -276,7 +279,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
}
const model = newSessionModel()
return model && preferredSelection(model)
if (!model) return
const fallback = preferredSelection(model)
const byAgent = fallbackBySessionAgent.get(sessionID) ?? new Map<string, ModelSelection>()
byAgent.set(current.id, fallback)
fallbackBySessionAgent.set(sessionID, byAgent)
return fallback
}
function setSessionSelection(sessionID: string, agentID: string, selection: ModelSelection | undefined) {
@@ -344,6 +352,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
onCleanup(
event.on("session.deleted", (evt) => {
pendingSelectionCommits.delete(evt.data.sessionID)
fallbackBySessionAgent.delete(evt.data.sessionID)
setSelectionState("selectionBySessionAgent", evt.data.sessionID, undefined)
}),
)
@@ -1,6 +1,8 @@
import { expect, test } from "bun:test"
import { agent, model, renderLocal, session } from "../fixture/local"
import { json } from "../fixture/tui-client"
import { createModelPreferenceRepository } from "../../src/model-preference"
import path from "node:path"
test("cycles all recent models in a stable order in both directions", async () => {
await using setup = await renderLocal({
@@ -80,6 +82,34 @@ test("agent and model drafts are isolated across sessions and survive navigation
expect(setup.local.model.selection()).toEqual({ providerID: "provider", modelID: "first", variant: "low" })
})
test("a model selected in another instance updates recents without changing this session's model", async () => {
await using setup = await renderLocal({
models: [model("first", ["high"]), model("second")],
preferences: { recent: [{ providerID: "provider", modelID: "first" }] },
sessions: [session("ses_first"), session("ses_second")],
fetch: selectionMessage,
})
await Promise.all([setup.data.session.sync("ses_first"), setup.data.session.sync("ses_second")])
setup.route.navigate({ type: "session", sessionID: "ses_first" })
expect(setup.local.model.selection()).toEqual({ providerID: "provider", modelID: "first", variant: undefined })
const repository = createModelPreferenceRepository(path.join(setup.state, "model.json"))
await repository.addRecent({ providerID: "provider", modelID: "second" })
await repository.setFavorite({ providerID: "provider", modelID: "second" }, true)
await repository.saveVariant({ providerID: "provider", modelID: "first" }, "high")
for (let i = 0; i < 100 && setup.local.model.recent()[0]?.modelID !== "second"; i++) await Bun.sleep(10)
expect(setup.local.model.recent()[0]?.modelID).toBe("second")
expect(setup.local.model.favorite()[0]?.modelID).toBe("second")
expect(setup.local.model.selection()).toEqual({ providerID: "provider", modelID: "first", variant: undefined })
setup.route.navigate({ type: "session", sessionID: "ses_second" })
expect(setup.local.model.current()?.modelID).toBe("second")
setup.route.navigate({ type: "session", sessionID: "ses_first" })
expect(setup.local.model.current()?.modelID).toBe("first")
await publishSelection(setup, "build", "second")
expect(setup.local.model.current()?.modelID).toBe("second")
})
test("falls back from an unavailable session model without changing durable state", async () => {
const selected = { providerID: "provider", id: "missing", variant: "high" }
await using setup = await renderLocal({