Skip to content

Commit 687b758

Browse files
authored
app: better loading (#23489)
1 parent 84e322b commit 687b758

File tree

9 files changed

+142
-107
lines changed

9 files changed

+142
-107
lines changed

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

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useFilteredList } from "@opencode-ai/ui/hooks"
22
import { useSpring } from "@opencode-ai/ui/motion-spring"
3-
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js"
3+
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal, createResource } from "solid-js"
44
import { createStore } from "solid-js/store"
55
import { useLocal } from "@/context/local"
66
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
@@ -54,7 +54,7 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments"
5454
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
5555
import { promptPlaceholder } from "./prompt-input/placeholder"
5656
import { ImagePreview } from "@opencode-ai/ui/image-preview"
57-
import { useQuery } from "@tanstack/solid-query"
57+
import { useQueries, useQuery } from "@tanstack/solid-query"
5858
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
5959

6060
interface PromptInputProps {
@@ -1252,16 +1252,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
12521252
}
12531253
}
12541254

1255-
const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory))
1256-
const agentsLoading = () => agentsQuery.isLoading
1257-
1258-
const globalProvidersQuery = useQuery(() => loadProvidersQuery(null))
1259-
const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory))
1255+
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
1256+
queries: [loadAgentsQuery(sdk.directory), loadProvidersQuery(null), loadProvidersQuery(sdk.directory)],
1257+
}))
12601258

1259+
const agentsLoading = () => agentsQuery.isLoading
12611260
const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading
12621261

1262+
const [promptReady] = createResource(
1263+
() => prompt.ready().promise,
1264+
(p) => p,
1265+
)
1266+
12631267
return (
12641268
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
1269+
{(promptReady(), null)}
12651270
<PromptPopover
12661271
popover={store.popover}
12671272
setSlashPopoverRef={(el) => (slashPopoverRef = el)}
@@ -1358,15 +1363,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
13581363
}}
13591364
style={{ "padding-bottom": space }}
13601365
/>
1361-
<Show when={!prompt.dirty()}>
1362-
<div
1363-
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
1364-
classList={{ "font-mono!": store.mode === "shell" }}
1365-
style={{ "padding-bottom": space }}
1366-
>
1367-
{placeholder()}
1368-
</div>
1369-
</Show>
1366+
<div
1367+
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
1368+
classList={{ "font-mono!": store.mode === "shell" }}
1369+
style={{ "padding-bottom": space, display: prompt.dirty() ? "none" : undefined }}
1370+
>
1371+
{placeholder()}
1372+
</div>
13701373
</div>
13711374

13721375
<div
@@ -1457,7 +1460,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
14571460
</div>
14581461
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
14591462
<Show when={!agentsLoading()}>
1460-
<div data-component="prompt-agent-control">
1463+
<div data-component="prompt-agent-control" style={{ animation: "fade-in 0.3s" }}>
14611464
<TooltipKeybind
14621465
placement="top"
14631466
gutter={4}
@@ -1483,7 +1486,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
14831486
</Show>
14841487
<Show when={!providersLoading()}>
14851488
<Show when={store.mode !== "shell"}>
1486-
<div data-component="prompt-model-control">
1489+
<div data-component="prompt-model-control" style={{ animation: "fade-in 0.3s" }}>
14871490
<Show
14881491
when={providers.paid().length > 0}
14891492
fallback={
@@ -1554,7 +1557,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
15541557
</TooltipKeybind>
15551558
</Show>
15561559
</div>
1557-
<div data-component="prompt-variant-control">
1560+
<div data-component="prompt-variant-control" style={{ animation: "fade-in 0.3s" }}>
15581561
<TooltipKeybind
15591562
placement="top"
15601563
gutter={4}

packages/app/src/context/global-sync.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
} from "@opencode-ai/sdk/v2/client"
1010
import { showToast } from "@opencode-ai/ui/toast"
1111
import { getFilename } from "@opencode-ai/shared/util/path"
12-
import { createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
12+
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
1313
import { createStore, produce, reconcile } from "solid-js/store"
1414
import { useLanguage } from "@/context/language"
1515
import { Persist, persisted } from "@/utils/persist"
@@ -223,16 +223,18 @@ function createGlobalSync() {
223223
limit,
224224
permission: store.permission,
225225
})
226-
setStore(
227-
"sessionTotal",
228-
estimateRootSessionTotal({
229-
count: nonArchived.length,
230-
limit: x.limit,
231-
limited: x.limited,
232-
}),
233-
)
234-
setStore("session", reconcile(sessions, { key: "id" }))
235-
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
226+
batch(() => {
227+
setStore(
228+
"sessionTotal",
229+
estimateRootSessionTotal({
230+
count: nonArchived.length,
231+
limit: x.limit,
232+
limited: x.limited,
233+
}),
234+
)
235+
setStore("session", reconcile(sessions, { key: "id" }))
236+
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
237+
})
236238
sessionMeta.set(directory, { limit })
237239
})
238240
.catch((err) => {

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

Lines changed: 79 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import type { State, VcsCache } from "./types"
1919
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
2020
import { formatServerError } from "@/utils/server-errors"
2121
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
22-
import { loadSessionsQuery } from "../global-sync"
2322

2423
type GlobalStore = {
2524
ready: boolean
@@ -82,6 +81,9 @@ export async function bootstrapGlobal(input: {
8281
input.setGlobalStore("config", x.data!)
8382
}),
8483
),
84+
]
85+
86+
const slow = [
8587
() =>
8688
input.queryClient.fetchQuery({
8789
...loadProvidersQuery(null),
@@ -93,9 +95,6 @@ export async function bootstrapGlobal(input: {
9395
}),
9496
),
9597
}),
96-
]
97-
98-
const slow = [
9998
() =>
10099
retry(() =>
101100
input.globalSDK.path.get().then((x) => {
@@ -183,8 +182,43 @@ function warmSessions(input: {
183182
export const loadProvidersQuery = (directory: string | null) =>
184183
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
185184

186-
export const loadAgentsQuery = (directory: string | null) =>
187-
queryOptions<null>({ queryKey: [directory, "agents"], queryFn: skipToken })
185+
export const loadAgentsQuery = (
186+
directory: string | null,
187+
sdk?: OpencodeClient,
188+
transform?: (x: Awaited<ReturnType<OpencodeClient["app"]["agents"]>>) => void,
189+
) =>
190+
queryOptions<null>({
191+
queryKey: [directory, "agents"],
192+
queryFn:
193+
sdk && transform
194+
? () =>
195+
retry(() =>
196+
sdk.app
197+
.agents()
198+
.then(transform)
199+
.then(() => null),
200+
)
201+
: skipToken,
202+
})
203+
204+
export const loadPathQuery = (
205+
directory: string | null,
206+
sdk?: OpencodeClient,
207+
transform?: (x: Awaited<ReturnType<OpencodeClient["path"]["get"]>>) => void,
208+
) =>
209+
queryOptions<Path>({
210+
queryKey: [directory, "path"],
211+
queryFn:
212+
sdk && transform
213+
? () =>
214+
retry(() =>
215+
sdk.path.get().then(async (x) => {
216+
transform(x)
217+
return x.data!
218+
}),
219+
)
220+
: skipToken,
221+
})
188222

189223
export async function bootstrapDirectory(input: {
190224
directory: string
@@ -222,45 +256,27 @@ export async function bootstrapDirectory(input: {
222256
input.setStore("lsp", [])
223257
if (loading) input.setStore("status", "partial")
224258

225-
const fast = [() => Promise.resolve(input.loadSessions(input.directory))]
226-
227-
const errs = errors(await runAll(fast))
228-
if (errs.length > 0) {
229-
console.error("Failed to bootstrap instance", errs[0])
230-
const project = getFilename(input.directory)
231-
showToast({
232-
variant: "error",
233-
title: input.translate("toast.project.reloadFailed.title", { project }),
234-
description: formatServerError(errs[0], input.translate),
235-
})
236-
}
237-
259+
const rev = (providerRev.get(input.directory) ?? 0) + 1
260+
providerRev.set(input.directory, rev)
238261
;(async () => {
239262
const slow = [
263+
() => Promise.resolve(input.loadSessions(input.directory)),
240264
() =>
241-
input.queryClient.ensureQueryData({
242-
...loadAgentsQuery(input.directory),
243-
queryFn: () =>
244-
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then(
245-
() => null,
246-
),
247-
}),
265+
input.queryClient.ensureQueryData(
266+
loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))),
267+
),
248268
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
249269
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
250-
() =>
251-
seededProject
252-
? Promise.resolve()
253-
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
254-
() =>
255-
seededPath
256-
? Promise.resolve()
257-
: retry(() =>
258-
input.sdk.path.get().then((x) => {
259-
input.setStore("path", x.data!)
260-
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
261-
if (next) input.setStore("project", next)
262-
}),
263-
),
270+
!seededProject &&
271+
(() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))),
272+
!seededPath &&
273+
(() =>
274+
input.queryClient.ensureQueryData(
275+
loadPathQuery(input.directory, input.sdk, (x) => {
276+
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
277+
if (next) input.setStore("project", next)
278+
}),
279+
)),
264280
() =>
265281
retry(() =>
266282
input.sdk.vcs.get().then((x) => {
@@ -330,7 +346,28 @@ export async function bootstrapDirectory(input: {
330346
input.setStore("mcp_ready", true)
331347
}),
332348
),
333-
]
349+
() =>
350+
input.queryClient.ensureQueryData({
351+
...loadProvidersQuery(input.directory),
352+
queryFn: () =>
353+
retry(() => input.sdk.provider.list())
354+
.then((x) => {
355+
if (providerRev.get(input.directory) !== rev) return
356+
input.setStore("provider", normalizeProviderList(x.data!))
357+
input.setStore("provider_ready", true)
358+
})
359+
.catch((err) => {
360+
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
361+
const project = getFilename(input.directory)
362+
showToast({
363+
variant: "error",
364+
title: input.translate("toast.project.reloadFailed.title", { project }),
365+
description: formatServerError(err, input.translate),
366+
})
367+
})
368+
.then(() => null),
369+
}),
370+
].filter(Boolean) as (() => Promise<any>)[]
334371

335372
await waitForPaint()
336373
const slowErrs = errors(await runAll(slow))
@@ -344,29 +381,6 @@ export async function bootstrapDirectory(input: {
344381
})
345382
}
346383

347-
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
348-
349-
const rev = (providerRev.get(input.directory) ?? 0) + 1
350-
providerRev.set(input.directory, rev)
351-
void input.queryClient.ensureQueryData({
352-
...loadSessionsQuery(input.directory),
353-
queryFn: () =>
354-
retry(() => input.sdk.provider.list())
355-
.then((x) => {
356-
if (providerRev.get(input.directory) !== rev) return
357-
input.setStore("provider", normalizeProviderList(x.data!))
358-
input.setStore("provider_ready", true)
359-
})
360-
.catch((err) => {
361-
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
362-
const project = getFilename(input.directory)
363-
showToast({
364-
variant: "error",
365-
title: input.translate("toast.project.reloadFailed.title", { project }),
366-
description: formatServerError(err, input.translate),
367-
})
368-
})
369-
.then(() => null),
370-
})
384+
if (loading && slowErrs.length === 0) input.setStore("status", "complete")
371385
})()
372386
}

packages/app/src/context/global-sync/child-store.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
type VcsCache,
1515
} from "./types"
1616
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
17+
import { useQuery } from "@tanstack/solid-query"
18+
import { loadPathQuery } from "./bootstrap"
1719

1820
export function createChildStoreManager(input: {
1921
owner: Owner
@@ -156,14 +158,20 @@ export function createChildStoreManager(input: {
156158
createRoot((dispose) => {
157159
const initialMeta = meta[0].value
158160
const initialIcon = icon[0].value
161+
162+
const pathQuery = useQuery(() => loadPathQuery(directory))
159163
const child = createStore<State>({
160164
project: "",
161165
projectMeta: initialMeta,
162166
icon: initialIcon,
163167
provider_ready: false,
164168
provider: { all: [], connected: [], default: {} },
165169
config: {},
166-
path: { state: "", config: "", worktree: "", directory: "", home: "" },
170+
get path() {
171+
if (pathQuery.isLoading || !pathQuery.data)
172+
return { state: "", config: "", worktree: "", directory: "", home: "" }
173+
return pathQuery.data
174+
},
167175
status: "loading" as const,
168176
agent: [],
169177
command: [],

packages/app/src/context/prompt.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,9 @@ function createPromptSession(dir: string, id: string | undefined) {
185185

186186
return {
187187
ready,
188-
current: createMemo(() => store.prompt),
188+
current: () => store.prompt,
189189
cursor: createMemo(() => store.cursor),
190-
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
190+
dirty: () => !isPromptEqual(store.prompt, DEFAULT_PROMPT),
191191
context: {
192192
items: createMemo(() => store.context.items),
193193
add(item: ContextItem) {
@@ -277,7 +277,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
277277
const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session())
278278

279279
return {
280-
ready: () => session().ready(),
280+
ready: () => session().ready,
281281
current: () => session().current(),
282282
cursor: () => session().cursor(),
283283
dirty: () => session().dirty(),

packages/app/src/index.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,13 @@
7373
width: auto;
7474
}
7575
}
76+
77+
@keyframes fade-in {
78+
from {
79+
opacity: 0;
80+
}
81+
to {
82+
opacity: 1;
83+
}
84+
}
7685
}

0 commit comments

Comments
 (0)