From f5c3d352a1532ef55bce09cacc054f79388dcd68 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Wed, 6 May 2026 10:09:32 +1000 Subject: [PATCH] fix(app): require query functions for sync queries (#25939) --- .../app/src/components/dialog-select-mcp.tsx | 4 +- packages/app/src/components/prompt-input.tsx | 8 +- .../src/components/status-popover-body.tsx | 4 +- packages/app/src/context/global-sync.tsx | 30 +++-- .../app/src/context/global-sync/bootstrap.ts | 105 +++++------------- .../src/pages/layout/sidebar-workspace.tsx | 14 +-- 6 files changed, 66 insertions(+), 99 deletions(-) diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx index 9bb36d32d8..576ec8fec4 100644 --- a/packages/app/src/components/dialog-select-mcp.tsx +++ b/packages/app/src/components/dialog-select-mcp.tsx @@ -6,7 +6,7 @@ import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" import { useLanguage } from "@/context/language" -import { loadMcpQuery } from "@/context/global-sync" +import { mcpQueryKey } from "@/context/global-sync" const statusLabels = { connected: "mcp.status.connected", @@ -32,7 +32,7 @@ export const DialogSelectMcp: Component = () => { if (sync.data.mcp[name]?.status === "connected") await sdk.client.mcp.disconnect({ name }) else await sdk.client.mcp.connect({ name }) }, - onSuccess: () => queryClient.refetchQueries({ queryKey: loadMcpQuery(sync.directory).queryKey }), + onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }), })) const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 0a18096164..2417fa98e2 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -16,6 +16,7 @@ import { } from "@/context/prompt" import { useLayout } from "@/context/layout" import { useSDK } from "@/context/sdk" +import { useGlobalSDK } from "@/context/global-sdk" import { useSync } from "@/context/sync" import { useComments } from "@/context/comments" import { Button } from "@opencode-ai/ui/button" @@ -102,6 +103,7 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/ export const PromptInput: Component = (props) => { const sdk = useSDK() + const globalSDK = useGlobalSDK() const sync = useSync() const local = useLocal() @@ -1253,7 +1255,11 @@ export const PromptInput: Component = (props) => { } const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({ - queries: [loadAgentsQuery(sdk.directory), loadProvidersQuery(null), loadProvidersQuery(sdk.directory)], + queries: [ + loadAgentsQuery(sdk.directory, sdk.client), + loadProvidersQuery(null, globalSDK.client), + loadProvidersQuery(sdk.directory, sdk.client), + ], })) const agentsLoading = () => agentsQuery.isLoading diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx index 952e3eac64..bbac562784 100644 --- a/packages/app/src/components/status-popover-body.tsx +++ b/packages/app/src/components/status-popover-body.tsx @@ -15,7 +15,7 @@ import { useSDK } from "@/context/sdk" import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" import { useSync } from "@/context/sync" import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health" -import { loadMcpQuery } from "@/context/global-sync" +import { mcpQueryKey } from "@/context/global-sync" const pollMs = 10_000 @@ -145,7 +145,7 @@ const useMcpToggleMutation = () => { const status = sync.data.mcp[name] await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name })) }, - onSuccess: () => queryClient.refetchQueries({ queryKey: loadMcpQuery(sync.directory).queryKey }), + onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }), onError: (err) => { showToast({ variant: "error", diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 6190deb1ee..31c90463d8 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -20,7 +20,6 @@ import { clearProviderRev, loadGlobalConfigQuery, loadPathQuery, - loadProjectsQuery, loadProvidersQuery, } from "./global-sync/bootstrap" import { createChildStoreManager } from "./global-sync/child-store" @@ -31,7 +30,7 @@ import { trimSessions } from "./global-sync/session-trim" import type { ProjectMeta } from "./global-sync/types" import { SESSION_RECENT_LIMIT } from "./global-sync/types" import { formatServerError } from "@/utils/server-errors" -import { queryOptions, skipToken, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query" +import { queryOptions, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query" import { createRefreshQueue } from "./global-sync/queue" import { directoryKey } from "./global-sync/utils" @@ -49,19 +48,22 @@ type GlobalStore = { reload: undefined | "pending" | "complete" } -export const loadSessionsQuery = (directory: string) => - queryOptions({ queryKey: [directory, "loadSessions"], queryFn: skipToken }) +export const loadSessionsQueryKey = (directory: string) => [directory, "loadSessions"] as const -export const loadMcpQuery = (directory: string, sdk?: OpencodeClient) => +export const mcpQueryKey = (directory: string) => [directory, "mcp"] as const + +export const loadMcpQuery = (directory: string, sdk: OpencodeClient) => queryOptions({ - queryKey: [directory, "mcp"], - queryFn: sdk ? () => sdk.mcp.status().then((r) => r.data ?? {}) : skipToken, + queryKey: mcpQueryKey(directory), + queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}), }) -export const loadLspQuery = (directory: string, sdk?: OpencodeClient) => +export const lspQueryKey = (directory: string) => [directory, "lsp"] as const + +export const loadLspQuery = (directory: string, sdk: OpencodeClient) => queryOptions({ - queryKey: [directory, "lsp"], - queryFn: sdk ? () => sdk.lsp.status().then((r) => r.data ?? []) : skipToken, + queryKey: lspQueryKey(directory), + queryFn: () => sdk.lsp.status().then((r) => r.data ?? []), }) function createGlobalSync() { @@ -76,7 +78,11 @@ function createGlobalSync() { const sessionMeta = new Map() const [configQuery, providerQuery, pathQuery] = useQueries(() => ({ - queries: [loadGlobalConfigQuery(), loadProvidersQuery(null), loadPathQuery(null), loadProjectsQuery()], + queries: [ + loadGlobalConfigQuery(globalSDK.client), + loadProvidersQuery(null, globalSDK.client), + loadPathQuery(null, globalSDK.client), + ], })) const [globalStore, setGlobalStore] = createStore({ @@ -233,7 +239,7 @@ function createGlobalSync() { const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT) const promise = queryClient .fetchQuery({ - ...loadSessionsQuery(key), + queryKey: loadSessionsQueryKey(key), queryFn: () => loadRootSessionsWithFallback({ directory, diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index e85516bf14..531917bde6 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -18,7 +18,7 @@ import { reconcile, type SetStoreFunction, type Store } from "solid-js/store" 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 { QueryClient, queryOptions } from "@tanstack/solid-query" import { loadMcpQuery } from "../global-sync" type GlobalStore = { @@ -83,44 +83,25 @@ function showErrors(input: { }) } -export const loadGlobalConfigQuery = ( - sdk?: OpencodeClient, - transform?: (x: Awaited>) => void, -) => +export const loadGlobalConfigQuery = (sdk: OpencodeClient) => queryOptions({ queryKey: ["config"], - queryFn: sdk - ? () => - retry(() => - sdk.global.config.get().then((x) => { - transform?.(x) - return x.data! - }), - ) - : skipToken, + queryFn: () => retry(() => sdk.global.config.get().then((x) => x.data!)), }) -export const loadProjectsQuery = ( - sdk?: OpencodeClient, - transform?: (x: Awaited>["data"]) => void, -) => +export const loadProjectsQuery = (sdk: OpencodeClient) => queryOptions({ queryKey: ["project"], - queryFn: sdk - ? () => - retry(() => - sdk.project - .list() - .then((x) => { - return (x.data ?? []) - .filter((p) => !!p?.id) - .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) - .slice() - .sort((a, b) => cmp(a.id, b.id)) - }) - .then(transform), - ) - : skipToken, + queryFn: () => + retry(() => + sdk.project.list().then((x) => { + return (x.data ?? []) + .filter((p) => !!p?.id) + .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) + .slice() + .sort((a, b) => cmp(a.id, b.id)) + }), + ), }) export async function bootstrapGlobal(input: { @@ -136,9 +117,9 @@ export async function bootstrapGlobal(input: { () => input.queryClient.fetchQuery(loadProvidersQuery(null, input.globalSDK)), () => input.queryClient.fetchQuery(loadPathQuery(null, input.globalSDK)), () => - input.queryClient.fetchQuery( - loadProjectsQuery(input.globalSDK, (data) => input.setGlobalStore("project", data ?? [])), - ), + input.queryClient + .fetchQuery(loadProjectsQuery(input.globalSDK)) + .then((data) => input.setGlobalStore("project", data)), ] await runAll(slow) // showErrors({ @@ -197,46 +178,22 @@ function warmSessions(input: { ).then(() => undefined) } -export const loadProvidersQuery = (directory: string | null, sdk?: OpencodeClient) => +export const loadProvidersQuery = (directory: string | null, sdk: OpencodeClient) => queryOptions({ queryKey: [directory, "providers"], - queryFn: sdk ? () => retry(() => sdk.provider.list().then((x) => normalizeProviderList(x.data!))) : skipToken, + queryFn: () => retry(() => sdk.provider.list().then((x) => normalizeProviderList(x.data!))), }) -export const loadAgentsQuery = ( - directory: string | null, - sdk?: OpencodeClient, - transform?: (x: Awaited>) => void, -) => +export const loadAgentsQuery = (directory: string | null, sdk: OpencodeClient) => queryOptions({ queryKey: [directory, "agents"], - queryFn: sdk - ? () => - retry(() => - sdk.app.agents().then((x) => { - transform?.(x) - return x.data! - }), - ) - : skipToken, + queryFn: () => retry(() => sdk.app.agents().then((x) => normalizeAgentList(x.data))), }) -export const loadPathQuery = ( - directory: string | null, - sdk?: OpencodeClient, - transform?: (x: Awaited>) => void, -) => +export const loadPathQuery = (directory: string | null, sdk: OpencodeClient) => queryOptions({ queryKey: [directory, "path"], - queryFn: sdk - ? () => - retry(() => - sdk.path.get().then(async (x) => { - transform?.(x) - return x.data! - }), - ) - : skipToken, + queryFn: () => retry(() => sdk.path.get().then((x) => x.data!)), }) export async function bootstrapDirectory(input: { @@ -271,9 +228,9 @@ export async function bootstrapDirectory(input: { const slow = [ () => Promise.resolve(input.loadSessions(input.directory)), () => - input.queryClient.ensureQueryData( - loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))), - ), + input.queryClient + .ensureQueryData(loadAgentsQuery(input.directory, input.sdk)) + .then((data) => input.setStore("agent", data)), () => retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))), () => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))), @@ -281,12 +238,10 @@ export async function bootstrapDirectory(input: { (() => 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) - }), - )), + input.queryClient.ensureQueryData(loadPathQuery(input.directory, input.sdk)).then((data) => { + const next = projectID(data.directory ?? input.directory, input.global.project) + if (next) input.setStore("project", next) + })), () => retry(() => input.sdk.vcs.get().then((x) => { diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index d2e887b444..9b80adac29 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -14,12 +14,12 @@ import { Spinner } from "@opencode-ai/ui/spinner" import { Tooltip } from "@opencode-ai/ui/tooltip" import { type Session } from "@opencode-ai/sdk/v2/client" import { type LocalProject } from "@/context/layout" -import { loadSessionsQuery, useGlobalSync } from "@/context/global-sync" +import { loadSessionsQueryKey, useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { pathKey } from "@/utils/path-key" import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items" import { sortedRootSessions } from "./helpers" -import { useQuery } from "@tanstack/solid-query" +import { useIsFetching } from "@tanstack/solid-query" type InlineEditorComponent = (props: { id: string @@ -320,9 +320,9 @@ export const SortableWorkspace = (props: { const boot = createMemo(() => open() || active()) const count = createMemo(() => sessions()?.length ?? 0) const hasMore = createMemo(() => workspaceStore.sessionTotal > count()) - const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) })) + const fetching = useIsFetching(() => ({ queryKey: loadSessionsQueryKey(props.directory) })) const busy = createMemo(() => props.ctx.isBusy(props.directory)) - const loading = () => query.isLoading && count() === 0 + const loading = () => fetching() > 0 && count() === 0 const touch = createMediaQuery("(hover: none)") const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id))) const loadMore = async () => { @@ -427,7 +427,7 @@ export const SortableWorkspace = (props: { mobile={props.mobile} ctx={props.ctx} showNew={showNew} - loading={() => query.isLoading && count() === 0} + loading={loading} sessions={sessions} hasMore={hasMore} loadMore={loadMore} @@ -454,9 +454,9 @@ export const LocalWorkspace = (props: { const slug = createMemo(() => base64Encode(props.project.worktree)) const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow())) const count = createMemo(() => sessions()?.length ?? 0) - const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) })) + const fetching = useIsFetching(() => ({ queryKey: loadSessionsQueryKey(props.project.worktree) })) const hasMore = createMemo(() => workspace().store.sessionTotal > count()) - const loading = () => query.isLoading && count() === 0 + const loading = () => fetching() > 0 && count() === 0 const loadMore = async () => { workspace().setStore("limit", (limit) => (limit ?? 0) + 5) await globalSync.project.loadSessions(props.project.worktree)