From 9e15b2815c4ce960b3712b865204d6b9c04b09fd Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 17 Apr 2026 17:04:38 +0800 Subject: [PATCH 1/2] app: move more to tanstack query + suspend on more promises --- packages/app/src/components/prompt-input.tsx | 24 +++-- packages/app/src/context/global-sync.tsx | 24 +++-- .../app/src/context/global-sync/bootstrap.ts | 101 ++++++++++-------- .../src/context/global-sync/child-store.ts | 10 +- packages/app/src/context/prompt.tsx | 6 +- packages/app/src/pages/layout/helpers.ts | 2 +- .../src/pages/layout/sidebar-workspace.tsx | 2 +- 7 files changed, 98 insertions(+), 71 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 156b0b3a4a22..02e5828cca15 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1,6 +1,6 @@ import { useFilteredList } from "@opencode-ai/ui/hooks" import { useSpring } from "@opencode-ai/ui/motion-spring" -import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js" +import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal, createResource } from "solid-js" import { createStore } from "solid-js/store" import { useLocal } from "@/context/local" import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file" @@ -1260,8 +1260,14 @@ export const PromptInput: Component = (props) => { const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading + const [promptReady] = createResource( + () => prompt.ready().promise, + (p) => p, + ) + return (
+ {(promptReady(), null)} (slashPopoverRef = el)} @@ -1358,15 +1364,13 @@ export const PromptInput: Component = (props) => { }} style={{ "padding-bottom": space }} /> - -
- {placeholder()} -
-
+
+ {placeholder()} +
{ + setStore( + "sessionTotal", + estimateRootSessionTotal({ + count: nonArchived.length, + limit: x.limit, + limited: x.limited, + }), + ) + setStore("session", reconcile(sessions, { key: "id" })) + cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo) + }) sessionMeta.set(directory, { limit }) }) .catch((err) => { diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 17fe726f9093..1ac3adce4aeb 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -19,7 +19,6 @@ import type { State, VcsCache } from "./types" import { cmp, normalizeAgentList, normalizeProviderList } from "./utils" import { formatServerError } from "@/utils/server-errors" import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query" -import { loadSessionsQuery } from "../global-sync" type GlobalStore = { ready: boolean @@ -82,6 +81,9 @@ export async function bootstrapGlobal(input: { input.setGlobalStore("config", x.data!) }), ), + ] + + const slow = [ () => input.queryClient.fetchQuery({ ...loadProvidersQuery(null), @@ -93,9 +95,6 @@ export async function bootstrapGlobal(input: { }), ), }), - ] - - const slow = [ () => retry(() => input.globalSDK.path.get().then((x) => { @@ -183,8 +182,43 @@ function warmSessions(input: { export const loadProvidersQuery = (directory: string | null) => queryOptions({ queryKey: [directory, "providers"], queryFn: skipToken }) -export const loadAgentsQuery = (directory: string | null) => - queryOptions({ queryKey: [directory, "agents"], queryFn: skipToken }) +export const loadAgentsQuery = ( + directory: string | null, + sdk?: OpencodeClient, + transform?: (x: Awaited>) => void, +) => + queryOptions({ + queryKey: [directory, "agents"], + queryFn: + sdk && transform + ? () => + retry(() => + sdk.app + .agents() + .then(transform) + .then(() => null), + ) + : skipToken, + }) + +export const loadPathQuery = ( + directory: string | null, + sdk?: OpencodeClient, + transform?: (x: Awaited>) => void, +) => + queryOptions({ + queryKey: [directory, "path"], + queryFn: + sdk && transform + ? () => + retry(() => + sdk.path.get().then(async (x) => { + transform(x) + return x.data! + }), + ) + : skipToken, + }) export async function bootstrapDirectory(input: { directory: string @@ -221,46 +255,25 @@ export async function bootstrapDirectory(input: { input.setStore("lsp_ready", false) input.setStore("lsp", []) if (loading) input.setStore("status", "partial") - - const fast = [() => Promise.resolve(input.loadSessions(input.directory))] - - const errs = errors(await runAll(fast)) - if (errs.length > 0) { - console.error("Failed to bootstrap instance", errs[0]) - const project = getFilename(input.directory) - showToast({ - variant: "error", - title: input.translate("toast.project.reloadFailed.title", { project }), - description: formatServerError(errs[0], input.translate), - }) - } - ;(async () => { const slow = [ + () => Promise.resolve(input.loadSessions(input.directory)), () => - input.queryClient.ensureQueryData({ - ...loadAgentsQuery(input.directory), - queryFn: () => - retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then( - () => null, - ), - }), + input.queryClient.ensureQueryData( + loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))), + ), () => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))), () => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))), - () => - seededProject - ? Promise.resolve() - : retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)), - () => - 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) - }), - ), + !seededProject && + (() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))), + !seededPath && + (() => + input.queryClient.ensureQueryData( + loadPathQuery(input.directory, input.sdk, (x) => { + const next = projectID(x.data?.directory ?? input.directory, input.global.project) + if (next) input.setStore("project", next) + }), + )), () => retry(() => input.sdk.vcs.get().then((x) => { @@ -330,7 +343,7 @@ export async function bootstrapDirectory(input: { input.setStore("mcp_ready", true) }), ), - ] + ].filter(Boolean) as (() => Promise)[] await waitForPaint() const slowErrs = errors(await runAll(slow)) @@ -344,12 +357,12 @@ export async function bootstrapDirectory(input: { }) } - if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete") + if (loading && slowErrs.length === 0) input.setStore("status", "complete") const rev = (providerRev.get(input.directory) ?? 0) + 1 providerRev.set(input.directory, rev) void input.queryClient.ensureQueryData({ - ...loadSessionsQuery(input.directory), + ...loadProvidersQuery(input.directory), queryFn: () => retry(() => input.sdk.provider.list()) .then((x) => { diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 3fe67e4fbe65..c92d2ae57084 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -14,6 +14,8 @@ import { type VcsCache, } from "./types" import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction" +import { useQuery } from "@tanstack/solid-query" +import { loadPathQuery } from "./bootstrap" export function createChildStoreManager(input: { owner: Owner @@ -156,6 +158,8 @@ export function createChildStoreManager(input: { createRoot((dispose) => { const initialMeta = meta[0].value const initialIcon = icon[0].value + + const pathQuery = useQuery(() => loadPathQuery(directory)) const child = createStore({ project: "", projectMeta: initialMeta, @@ -163,7 +167,11 @@ export function createChildStoreManager(input: { provider_ready: false, provider: { all: [], connected: [], default: {} }, config: {}, - path: { state: "", config: "", worktree: "", directory: "", home: "" }, + get path() { + if (pathQuery.isLoading || !pathQuery.data) + return { state: "", config: "", worktree: "", directory: "", home: "" } + return pathQuery.data + }, status: "loading" as const, agent: [], command: [], diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx index 9b666e5e751a..15af57b355ee 100644 --- a/packages/app/src/context/prompt.tsx +++ b/packages/app/src/context/prompt.tsx @@ -185,9 +185,9 @@ function createPromptSession(dir: string, id: string | undefined) { return { ready, - current: createMemo(() => store.prompt), + current: () => store.prompt, cursor: createMemo(() => store.cursor), - dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)), + dirty: () => !isPromptEqual(store.prompt, DEFAULT_PROMPT), context: { items: createMemo(() => store.context.items), add(item: ContextItem) { @@ -277,7 +277,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext( const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session()) return { - ready: () => session().ready(), + ready: () => session().ready, current: () => session().current(), cursor: () => session().cursor(), dirty: () => session().dirty(), diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index 26b66d166823..32b94c9cb760 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -31,7 +31,7 @@ function sortSessions(now: number) { const isRootVisibleSession = (session: Session, directory: string) => workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived -const roots = (store: SessionStore) => +export const roots = (store: SessionStore) => (store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory)) export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now)) diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index 0202cfc3bece..cbb570106530 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -321,7 +321,7 @@ export const SortableWorkspace = (props: { const hasMore = createMemo(() => workspaceStore.sessionTotal > count()) const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) })) const busy = createMemo(() => props.ctx.isBusy(props.directory)) - const loading = () => query.isLoading + const loading = () => query.isLoading && count() === 0 const touch = createMediaQuery("(hover: none)") const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id))) const loadMore = async () => { From 888123209c00b476da83665623bccfcddd89e18f Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 17 Apr 2026 18:02:10 +0800 Subject: [PATCH 2/2] faster preload --- packages/app/src/components/prompt-input.tsx | 17 ++++--- .../app/src/context/global-sync/bootstrap.ts | 47 ++++++++++--------- packages/app/src/index.css | 9 ++++ packages/app/src/pages/directory-layout.tsx | 11 ++--- 4 files changed, 46 insertions(+), 38 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 02e5828cca15..1131baa498ee 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -54,7 +54,7 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments" import { PromptDragOverlay } from "./prompt-input/drag-overlay" import { promptPlaceholder } from "./prompt-input/placeholder" import { ImagePreview } from "@opencode-ai/ui/image-preview" -import { useQuery } from "@tanstack/solid-query" +import { useQueries, useQuery } from "@tanstack/solid-query" import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap" interface PromptInputProps { @@ -1252,12 +1252,11 @@ export const PromptInput: Component = (props) => { } } - const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory)) - const agentsLoading = () => agentsQuery.isLoading - - const globalProvidersQuery = useQuery(() => loadProvidersQuery(null)) - const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory)) + const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({ + queries: [loadAgentsQuery(sdk.directory), loadProvidersQuery(null), loadProvidersQuery(sdk.directory)], + })) + const agentsLoading = () => agentsQuery.isLoading const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading const [promptReady] = createResource( @@ -1461,7 +1460,7 @@ export const PromptInput: Component = (props) => {
-
+
= (props) => { -
+
0} fallback={ @@ -1558,7 +1557,7 @@ export const PromptInput: Component = (props) => {
-
+
{ const slow = [ () => Promise.resolve(input.loadSessions(input.directory)), @@ -343,6 +346,27 @@ export async function bootstrapDirectory(input: { input.setStore("mcp_ready", true) }), ), + () => + input.queryClient.ensureQueryData({ + ...loadProvidersQuery(input.directory), + queryFn: () => + 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) 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), + }) + }) + .then(() => null), + }), ].filter(Boolean) as (() => Promise)[] await waitForPaint() @@ -358,28 +382,5 @@ export async function bootstrapDirectory(input: { } if (loading && slowErrs.length === 0) input.setStore("status", "complete") - - const rev = (providerRev.get(input.directory) ?? 0) + 1 - providerRev.set(input.directory, rev) - void input.queryClient.ensureQueryData({ - ...loadProvidersQuery(input.directory), - queryFn: () => - 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) 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), - }) - }) - .then(() => null), - }) })() } diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 629ac80a8698..a59b549a6e7b 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -66,4 +66,13 @@ width: auto; } } + + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } } diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index f604dd6c5c78..36514f56c63a 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -2,7 +2,7 @@ import { DataProvider } from "@opencode-ai/ui/context" import { showToast } from "@opencode-ai/ui/toast" import { base64Encode } from "@opencode-ai/shared/util/encode" import { useLocation, useNavigate, useParams } from "@solidjs/router" -import { createEffect, createMemo, type ParentProps, Show } from "solid-js" +import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js" import { useLanguage } from "@/context/language" import { LocalProvider } from "@/context/local" import { SDKProvider } from "@/context/sdk" @@ -23,11 +23,10 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true }) }) - createEffect(() => { - const id = params.id - if (!id) return - void sync.session.sync(id) - }) + createResource( + () => params.id, + (id) => sync.session.sync(id), + ) return (