fix(tui): stabilize generated session titles (#39894)
This commit is contained in:
parent
7fd12c560c
commit
dc3c996892
4 changed files with 191 additions and 32 deletions
|
|
@ -2,41 +2,56 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import type { OpenCodeEvent } from "@opencode-ai/client"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { mkdtempSync, rmSync } from "fs"
|
||||
import { mkdtempSync, rmSync, watch } from "fs"
|
||||
import { tmpdir } from "os"
|
||||
import path from "path"
|
||||
import { ConfigProvider } from "../../src/config"
|
||||
import { ClientProvider, useClient } from "../../src/context/client"
|
||||
import { DataProvider } from "../../src/context/data"
|
||||
import { DataProvider, useData } from "../../src/context/data"
|
||||
import { RouteProvider, useRoute } from "../../src/context/route"
|
||||
import { TuiAppProvider } from "../../src/context/runtime"
|
||||
import { SessionTabsProvider, useSessionTabs } from "../../src/context/session-tabs"
|
||||
import { NEW_SESSION_TAB_TITLE } from "../../src/context/session-tabs-model"
|
||||
import { StorageProvider } from "../../src/context/storage"
|
||||
import { createApi, createEventStream, createFetch, directory } from "../fixture/tui-client"
|
||||
import { createApi, createEventStream, createFetch, directory, json } from "../fixture/tui-client"
|
||||
import { TestTuiContexts } from "../fixture/tui-environment"
|
||||
import { createTuiResolvedConfig } from "../fixture/tui-runtime"
|
||||
|
||||
async function wait(fn: () => boolean, timeout = 2_000) {
|
||||
async function wait(fn: () => boolean | Promise<boolean>, timeout = 2_000) {
|
||||
const start = Date.now()
|
||||
while (!fn()) {
|
||||
while (!(await fn())) {
|
||||
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
|
||||
await Bun.sleep(10)
|
||||
}
|
||||
}
|
||||
|
||||
async function renderSessionTabs(initialSessionID: string) {
|
||||
const state = mkdtempSync(path.join(tmpdir(), "opencode-session-tabs-"))
|
||||
async function renderSessionTabs(initialSessionID: string, options?: { state?: string; title?: string }) {
|
||||
const state = options?.state ?? mkdtempSync(path.join(tmpdir(), "opencode-session-tabs-"))
|
||||
const events = createEventStream()
|
||||
const calls = createFetch(undefined, events)
|
||||
const calls = createFetch((url) => {
|
||||
if (url.pathname !== `/api/session/${initialSessionID}`) return
|
||||
return json({
|
||||
data: {
|
||||
id: initialSessionID,
|
||||
title: options?.title,
|
||||
projectID: "project",
|
||||
location: { directory },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 0, updated: 0 },
|
||||
},
|
||||
})
|
||||
}, events)
|
||||
let tabs!: ReturnType<typeof useSessionTabs>
|
||||
let route!: ReturnType<typeof useRoute>
|
||||
let client!: ReturnType<typeof useClient>
|
||||
let data!: ReturnType<typeof useData>
|
||||
|
||||
function Probe() {
|
||||
tabs = useSessionTabs()
|
||||
route = useRoute()
|
||||
client = useClient()
|
||||
data = useData()
|
||||
return <box />
|
||||
}
|
||||
|
||||
|
|
@ -64,11 +79,12 @@ async function renderSessionTabs(initialSessionID: string) {
|
|||
return {
|
||||
tabs,
|
||||
route,
|
||||
data,
|
||||
state,
|
||||
emit: (event: OpenCodeEvent) => events.emit({ ...event, location: { directory } }),
|
||||
destroy() {
|
||||
app.renderer.destroy()
|
||||
rmSync(state, { recursive: true, force: true })
|
||||
if (!options?.state) rmSync(state, { recursive: true, force: true })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -88,6 +104,50 @@ test("stores session tabs globally by default", async () => {
|
|||
}
|
||||
})
|
||||
|
||||
test("concurrent TUIs do not alternate shared tab titles from divergent session caches", async () => {
|
||||
const state = mkdtempSync(path.join(tmpdir(), "opencode-session-tabs-shared-"))
|
||||
let titled: Awaited<ReturnType<typeof renderSessionTabs>> | undefined
|
||||
let untitled: Awaited<ReturnType<typeof renderSessionTabs>> | undefined
|
||||
|
||||
try {
|
||||
titled = await renderSessionTabs("shared", { state, title: "Generated title" })
|
||||
untitled = await renderSessionTabs("shared", { state })
|
||||
const file = path.join(state, "test", "tui", "tabs.json")
|
||||
await titled.data.session.sync("shared")
|
||||
await wait(async () => {
|
||||
if (!(await Bun.file(file).exists())) return false
|
||||
return (await Bun.file(file).json()).global.tabs[0]?.title === "Generated title"
|
||||
})
|
||||
const observed = ["Generated title"]
|
||||
const pending = new Set<Promise<void>>()
|
||||
const watcher = watch(path.dirname(file), (_, name) => {
|
||||
if (name !== path.basename(file)) return
|
||||
const read = Bun.file(file)
|
||||
.json()
|
||||
.then((value) => {
|
||||
const title = value.global.tabs[0]?.title
|
||||
if (title && observed.at(-1) !== title) observed.push(title)
|
||||
})
|
||||
.catch(() => undefined)
|
||||
.finally(() => pending.delete(read))
|
||||
pending.add(read)
|
||||
})
|
||||
try {
|
||||
await untitled.data.session.sync("shared")
|
||||
await Bun.sleep(500)
|
||||
} finally {
|
||||
watcher.close()
|
||||
await Promise.allSettled(pending)
|
||||
}
|
||||
|
||||
expect(observed).toEqual(["Generated title"])
|
||||
} finally {
|
||||
titled?.destroy()
|
||||
untitled?.destroy()
|
||||
rmSync(state, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test("user prompt admissions pulse an already-busy background tab", async () => {
|
||||
const setup = await renderSessionTabs("background")
|
||||
const admitted = (sessionID: string, inputID: string): OpenCodeEvent => ({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue