fix(app): seed session status before warming session info (#34864)
This commit is contained in:
@@ -1,50 +1,55 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { QueryClient } from "@tanstack/solid-query"
|
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 type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||||
import { bootstrapDirectory, loadPathQuery, loadProvidersQuery } from "./bootstrap"
|
import { bootstrapDirectory, loadPathQuery, loadProvidersQuery } from "./bootstrap"
|
||||||
import type { State, VcsCache } from "./types"
|
import type { State, VcsCache } from "./types"
|
||||||
|
import { createServerSession } from "../server-session"
|
||||||
import { ServerScope } from "@/utils/server-scope"
|
import { ServerScope } from "@/utils/server-scope"
|
||||||
|
|
||||||
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
|
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
|
||||||
|
|
||||||
|
function directoryState() {
|
||||||
|
return createStore<State>({
|
||||||
|
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", () => {
|
describe("bootstrapDirectory", () => {
|
||||||
test("marks a loading directory partial during bootstrap and complete after success", async () => {
|
test("marks a loading directory partial during bootstrap and complete after success", async () => {
|
||||||
const mcpReads: string[] = []
|
const mcpReads: string[] = []
|
||||||
const [store, setStore] = createStore<State>({
|
const [store, setStore] = directoryState()
|
||||||
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: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
await bootstrapDirectory({
|
await bootstrapDirectory({
|
||||||
directory: "/project",
|
directory: "/project",
|
||||||
@@ -93,6 +98,66 @@ describe("bootstrapDirectory", () => {
|
|||||||
expect(store.status).toBe("complete")
|
expect(store.status).toBe("complete")
|
||||||
expect(mcpReads).toEqual([])
|
expect(mcpReads).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("seeds session status even while warming session info stalls", async () => {
|
||||||
|
const [store, setStore] = directoryState()
|
||||||
|
const stalled = Promise.withResolvers<never>()
|
||||||
|
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", () => {
|
describe("query keys", () => {
|
||||||
|
|||||||
@@ -247,25 +247,29 @@ export async function bootstrapDirectory(input: {
|
|||||||
() =>
|
() =>
|
||||||
retry(() =>
|
retry(() =>
|
||||||
input.sdk.session.status().then(async (x) => {
|
input.sdk.session.status().then(async (x) => {
|
||||||
if (input.session) {
|
if (!input.session) {
|
||||||
const statuses = x.data ?? {}
|
input.setStore("session_status", x.data!)
|
||||||
await Promise.all(
|
return
|
||||||
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!)
|
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 &&
|
!seededProject &&
|
||||||
|
|||||||
Reference in New Issue
Block a user