From ba35441841a9e28a15b33118876d56be1eb01814 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:18:08 +1000 Subject: [PATCH] fix(app): seed session status before warming session info --- .../src/context/global-sync/bootstrap.test.ts | 133 +++++++++++++----- .../app/src/context/global-sync/bootstrap.ts | 40 +++--- 2 files changed, 121 insertions(+), 52 deletions(-) diff --git a/packages/app/src/context/global-sync/bootstrap.test.ts b/packages/app/src/context/global-sync/bootstrap.test.ts index 73de5b9ce6db..40735fb822c0 100644 --- a/packages/app/src/context/global-sync/bootstrap.test.ts +++ b/packages/app/src/context/global-sync/bootstrap.test.ts @@ -1,50 +1,55 @@ import { describe, expect, test } from "bun:test" import { createStore } from "solid-js/store" import { QueryClient } from "@tanstack/solid-query" -import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client" +import type { Config, OpencodeClient, Project, Session } from "@opencode-ai/sdk/v2/client" import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import { bootstrapDirectory, loadPathQuery, loadProvidersQuery } from "./bootstrap" import type { State, VcsCache } from "./types" +import { createServerSession } from "../server-session" import { ServerScope } from "@/utils/server-scope" const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse +function directoryState() { + return createStore({ + status: "loading", + agent: [], + command: [], + reference: [], + project: "", + projectMeta: undefined, + icon: undefined, + provider_ready: true, + provider, + config: {}, + path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" }, + session: [], + sessionTotal: 0, + session_status: {}, + session_working(id: string) { + return this.session_status[id]?.type !== "idle" + }, + session_diff: {}, + todo: {}, + permission: {}, + question: {}, + mcp_ready: true, + mcp: {}, + mcp_resource: {}, + lsp_ready: true, + lsp: [], + vcs: undefined, + limit: 5, + message: {}, + part: {}, + part_text_accum_delta: {}, + }) +} + describe("bootstrapDirectory", () => { test("marks a loading directory partial during bootstrap and complete after success", async () => { const mcpReads: string[] = [] - const [store, setStore] = createStore({ - status: "loading", - agent: [], - command: [], - reference: [], - project: "", - projectMeta: undefined, - icon: undefined, - provider_ready: true, - provider, - config: {}, - path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" }, - session: [], - sessionTotal: 0, - session_status: {}, - session_working(id: string) { - return this.session_status[id]?.type !== "idle" - }, - session_diff: {}, - todo: {}, - permission: {}, - question: {}, - mcp_ready: true, - mcp: {}, - mcp_resource: {}, - lsp_ready: true, - lsp: [], - vcs: undefined, - limit: 5, - message: {}, - part: {}, - part_text_accum_delta: {}, - }) + const [store, setStore] = directoryState() await bootstrapDirectory({ directory: "/project", @@ -93,6 +98,66 @@ describe("bootstrapDirectory", () => { expect(store.status).toBe("complete") expect(mcpReads).toEqual([]) }) + + test("seeds session status even while warming session info stalls", async () => { + const [store, setStore] = directoryState() + const stalled = Promise.withResolvers() + const client = { + app: { agents: async () => ({ data: [{ name: "build", mode: "primary" }] }) }, + config: { get: async () => ({ data: {} }) }, + session: { + status: async () => ({ data: { ses_busy: { type: "busy" } } }), + get: () => stalled.promise, + }, + vcs: { get: async () => ({ data: undefined }) }, + command: { list: async () => ({ data: [] }) }, + permission: { list: async () => ({ data: [] }) }, + question: { list: async () => ({ data: [] }) }, + v2: { reference: { list: async () => ({ data: { data: [] } }) } }, + mcp: { status: async () => ({ data: {} }) }, + provider: { list: async () => ({ data: { all: [], connected: [], default: {} } }) }, + } as unknown as OpencodeClient + const session = createServerSession(client) + const stale: Session = { + id: "ses_stale", + slug: "ses_stale", + projectID: "project", + directory: "/project", + title: "stale", + version: "1", + time: { created: 1, updated: 1 }, + } + session.remember(stale) + session.set("session_status", stale.id, { type: "busy" }) + + await bootstrapDirectory({ + directory: "/project", + scope: ServerScope.local, + mcp: false, + global: { + config: {} satisfies Config, + path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" }, + project: [{ id: "project", worktree: "/project" } as Project], + provider, + }, + sdk: client, + store, + setStore, + vcsCache: { setStore() {} } as unknown as VcsCache, + loadSessions() {}, + translate: (key) => key, + queryClient: new QueryClient(), + session, + }) + + const deadline = Date.now() + 500 + while (!session.data.session_working("ses_busy") && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 10)) + } + + expect(session.data.session_status["ses_busy"]?.type).toBe("busy") + expect(session.data.session_status[stale.id]).toBeUndefined() + }) }) describe("query keys", () => { diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 8f253a975a25..c63b702522f5 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -247,25 +247,29 @@ export async function bootstrapDirectory(input: { () => retry(() => input.sdk.session.status().then(async (x) => { - if (input.session) { - const statuses = x.data ?? {} - await Promise.all( - Object.keys(statuses).map((sessionID) => input.session!.resolve(sessionID).catch(() => undefined)), - ) - input.session.set( - "session_status", - produce((draft) => { - for (const sessionID of Object.keys(draft)) { - if (statuses[sessionID]) continue - if (input.session?.get(sessionID)?.directory === input.directory) delete draft[sessionID] - } - }), - ) - for (const [sessionID, status] of Object.entries(statuses)) { - input.session.set("session_status", sessionID, reconcile(status)) - } + if (!input.session) { + input.setStore("session_status", x.data!) + return } - if (!input.session) input.setStore("session_status", x.data!) + const statuses = x.data ?? {} + input.session.set( + "session_status", + produce((draft) => { + for (const sessionID of Object.keys(draft)) { + if (statuses[sessionID]) continue + if (input.session?.get(sessionID)?.directory === input.directory) delete draft[sessionID] + } + }), + ) + for (const [sessionID, status] of Object.entries(statuses)) { + input.session.set("session_status", sessionID, reconcile(status)) + } + // Warm session info only after seeding statuses so a stalled session + // fetch cannot park busy indicators behind it, mirroring how live + // session.status events apply first and resolve info in the background. + await Promise.all( + Object.keys(statuses).map((sessionID) => input.session!.resolve(sessionID).catch(() => undefined)), + ) }), ), !seededProject &&