fix(tui): sync durable session model changes
This commit is contained in:
parent
3100701488
commit
1dc54945d5
3 changed files with 169 additions and 5 deletions
|
|
@ -340,7 +340,8 @@ export function Prompt(props: PromptProps) {
|
|||
),
|
||||
)
|
||||
|
||||
// Initialize agent/model/variant from the durable V2 Session state.
|
||||
// Initialize the agent from the durable V2 Session state. The model context
|
||||
// follows durable Session model changes while preserving unsent local picks.
|
||||
let syncedSessionID: string | undefined
|
||||
createEffect(() => {
|
||||
const sessionID = props.sessionID
|
||||
|
|
@ -349,10 +350,6 @@ export function Prompt(props: PromptProps) {
|
|||
if (!session) return
|
||||
const agent = session.agent && local.agent.list().find((agent) => agent.id === session.agent)
|
||||
if (agent && !args.agent) local.agent.set(agent.id)
|
||||
if (session.model) {
|
||||
local.model.set({ providerID: session.model.providerID, modelID: session.model.id })
|
||||
local.model.variant.set(session.model.variant)
|
||||
}
|
||||
syncedSessionID = sessionID
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -245,6 +245,27 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||
)
|
||||
})
|
||||
|
||||
let syncedSessionModel: string | undefined
|
||||
createEffect(() => {
|
||||
if (route.data.type !== "session") {
|
||||
syncedSessionModel = undefined
|
||||
return
|
||||
}
|
||||
const session = data.session.get(route.data.sessionID)
|
||||
const selected = session?.model
|
||||
const a = agent.current()
|
||||
if (!selected || !a) return
|
||||
const model = { providerID: selected.providerID, modelID: selected.id }
|
||||
if (!isModelValid(model)) return
|
||||
const fingerprint = [session.id, a.id, selected.providerID, selected.id, selected.variant ?? "default"].join(":")
|
||||
if (fingerprint === syncedSessionModel) return
|
||||
syncedSessionModel = fingerprint
|
||||
batch(() => {
|
||||
setModelStore("model", a.id, model)
|
||||
setModelStore("variant", `${selected.providerID}/${selected.id}`, selected.variant ?? "default")
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
current: currentModel,
|
||||
get ready() {
|
||||
|
|
|
|||
146
packages/tui/test/cli/tui/local-model.test.tsx
Normal file
146
packages/tui/test/cli/tui/local-model.test.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { expect, test } from "bun:test"
|
||||
import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { onMount } from "solid-js"
|
||||
import { ArgsProvider } from "../../../src/context/args"
|
||||
import { DataProvider } from "../../../src/context/data"
|
||||
import { KVProvider } from "../../../src/context/kv"
|
||||
import { LocalProvider, useLocal } from "../../../src/context/local"
|
||||
import { PermissionProvider } from "../../../src/context/permission"
|
||||
import { ProjectProvider } from "../../../src/context/project"
|
||||
import { RouteProvider } from "../../../src/context/route"
|
||||
import { SDKProvider } from "../../../src/context/sdk"
|
||||
import { SyncProvider } from "../../../src/context/sync"
|
||||
import { ThemeProvider } from "../../../src/context/theme"
|
||||
import { TuiConfigProvider } from "../../../src/config"
|
||||
import { ToastProvider } from "../../../src/ui/toast"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
||||
import { createApi, createClient, createEventStream, createFetch, directory, json } from "../../fixture/tui-sdk"
|
||||
|
||||
async function wait(fn: () => boolean, timeout = 2000) {
|
||||
const start = Date.now()
|
||||
while (!fn()) {
|
||||
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
|
||||
await Bun.sleep(10)
|
||||
}
|
||||
}
|
||||
|
||||
test("follows durable model switches for the active session", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const state = path.join(tmp.path, "state")
|
||||
await mkdir(state, { recursive: true })
|
||||
await Bun.write(path.join(state, "kv.json"), "{}")
|
||||
const events = createEventStream()
|
||||
const session = {
|
||||
id: "ses_test",
|
||||
projectID: "proj_test",
|
||||
agent: "build",
|
||||
model: { providerID: "repro", id: "alpha" },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 0, updated: 0 },
|
||||
title: "Test session",
|
||||
location: { directory },
|
||||
}
|
||||
const calls = createFetch((url) => {
|
||||
if (url.pathname === "/api/session") return json({ data: [session], cursor: {} })
|
||||
if (url.pathname === "/api/model")
|
||||
return json({
|
||||
location: { directory, project: { id: "proj_test", directory } },
|
||||
data: [
|
||||
{ providerID: "repro", id: "alpha", name: "Alpha" },
|
||||
{ providerID: "repro", id: "beta", name: "Beta" },
|
||||
{ providerID: "repro", id: "gamma", name: "Gamma", variants: [{ id: "high" }] },
|
||||
],
|
||||
})
|
||||
if (url.pathname === "/api/agent")
|
||||
return json({
|
||||
location: { directory, project: { id: "proj_test", directory } },
|
||||
data: [{ id: "build", mode: "primary", hidden: false }],
|
||||
})
|
||||
return undefined
|
||||
}, events)
|
||||
let local!: ReturnType<typeof useLocal>
|
||||
let mounted!: () => void
|
||||
const ready = new Promise<void>((resolve) => {
|
||||
mounted = resolve
|
||||
})
|
||||
|
||||
function Probe() {
|
||||
local = useLocal()
|
||||
onMount(mounted)
|
||||
return <text>{local.model.current()?.modelID}</text>
|
||||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts directory={tmp.path} paths={{ state }}>
|
||||
<ArgsProvider>
|
||||
<KVProvider>
|
||||
<ToastProvider>
|
||||
<RouteProvider initialRoute={{ type: "session", sessionID: session.id }}>
|
||||
<TuiConfigProvider config={createTuiResolvedConfig()}>
|
||||
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||
<PermissionProvider>
|
||||
<ProjectProvider>
|
||||
<SyncProvider>
|
||||
<DataProvider>
|
||||
<ThemeProvider mode="dark">
|
||||
<LocalProvider>
|
||||
<Probe />
|
||||
</LocalProvider>
|
||||
</ThemeProvider>
|
||||
</DataProvider>
|
||||
</SyncProvider>
|
||||
</ProjectProvider>
|
||||
</PermissionProvider>
|
||||
</SDKProvider>
|
||||
</TuiConfigProvider>
|
||||
</RouteProvider>
|
||||
</ToastProvider>
|
||||
</KVProvider>
|
||||
</ArgsProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
await ready
|
||||
await wait(() => local.model.ready && local.model.current()?.modelID === "alpha")
|
||||
local.model.set({ providerID: "repro", modelID: "beta" })
|
||||
|
||||
events.emit({
|
||||
id: "evt_model_alpha",
|
||||
created: 1,
|
||||
type: "session.model.selected",
|
||||
durable: { aggregateID: session.id, seq: 1, version: 1 },
|
||||
location: { directory },
|
||||
data: {
|
||||
sessionID: session.id,
|
||||
model: { providerID: "repro", id: "alpha" },
|
||||
},
|
||||
})
|
||||
await Bun.sleep(20)
|
||||
expect(local.model.current()?.modelID).toBe("beta")
|
||||
|
||||
events.emit({
|
||||
id: "evt_model_gamma",
|
||||
created: 2,
|
||||
type: "session.model.selected",
|
||||
durable: { aggregateID: session.id, seq: 2, version: 1 },
|
||||
location: { directory },
|
||||
data: {
|
||||
sessionID: session.id,
|
||||
model: { providerID: "repro", id: "gamma", variant: "high" },
|
||||
},
|
||||
})
|
||||
|
||||
await wait(() => local.model.current()?.modelID === "gamma")
|
||||
expect(local.model.current()).toEqual({ providerID: "repro", modelID: "gamma" })
|
||||
expect(local.model.variant.current()).toBe("high")
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue