Skip to content

Commit 98b3340

Browse files
authored
fix(app): more startup efficiency (anomalyco#18985)
1 parent 5e684c6 commit 98b3340

File tree

8 files changed

+241
-129
lines changed

8 files changed

+241
-129
lines changed

packages/app/src/components/prompt-input.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
572572
const open = recent()
573573
const seen = new Set(open)
574574
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
575+
if (!query.trim()) return [...agents, ...pinned]
575576
const paths = await files.searchFilesAndDirectories(query)
576577
const fileOptions: AtOption[] = paths
577578
.filter((path) => !seen.has(path))

packages/app/src/context/global-sync/bootstrap.ts

Lines changed: 189 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -31,52 +31,102 @@ type GlobalStore = {
3131
reload: undefined | "pending" | "complete"
3232
}
3333

34+
function waitForPaint() {
35+
return new Promise<void>((resolve) => {
36+
let done = false
37+
const finish = () => {
38+
if (done) return
39+
done = true
40+
resolve()
41+
}
42+
const timer = setTimeout(finish, 50)
43+
if (typeof requestAnimationFrame !== "function") return
44+
requestAnimationFrame(() => {
45+
clearTimeout(timer)
46+
finish()
47+
})
48+
})
49+
}
50+
51+
function errors(list: PromiseSettledResult<unknown>[]) {
52+
return list.filter((item): item is PromiseRejectedResult => item.status === "rejected").map((item) => item.reason)
53+
}
54+
55+
function runAll(list: Array<() => Promise<unknown>>) {
56+
return Promise.allSettled(list.map((item) => item()))
57+
}
58+
59+
function showErrors(input: {
60+
errors: unknown[]
61+
title: string
62+
translate: (key: string, vars?: Record<string, string | number>) => string
63+
formatMoreCount: (count: number) => string
64+
}) {
65+
if (input.errors.length === 0) return
66+
const message = formatServerError(input.errors[0], input.translate)
67+
const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : ""
68+
showToast({
69+
variant: "error",
70+
title: input.title,
71+
description: message + more,
72+
})
73+
}
74+
3475
export async function bootstrapGlobal(input: {
3576
globalSDK: OpencodeClient
3677
requestFailedTitle: string
3778
translate: (key: string, vars?: Record<string, string | number>) => string
3879
formatMoreCount: (count: number) => string
3980
setGlobalStore: SetStoreFunction<GlobalStore>
4081
}) {
41-
const tasks = [
42-
retry(() =>
43-
input.globalSDK.path.get().then((x) => {
44-
input.setGlobalStore("path", x.data!)
45-
}),
46-
),
47-
retry(() =>
48-
input.globalSDK.global.config.get().then((x) => {
49-
input.setGlobalStore("config", x.data!)
50-
}),
51-
),
52-
retry(() =>
53-
input.globalSDK.project.list().then((x) => {
54-
const projects = (x.data ?? [])
55-
.filter((p) => !!p?.id)
56-
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
57-
.slice()
58-
.sort((a, b) => cmp(a.id, b.id))
59-
input.setGlobalStore("project", projects)
60-
}),
61-
),
62-
retry(() =>
63-
input.globalSDK.provider.list().then((x) => {
64-
input.setGlobalStore("provider", normalizeProviderList(x.data!))
65-
}),
66-
),
82+
const fast = [
83+
() =>
84+
retry(() =>
85+
input.globalSDK.path.get().then((x) => {
86+
input.setGlobalStore("path", x.data!)
87+
}),
88+
),
89+
() =>
90+
retry(() =>
91+
input.globalSDK.global.config.get().then((x) => {
92+
input.setGlobalStore("config", x.data!)
93+
}),
94+
),
95+
() =>
96+
retry(() =>
97+
input.globalSDK.provider.list().then((x) => {
98+
input.setGlobalStore("provider", normalizeProviderList(x.data!))
99+
}),
100+
),
67101
]
68102

69-
const results = await Promise.allSettled(tasks)
70-
const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason)
71-
if (errors.length) {
72-
const message = formatServerError(errors[0], input.translate)
73-
const more = errors.length > 1 ? input.formatMoreCount(errors.length - 1) : ""
74-
showToast({
75-
variant: "error",
76-
title: input.requestFailedTitle,
77-
description: message + more,
78-
})
79-
}
103+
const slow = [
104+
() =>
105+
retry(() =>
106+
input.globalSDK.project.list().then((x) => {
107+
const projects = (x.data ?? [])
108+
.filter((p) => !!p?.id)
109+
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
110+
.slice()
111+
.sort((a, b) => cmp(a.id, b.id))
112+
input.setGlobalStore("project", projects)
113+
}),
114+
),
115+
]
116+
117+
showErrors({
118+
errors: errors(await runAll(fast)),
119+
title: input.requestFailedTitle,
120+
translate: input.translate,
121+
formatMoreCount: input.formatMoreCount,
122+
})
123+
await waitForPaint()
124+
showErrors({
125+
errors: errors(await runAll(slow)),
126+
title: input.requestFailedTitle,
127+
translate: input.translate,
128+
formatMoreCount: input.formatMoreCount,
129+
})
80130
input.setGlobalStore("ready", true)
81131
}
82132

@@ -119,95 +169,113 @@ export async function bootstrapDirectory(input: {
119169
}
120170
if (loading) input.setStore("status", "partial")
121171

122-
const results = await Promise.allSettled([
123-
seededProject
124-
? Promise.resolve()
125-
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
126-
retry(() =>
127-
input.sdk.provider.list().then((x) => {
128-
input.setStore("provider", normalizeProviderList(x.data!))
129-
}),
130-
),
131-
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? []))),
132-
retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
133-
retry(() =>
134-
input.sdk.path.get().then((x) => {
135-
input.setStore("path", x.data!)
136-
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
137-
if (next) input.setStore("project", next)
138-
}),
139-
),
140-
retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
141-
retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
142-
input.loadSessions(input.directory),
143-
retry(() => input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!))),
144-
retry(() => input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!))),
145-
retry(() =>
146-
input.sdk.vcs.get().then((x) => {
147-
const next = x.data ?? input.store.vcs
148-
input.setStore("vcs", next)
149-
if (next?.branch) input.vcsCache.setStore("value", next)
150-
}),
151-
),
152-
retry(() =>
153-
input.sdk.permission.list().then((x) => {
154-
const grouped = groupBySession(
155-
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
156-
)
157-
batch(() => {
158-
for (const sessionID of Object.keys(input.store.permission)) {
159-
if (grouped[sessionID]) continue
160-
input.setStore("permission", sessionID, [])
161-
}
162-
for (const [sessionID, permissions] of Object.entries(grouped)) {
163-
input.setStore(
164-
"permission",
165-
sessionID,
166-
reconcile(
167-
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
168-
{ key: "id" },
169-
),
170-
)
171-
}
172-
})
173-
}),
174-
),
175-
retry(() =>
176-
input.sdk.question.list().then((x) => {
177-
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
178-
batch(() => {
179-
for (const sessionID of Object.keys(input.store.question)) {
180-
if (grouped[sessionID]) continue
181-
input.setStore("question", sessionID, [])
182-
}
183-
for (const [sessionID, questions] of Object.entries(grouped)) {
184-
input.setStore(
185-
"question",
186-
sessionID,
187-
reconcile(
188-
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
189-
{ key: "id" },
190-
),
191-
)
192-
}
193-
})
194-
}),
195-
),
196-
])
197-
198-
const errors = results
199-
.filter((item): item is PromiseRejectedResult => item.status === "rejected")
200-
.map((item) => item.reason)
201-
if (errors.length > 0) {
202-
console.error("Failed to bootstrap instance", errors[0])
172+
const fast = [
173+
() =>
174+
seededProject
175+
? Promise.resolve()
176+
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
177+
() => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? []))),
178+
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
179+
() =>
180+
retry(() =>
181+
input.sdk.path.get().then((x) => {
182+
input.setStore("path", x.data!)
183+
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
184+
if (next) input.setStore("project", next)
185+
}),
186+
),
187+
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
188+
() =>
189+
retry(() =>
190+
input.sdk.vcs.get().then((x) => {
191+
const next = x.data ?? input.store.vcs
192+
input.setStore("vcs", next)
193+
if (next?.branch) input.vcsCache.setStore("value", next)
194+
}),
195+
),
196+
() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
197+
() =>
198+
retry(() =>
199+
input.sdk.permission.list().then((x) => {
200+
const grouped = groupBySession(
201+
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
202+
)
203+
batch(() => {
204+
for (const sessionID of Object.keys(input.store.permission)) {
205+
if (grouped[sessionID]) continue
206+
input.setStore("permission", sessionID, [])
207+
}
208+
for (const [sessionID, permissions] of Object.entries(grouped)) {
209+
input.setStore(
210+
"permission",
211+
sessionID,
212+
reconcile(
213+
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
214+
{ key: "id" },
215+
),
216+
)
217+
}
218+
})
219+
}),
220+
),
221+
() =>
222+
retry(() =>
223+
input.sdk.question.list().then((x) => {
224+
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
225+
batch(() => {
226+
for (const sessionID of Object.keys(input.store.question)) {
227+
if (grouped[sessionID]) continue
228+
input.setStore("question", sessionID, [])
229+
}
230+
for (const [sessionID, questions] of Object.entries(grouped)) {
231+
input.setStore(
232+
"question",
233+
sessionID,
234+
reconcile(
235+
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
236+
{ key: "id" },
237+
),
238+
)
239+
}
240+
})
241+
}),
242+
),
243+
]
244+
245+
const slow = [
246+
() =>
247+
retry(() =>
248+
input.sdk.provider.list().then((x) => {
249+
input.setStore("provider", normalizeProviderList(x.data!))
250+
}),
251+
),
252+
() => Promise.resolve(input.loadSessions(input.directory)),
253+
() => retry(() => input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!))),
254+
() => retry(() => input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!))),
255+
]
256+
257+
const errs = errors(await runAll(fast))
258+
if (errs.length > 0) {
259+
console.error("Failed to bootstrap instance", errs[0])
260+
const project = getFilename(input.directory)
261+
showToast({
262+
variant: "error",
263+
title: input.translate("toast.project.reloadFailed.title", { project }),
264+
description: formatServerError(errs[0], input.translate),
265+
})
266+
}
267+
268+
await waitForPaint()
269+
const slowErrs = errors(await runAll(slow))
270+
if (slowErrs.length > 0) {
271+
console.error("Failed to finish bootstrap instance", slowErrs[0])
203272
const project = getFilename(input.directory)
204273
showToast({
205274
variant: "error",
206275
title: input.translate("toast.project.reloadFailed.title", { project }),
207-
description: formatServerError(errors[0], input.translate),
276+
description: formatServerError(slowErrs[0], input.translate),
208277
})
209-
return
210278
}
211279

