diff options
| author | Adam <[email protected]> | 2026-03-26 13:41:22 -0500 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-26 13:41:22 -0500 |
| commit | c7760b433b1bdbcaed7e7cd55d53b5b331f0f0fa (patch) | |
| tree | 4d1a865b8890dc30767b66293923c15e2b3f6d24 /packages/app/src/context/global-sync | |
| parent | 2e6ac8ff49eabcb1b62c1bd504338e7449f80c6e (diff) | |
| download | opencode-c7760b433b1bdbcaed7e7cd55d53b5b331f0f0fa.tar.gz opencode-c7760b433b1bdbcaed7e7cd55d53b5b331f0f0fa.zip | |
fix(app): more startup perf (#19288)
Diffstat (limited to 'packages/app/src/context/global-sync')
| -rw-r--r-- | packages/app/src/context/global-sync/bootstrap.ts | 165 | ||||
| -rw-r--r-- | packages/app/src/context/global-sync/child-store.ts | 3 | ||||
| -rw-r--r-- | packages/app/src/context/global-sync/types.ts | 3 |
3 files changed, 127 insertions, 44 deletions
diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 6eec688b7..869f8b7ea 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -7,6 +7,7 @@ import type { ProviderAuthResponse, ProviderListResponse, QuestionRequest, + Session, Todo, } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" @@ -52,6 +53,12 @@ function errors(list: PromiseSettledResult<unknown>[]) { return list.filter((item): item is PromiseRejectedResult => item.status === "rejected").map((item) => item.reason) } +const providerRev = new Map<string, number>() + +export function clearProviderRev(directory: string) { + providerRev.delete(directory) +} + function runAll(list: Array<() => Promise<unknown>>) { return Promise.allSettled(list.map((item) => item())) } @@ -144,6 +151,40 @@ function projectID(directory: string, projects: Project[]) { return projects.find((project) => project.worktree === directory || project.sandboxes?.includes(directory))?.id } +function mergeSession(setStore: SetStoreFunction<State>, session: Session) { + setStore("session", (list) => { + const next = list.slice() + const idx = next.findIndex((item) => item.id >= session.id) + if (idx === -1) return [...next, session] + if (next[idx]?.id === session.id) { + next[idx] = session + return next + } + next.splice(idx, 0, session) + return next + }) +} + +function warmSessions(input: { + ids: string[] + store: Store<State> + setStore: SetStoreFunction<State> + sdk: OpencodeClient +}) { + const known = new Set(input.store.session.map((item) => item.id)) + const ids = [...new Set(input.ids)].filter((id) => !!id && !known.has(id)) + if (ids.length === 0) return Promise.resolve() + return Promise.all( + ids.map((sessionID) => + retry(() => input.sdk.session.get({ sessionID })).then((x) => { + const session = x.data + if (!session?.id) return + mergeSession(input.setStore, session) + }), + ), + ).then(() => undefined) +} + export async function bootstrapDirectory(input: { directory: string sdk: OpencodeClient @@ -154,19 +195,29 @@ export async function bootstrapDirectory(input: { translate: (key: string, vars?: Record<string, string | number>) => string global: { config: Config + path: Path project: Project[] provider: ProviderListResponse } }) { const loading = input.store.status !== "complete" const seededProject = projectID(input.directory, input.global.project) + const seededPath = input.global.path.directory === input.directory ? input.global.path : undefined if (seededProject) input.setStore("project", seededProject) + if (seededPath) input.setStore("path", seededPath) if (input.store.provider.all.length === 0 && input.global.provider.all.length > 0) { input.setStore("provider", input.global.provider) } if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) { input.setStore("config", input.global.config) } + if (loading || input.store.provider.all.length === 0) { + input.setStore("provider_ready", false) + } + input.setStore("mcp_ready", false) + input.setStore("mcp", {}) + input.setStore("lsp_ready", false) + input.setStore("lsp", []) if (loading) input.setStore("status", "partial") const fast = [ @@ -177,13 +228,15 @@ export async function bootstrapDirectory(input: { () => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))), () => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))), () => - retry(() => - input.sdk.path.get().then((x) => { - input.setStore("path", x.data!) - const next = projectID(x.data?.directory ?? input.directory, input.global.project) - if (next) input.setStore("project", next) - }), - ), + seededPath + ? Promise.resolve() + : retry(() => + input.sdk.path.get().then((x) => { + input.setStore("path", x.data!) + const next = projectID(x.data?.directory ?? input.directory, input.global.project) + if (next) input.setStore("project", next) + }), + ), () => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))), () => retry(() => @@ -197,61 +250,66 @@ export async function bootstrapDirectory(input: { () => retry(() => input.sdk.permission.list().then((x) => { + const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id) const grouped = groupBySession( (x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID), ) - batch(() => { - for (const sessionID of Object.keys(input.store.permission)) { - if (grouped[sessionID]) continue - input.setStore("permission", sessionID, []) - } - for (const [sessionID, permissions] of Object.entries(grouped)) { - input.setStore( - "permission", - sessionID, - reconcile( - permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)), - { key: "id" }, - ), - ) - } - }) + return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() => + batch(() => { + for (const sessionID of Object.keys(input.store.permission)) { + if (grouped[sessionID]) continue + input.setStore("permission", sessionID, []) + } + for (const [sessionID, permissions] of Object.entries(grouped)) { + input.setStore( + "permission", + sessionID, + reconcile( + permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)), + { key: "id" }, + ), + ) + } + }), + ) }), ), () => retry(() => input.sdk.question.list().then((x) => { + const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id) const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID)) - batch(() => { - for (const sessionID of Object.keys(input.store.question)) { - if (grouped[sessionID]) continue - input.setStore("question", sessionID, []) - } - for (const [sessionID, questions] of Object.entries(grouped)) { - input.setStore( - "question", - sessionID, - reconcile( - questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)), - { key: "id" }, - ), - ) - } - }) + return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() => + batch(() => { + for (const sessionID of Object.keys(input.store.question)) { + if (grouped[sessionID]) continue + input.setStore("question", sessionID, []) + } + for (const [sessionID, questions] of Object.entries(grouped)) { + input.setStore( + "question", + sessionID, + reconcile( + questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)), + { key: "id" }, + ), + ) + } + }), + ) }), ), ] const slow = [ + () => Promise.resolve(input.loadSessions(input.directory)), () => retry(() => - input.sdk.provider.list().then((x) => { - input.setStore("provider", normalizeProviderList(x.data!)) + input.sdk.mcp.status().then((x) => { + input.setStore("mcp", x.data!) + input.setStore("mcp_ready", true) }), ), - () => Promise.resolve(input.loadSessions(input.directory)), - () => retry(() => input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!))), - () => retry(() => input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!))), ] const errs = errors(await runAll(fast)) @@ -278,4 +336,23 @@ export async function bootstrapDirectory(input: { } if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete") + + const rev = (providerRev.get(input.directory) ?? 0) + 1 + providerRev.set(input.directory, rev) + void retry(() => input.sdk.provider.list()) + .then((x) => { + if (providerRev.get(input.directory) !== rev) return + input.setStore("provider", normalizeProviderList(x.data!)) + input.setStore("provider_ready", true) + }) + .catch((err) => { + if (providerRev.get(input.directory) !== rev) return + console.error("Failed to refresh provider list", err) + const project = getFilename(input.directory) + showToast({ + variant: "error", + title: input.translate("toast.project.reloadFailed.title", { project }), + description: formatServerError(err, input.translate), + }) + }) } diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 70668350e..5678491f8 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -160,6 +160,7 @@ export function createChildStoreManager(input: { project: "", projectMeta: initialMeta, icon: initialIcon, + provider_ready: false, provider: { all: [], connected: [], default: {} }, config: {}, path: { state: "", config: "", worktree: "", directory: "", home: "" }, @@ -173,7 +174,9 @@ export function createChildStoreManager(input: { todo: {}, permission: {}, question: {}, + mcp_ready: false, mcp: {}, + lsp_ready: false, lsp: [], vcs: vcsStore.value, limit: 5, diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts index c61dc337d..1d6e550f8 100644 --- a/packages/app/src/context/global-sync/types.ts +++ b/packages/app/src/context/global-sync/types.ts @@ -38,6 +38,7 @@ export type State = { project: string projectMeta: ProjectMeta | undefined icon: string | undefined + provider_ready: boolean provider: ProviderListResponse config: Config path: Path @@ -58,9 +59,11 @@ export type State = { question: { [sessionID: string]: QuestionRequest[] } + mcp_ready: boolean mcp: { [name: string]: McpStatus } + lsp_ready: boolean lsp: LspStatus[] vcs: VcsInfo | undefined limit: number |
