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
|
let syncedSessionID: string | undefined
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const sessionID = props.sessionID
|
const sessionID = props.sessionID
|
||||||
|
|
@ -349,10 +350,6 @@ export function Prompt(props: PromptProps) {
|
||||||
if (!session) return
|
if (!session) return
|
||||||
const agent = session.agent && local.agent.list().find((agent) => agent.id === session.agent)
|
const agent = session.agent && local.agent.list().find((agent) => agent.id === session.agent)
|
||||||
if (agent && !args.agent) local.agent.set(agent.id)
|
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
|
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 {
|
return {
|
||||||
current: currentModel,
|
current: currentModel,
|
||||||
get ready() {
|
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