fix(tui): load sidebar project names sooner (#40763)

This commit is contained in:
Kit Langton 2026-08-05 19:31:42 -04:00 committed by GitHub
parent c46f6ae112
commit 6f91bc7415
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 54 additions and 16 deletions

View file

@ -157,13 +157,9 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
})
})
// Warm open tabs' session data so first switches render from cache instead of fetching inside
// the switch gesture. Uses only existing sync methods (each dedupes internally), so reruns on
// tab-set or connection changes are no-ops for already-warm sessions, and reconnects double as
// a cache refresh after an SSE gap. The delay lets the current session's own mount syncs get
// the first connection slots. The effect tracks only the id set: reorders, tab switches, and
// title updates neither restart the timer nor an in-flight warm pass; the timer callback
// itself runs untracked, where the current session is skipped.
// Load lightweight session metadata concurrently so persisted tabs can resolve their project
// labels immediately. Delay the heavier per-tab data so the visible session keeps the first
// connection slots and switches still render from a warm cache.
const openTabSessions = createMemo(() =>
state()
.tabs.map((tab) => tab.sessionID)
@ -173,7 +169,9 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
createEffect(() => {
if (!enabled()) return
if (client.connection.status() !== "connected") return
if (openTabSessions() === "") return
const sessionIDs = openTabSessions()
if (sessionIDs === "") return
void Promise.allSettled(sessionIDs.split("\n").map((sessionID) => data.session.sync(sessionID)))
let stale = false
const timer = setTimeout(async () => {
const sessions = state()
@ -182,7 +180,6 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
for (const sessionID of sessions) {
if (stale) return
await Promise.allSettled([
data.session.sync(sessionID),
data.session.message.sync(sessionID),
data.session.pending.sync(sessionID),
data.session.permission.sync(sessionID),

View file

@ -2,7 +2,7 @@
import { afterAll, expect, test } from "bun:test"
import type { OpenCodeEvent } from "@opencode-ai/client"
import { testRender } from "@opentui/solid"
import { mkdtempSync, readdirSync, rmSync, watch } from "fs"
import { mkdirSync, mkdtempSync, readdirSync, rmSync, watch } from "fs"
import { tmpdir } from "os"
import path from "path"
import { ConfigProvider } from "../../src/config"
@ -49,15 +49,33 @@ function stateDir(prefix: string) {
return dir
}
async function renderSessionTabs(initialSessionID: string, options?: { state?: string; title?: string }) {
async function renderSessionTabs(
initialSessionID: string,
options?: { state?: string; title?: string; home?: boolean; persisted?: string[]; sessionGate?: Promise<void> },
) {
const state = options?.state ?? stateDir("opencode-session-tabs-")
if (options?.persisted) {
const file = path.join(state, "test", "tui", "tabs.json")
mkdirSync(path.dirname(file), { recursive: true })
await Bun.write(
file,
JSON.stringify({
global: { tabs: options.persisted.map((sessionID) => ({ sessionID })), unread: {} },
cwd: {},
}),
)
}
const events = createEventStream()
const calls = createFetch((url) => {
if (url.pathname !== `/api/session/${initialSessionID}`) return
const sessions: string[] = []
const calls = createFetch(async (url) => {
const sessionID = url.pathname.match(/^\/api\/session\/([^/]+)$/)?.[1]
if (!sessionID) return undefined
sessions.push(sessionID)
await options?.sessionGate
return json({
data: {
id: initialSessionID,
title: options?.title,
id: sessionID,
title: sessionID === initialSessionID ? options?.title : undefined,
projectID: "project",
location: { directory },
cost: 0,
@ -84,7 +102,9 @@ async function renderSessionTabs(initialSessionID: string, options?: { state?: s
<TuiAppProvider value={{ name: "test", version: "test", channel: "test" }}>
<StorageProvider>
<ConfigProvider config={createTuiResolvedConfig({ tabs: { enabled: true } })}>
<RouteProvider initialRoute={{ type: "session", sessionID: initialSessionID }}>
<RouteProvider
initialRoute={options?.home ? { type: "home" } : { type: "session", sessionID: initialSessionID }}
>
<ClientProvider api={createApi(calls.fetch)}>
<DataProvider>
<SessionTabsProvider>
@ -104,6 +124,7 @@ async function renderSessionTabs(initialSessionID: string, options?: { state?: s
tabs,
route,
data,
sessions,
state,
emit: (event: OpenCodeEvent) => events.emit({ ...event, location: { directory } }),
destroy() {
@ -112,6 +133,26 @@ async function renderSessionTabs(initialSessionID: string, options?: { state?: s
}
}
test("loads persisted tab metadata concurrently on connect", async () => {
let release!: () => void
const sessionGate = new Promise<void>((resolve) => (release = resolve))
const setup = await renderSessionTabs("first", {
home: true,
persisted: ["first", "second"],
sessionGate,
})
try {
await wait(() => setup.sessions.length === 2)
expect(setup.sessions.toSorted()).toEqual(["first", "second"])
release()
await wait(() => setup.data.session.get("first") !== undefined && setup.data.session.get("second") !== undefined)
} finally {
release()
setup.destroy()
}
})
test("stores session tabs globally by default", async () => {
const setup = await renderSessionTabs("first")