Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/app/src/components/dialog-select-mcp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { mcpQueryKey } from "@/context/global-sync"
import { useQueryOptions } from "@/context/global-sync"

const statusLabels = {
connected: "mcp.status.connected",
Expand All @@ -20,6 +20,7 @@ export const DialogSelectMcp: Component = () => {
const sdk = useSDK()
const language = useLanguage()
const queryClient = useQueryClient()
const queryOptions = useQueryOptions()

const items = createMemo(() =>
Object.entries(sync.data.mcp ?? {})
Expand All @@ -32,7 +33,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: mcpQueryKey(sync.directory) }),
onSuccess: () => queryClient.refetchQueries({ queryKey: queryOptions.mcp(sync.directory).queryKey }),
}))

const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
Expand Down
11 changes: 3 additions & 8 deletions packages/app/src/components/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ 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"
Expand Down Expand Up @@ -56,7 +55,7 @@ import { PromptDragOverlay } from "./prompt-input/drag-overlay"
import { promptPlaceholder } from "./prompt-input/placeholder"
import { ImagePreview } from "@opencode-ai/ui/image-preview"
import { useQueries } from "@tanstack/solid-query"
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
import { useQueryOptions } from "@/context/global-sync"

interface PromptInputProps {
class?: string
Expand Down Expand Up @@ -103,7 +102,7 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/

export const PromptInput: Component<PromptInputProps> = (props) => {
const sdk = useSDK()
const globalSDK = useGlobalSDK()
const queryOptions = useQueryOptions()

const sync = useSync()
const local = useLocal()
Expand Down Expand Up @@ -1255,11 +1254,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}

const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
queries: [
loadAgentsQuery(sdk.directory, sdk.client),
loadProvidersQuery(null, globalSDK.client),
loadProvidersQuery(sdk.directory, sdk.client),
],
queries: [queryOptions.agents(sdk.directory), queryOptions.providers(null), queryOptions.providers(sdk.directory)],
}))

const agentsLoading = () => agentsQuery.isLoading
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/components/status-popover-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { mcpQueryKey } from "@/context/global-sync"
import { useQueryOptions } from "@/context/global-sync"

const pollMs = 10_000

Expand Down Expand Up @@ -139,13 +139,14 @@ const useMcpToggleMutation = () => {
const sdk = useSDK()
const language = useLanguage()
const queryClient = useQueryClient()
const queryOptions = useQueryOptions()

return useMutation(() => ({
mutationFn: async (name: string) => {
const status = sync.data.mcp[name]
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
},
onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }),
onSuccess: () => queryClient.refetchQueries({ queryKey: queryOptions.mcp(sync.directory).queryKey }),
onError: (err) => {
showToast({
variant: "error",
Expand Down
66 changes: 38 additions & 28 deletions packages/app/src/context/global-sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import {
bootstrapDirectory,
bootstrapGlobal,
clearProviderRev,
loadAgentsQuery,
loadGlobalConfigQuery,
loadPathQuery,
loadProjectsQuery,
loadProvidersQuery,
} from "./global-sync/bootstrap"
import { createChildStoreManager } from "./global-sync/child-store"
Expand Down Expand Up @@ -48,21 +50,15 @@ type GlobalStore = {
reload: undefined | "pending" | "complete"
}

export const loadSessionsQueryKey = (directory: string) => [directory, "loadSessions"] as const

export const mcpQueryKey = (directory: string) => [directory, "mcp"] as const

export const loadMcpQuery = (directory: string, sdk: OpencodeClient) =>
queryOptions({
queryKey: mcpQueryKey(directory),
queryKey: [directory, "mcp"] as const,
queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}),
})

export const lspQueryKey = (directory: string) => [directory, "lsp"] as const

export const loadLspQuery = (directory: string, sdk: OpencodeClient) =>
queryOptions({
queryKey: lspQueryKey(directory),
queryKey: [directory, "lsp"] as const,
queryFn: () => sdk.lsp.status().then((r) => r.data ?? []),
})

Expand All @@ -77,12 +73,33 @@ function createGlobalSync() {
const sessionLoads = new Map<string, Promise<void>>()
const sessionMeta = new Map<string, { limit: number }>()

const sdkFor = (directory: string) => {
const key = directoryKey(directory)
const cached = sdkCache.get(key)
if (cached) return cached
const sdk = globalSDK.createClient({
directory,
throwOnError: true,
})
sdkCache.set(key, sdk)
return sdk
}

const queryOptionsApi = {
globalConfig: () => loadGlobalConfigQuery(globalSDK.client),
projects: () => loadProjectsQuery(globalSDK.client),
providers: (directory: string | null) =>
loadProvidersQuery(directory, directory === null ? globalSDK.client : sdkFor(directory)),
path: (directory: string | null) =>
loadPathQuery(directory, directory === null ? globalSDK.client : sdkFor(directory)),
agents: (directory: string) => loadAgentsQuery(directory, sdkFor(directory)),
mcp: (directory: string) => loadMcpQuery(directory, sdkFor(directory)),
lsp: (directory: string) => loadLspQuery(directory, sdkFor(directory)),
sessions: (directory: string) => ({ queryKey: [directory, "loadSessions"] as const }),
}

const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
queries: [
loadGlobalConfigQuery(globalSDK.client),
loadProvidersQuery(null, globalSDK.client),
loadPathQuery(null, globalSDK.client),
],
queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)],
}))