212-
if (loading) input.setStore("status", "complete")
280+
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
213281
}

packages/app/src/context/settings.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,11 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
118118

119119
createEffect(() => {
120120
if (typeof document === "undefined") return
121-
void loadFont().then((x) => x.ensureMonoFont(store.appearance?.font))
122-
document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font))
121+
const id = store.appearance?.font ?? defaultSettings.appearance.font
122+
if (id !== defaultSettings.appearance.font) {
123+
void loadFont().then((x) => x.ensureMonoFont(id))
124+
}
125+
document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(id))
123126
})
124127

125128
return {

packages/app/src/context/sync.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
180180
return globalSync.child(directory)
181181
}
182182
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
183-
const messagePageSize = 200
183+
const initialMessagePageSize = 80
184+
const historyMessagePageSize = 200
184185
const inflight = new Map<string, Promise<void>>()
185186
const inflightDiff = new Map<string, Promise<void>>()
186187
const inflightTodo = new Map<string, Promise<void>>()
@@ -463,7 +464,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
463464
const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined
464465
if (cached && hasSession && !opts?.force) return
465466

466-
const limit = meta.limit[key] ?? messagePageSize
467+
const limit = meta.limit[key] ?? initialMessagePageSize
467468
const sessionReq =
468469
hasSession && !opts?.force
469470
? Promise.resolve()
@@ -560,7 +561,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
560561
const [, setStore] = globalSync.child(directory)
561562
touch(directory, setStore, sessionID)
562563
const key = keyFor(directory, sessionID)
563-
const step = count ?? messagePageSize
564+
const step = count ?? historyMessagePageSize
564565
if (meta.loading[key]) return
565566
if (meta.complete[key]) return
566567
const before = meta.cursor[key]

0 commit comments

Comments
 (0)