Compare commits

...
3 changed files with 169 additions and 5 deletions
+2 -5
View File
@@ -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
})
+21
View File
@@ -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() {
@@ -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()
}
})