const [globalStore, setGlobalStore] = createStore<GlobalStore>({
Expand Down Expand Up @@ -181,18 +198,6 @@ function createGlobalSync() {
bootstrapInstance,
})

const sdkFor = (directory: string) => {
const key = directoryKey(directory)
const cached = sdkCache.get(key)
if (cached) return cached
const sdk = globalSDK.createClient({
directory,
throwOnError: true,
})
sdkCache.set(key, sdk)
return sdk
}

const children = createChildStoreManager({
owner,
isBooting: (directory) => booting.has(directory),
Expand All @@ -209,7 +214,7 @@ function createGlobalSync() {
clearSessionPrefetchDirectory(key)
},
translate: language.t,
getSdk: sdkFor,
queryOptions: queryOptionsApi,
global: {
provider: globalStore.provider,
},
Expand Down Expand Up @@ -239,7 +244,7 @@ function createGlobalSync() {
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
const promise = queryClient
.fetchQuery({
queryKey: loadSessionsQueryKey(key),
...queryOptionsApi.sessions(key),
queryFn: () =>
loadRootSessionsWithFallback({
directory,
Expand Down Expand Up @@ -368,7 +373,7 @@ function createGlobalSync() {
setSessionTodo,
vcsCache: children.vcsCache.get(key),
loadLsp: () => {
void queryClient.fetchQuery(loadLspQuery(key, sdkFor(directory)))
void queryClient.fetchQuery(queryOptionsApi.lsp(key))
},
})
})
Expand Down Expand Up @@ -426,6 +431,7 @@ function createGlobalSync() {
},
child: children.child,
peek: children.peek,
queryOptions: queryOptionsApi,
// bootstrap,
updateConfig: updateConfigMutation.mutateAsync,
project: projectApi,
Expand All @@ -447,3 +453,7 @@ export function useGlobalSync() {
if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider")
return context
}

export function useQueryOptions() {
return useGlobalSync().queryOptions
}
7 changes: 6 additions & 1 deletion packages/app/src/context/global-sync/child-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ describe("createChildStoreManager", () => {
onBootstrap() {},
onDispose() {},
translate: (key) => key,
getSdk: () => null!,
queryOptions: {
lsp: () => null!,
mcp: () => null!,
path: () => null!,
providers: () => null!,
},
global: { provider: null! },
})

Expand Down
19 changes: 11 additions & 8 deletions packages/app/src/context/global-sync/child-store.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
import { Persist, persisted } from "@/utils/persist"
import type { OpencodeClient, ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
import type { ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
import {
DIR_IDLE_TTL_MS,
MAX_DIR_STORES,
Expand All @@ -26,7 +26,12 @@ export function createChildStoreManager(input: {
onBootstrap: (directory: string) => void
onDispose: (directory: string) => void
translate: (key: string, vars?: Record<string, string | number>) => string
getSdk: (directory: string) => OpencodeClient
queryOptions: {
lsp: (directory: string) => ReturnType<typeof loadLspQuery>
mcp: (directory: string) => ReturnType<typeof loadMcpQuery>
path: (directory: string) => ReturnType<typeof loadPathQuery>
providers: (directory: string) => ReturnType<typeof loadProvidersQuery>
}
global: {
provider: ProviderListResponse
}
Expand Down Expand Up @@ -171,17 +176,15 @@ export function createChildStoreManager(input: {

const init = () =>
createRoot((dispose) => {
const sdk = input.getSdk(directory)

const initialMeta = meta[0].value
const initialIcon = icon[0].value

const [pathQuery, mcpQuery, lspQuery, providerQuery] = useQueries(() => ({
queries: [
loadPathQuery(key, sdk),
loadMcpQuery(key, sdk),
loadLspQuery(key, sdk),
loadProvidersQuery(key, sdk),
input.queryOptions.path(key),
input.queryOptions.mcp(key),
input.queryOptions.lsp(key),
input.queryOptions.providers(key),
],
}))

Expand Down
8 changes: 5 additions & 3 deletions packages/app/src/pages/layout/sidebar-workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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 { loadSessionsQueryKey, useGlobalSync } from "@/context/global-sync"
import { useGlobalSync, useQueryOptions } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { pathKey } from "@/utils/path-key"
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
Expand Down Expand Up @@ -300,6 +300,7 @@ export const SortableWorkspace = (props: {
const navigate = useNavigate()
const params = useParams()
const globalSync = useGlobalSync()
const queryOptions = useQueryOptions()
const language = useLanguage()
const sortable = createSortable(props.directory)
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false })
Expand All @@ -320,7 +321,7 @@ export const SortableWorkspace = (props: {
const boot = createMemo(() => open() || active())
const count = createMemo(() => sessions()?.length ?? 0)
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
const fetching = useIsFetching(() => ({ queryKey: loadSessionsQueryKey(props.directory) }))
const fetching = useIsFetching(() => ({ queryKey: queryOptions.sessions(props.directory).queryKey }))
const busy = createMemo(() => props.ctx.isBusy(props.directory))
const loading = () => fetching() > 0 && count() === 0
const touch = createMediaQuery("(hover: none)")
Expand Down Expand Up @@ -446,6 +447,7 @@ export const LocalWorkspace = (props: {
mobile?: boolean
}): JSX.Element => {
const globalSync = useGlobalSync()
const queryOptions = useQueryOptions()
const language = useLanguage()
const workspace = createMemo(() => {
const [store, setStore] = globalSync.child(props.project.worktree)
Expand All @@ -454,7 +456,7 @@ 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 fetching = useIsFetching(() => ({ queryKey: loadSessionsQueryKey(props.project.worktree) }))
const fetching = useIsFetching(() => ({ queryKey: queryOptions.sessions(props.project.worktree).queryKey }))
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
const loading = () => fetching() > 0 && count() === 0
const loadMore = async () => {
Expand Down
Loading