Skip to content

Commit 4b64762

Browse files
adamdotdevinengineer
authored andcommitted
fix(app): more startup efficiency (anomalyco#18985)
1 parent b1dd119 commit 4b64762

4 files changed

Lines changed: 199 additions & 126 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
658658
const open = recent()
659659
const seen = new Set(open)
660660
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
661+
if (!query.trim()) return [...agents, ...pinned]
661662
const paths = await files.searchFilesAndDirectories(query)
662663
const fileOptions: AtOption[] = paths
663664
.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
@@ -142,8 +142,11 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
142142

143143
createEffect(() => {
144144
if (typeof document === "undefined") return
145-
void loadFont().then((x) => x.ensureMonoFont(store.appearance?.font))
146-
document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font))
145+
const id = store.appearance?.font ?? defaultSettings.appearance.font
146+
if (id !== defaultSettings.appearance.font) {
147+
void loadFont().then((x) => x.ensureMonoFont(id))
148+
}
149+
document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(id))
147150
})
148151

149152
return {

packages/app/src/pages/session.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,8 +1181,6 @@ export default function Page() {
11811181
on(
11821182
() => sdk.directory,
11831183
() => {
1184-
void file.tree.list("")
1185-
11861184
const tab = activeFileTab()
11871185
if (!tab) return
11881186
const path = file.pathFromTab(tab)
@@ -1725,6 +1723,9 @@ export default function Page() {
17251723
sessionID: () => params.id,
17261724
messagesReady,
17271725
visibleUserMessages,
1726+
historyMore,
1727+
historyLoading,
1728+
loadMore: (sessionID) => sync.session.history.loadMore(sessionID),
17281729
turnStart: historyWindow.turnStart,
17291730
currentMessageId: () => store.messageId,
17301731
pendingMessage: () => ui.pendingMessage,
@@ -1796,7 +1797,7 @@ export default function Page() {
17961797
<div class="flex-1 min-h-0 overflow-hidden">
17971798
<Switch>
17981799
<Match when={params.id}>
1799-
<Show when={lastUserMessage()}>
1800+
<Show when={messagesReady()}>
18001801
<MessageTimeline
18011802
mobileChanges={mobileChanges()}
18021803
mobileFallback={reviewContent({

0 commit comments

Comments
 (0)