}) {
"bg-icon-success-base": status() === "connected",
"bg-icon-critical-base": status() === "failed",
"bg-border-weak-base": status() === "disabled",
+ "bg-icon-warning-base animate-pulse": status() === "connecting",
"bg-icon-warning-base":
status() === "needs_auth" || status() === "needs_client_registration",
}}
@@ -467,8 +470,9 @@ export function StatusPopoverBody(props: { shown: Accessor
}) {
event.stopPropagation()}>
{
+ if (connecting()) return
if (toggleMcp.isPending) return
toggleMcp.mutate(name)
}}
diff --git a/packages/app/src/context/directory-sync.ts b/packages/app/src/context/directory-sync.ts
index a54701f0db..a9ae05d8d3 100644
--- a/packages/app/src/context/directory-sync.ts
+++ b/packages/app/src/context/directory-sync.ts
@@ -172,7 +172,8 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType
type Setter = Child[1]
- const current = createMemo(() => serverSync.child(directory))
+ serverSync.child(directory, { active: true })
+ const current = createMemo(() => serverSync.child(directory, { active: true }))
const target = (directory?: string) => {
if (!directory || directory === directory) return current()
return serverSync.child(directory)
diff --git a/packages/app/src/context/global-sync/bootstrap.test.ts b/packages/app/src/context/global-sync/bootstrap.test.ts
index b75ae5cc32..67c1cb05a4 100644
--- a/packages/app/src/context/global-sync/bootstrap.test.ts
+++ b/packages/app/src/context/global-sync/bootstrap.test.ts
@@ -10,6 +10,7 @@ const provider = { all: new Map(), connected: [], default: {} } satisfies Normal
describe("bootstrapDirectory", () => {
test("marks a loading directory partial during bootstrap and complete after success", async () => {
+ const mcpReads: string[] = []
const [store, setStore] = createStore({
status: "loading",
agent: [],
@@ -44,6 +45,7 @@ describe("bootstrapDirectory", () => {
await bootstrapDirectory({
directory: "/project",
+ active: false,
global: {
config: {} satisfies Config,
path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" },
@@ -55,10 +57,20 @@ describe("bootstrapDirectory", () => {
config: { get: async () => ({ data: {} }) },
session: { status: async () => ({ data: {} }) },
vcs: { get: async () => ({ data: undefined }) },
- command: { list: async () => ({ data: [] }) },
+ command: {
+ list: async () => {
+ mcpReads.push("command")
+ return { data: [] }
+ },
+ },
permission: { list: async () => ({ data: [] }) },
question: { list: async () => ({ data: [] }) },
- mcp: { status: async () => ({ data: {} }) },
+ mcp: {
+ status: async () => {
+ mcpReads.push("status")
+ return { data: {} }
+ },
+ },
provider: { list: async () => ({ data: { all: [], connected: [], default: {} } }) },
} as unknown as OpencodeClient,
store,
@@ -74,5 +86,6 @@ describe("bootstrapDirectory", () => {
await new Promise((resolve) => setTimeout(resolve, 80))
expect(store.status).toBe("complete")
+ expect(mcpReads).toEqual([])
})
})
diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts
index b6e2f10853..cc2b373bb9 100644
--- a/packages/app/src/context/global-sync/bootstrap.ts
+++ b/packages/app/src/context/global-sync/bootstrap.ts
@@ -198,6 +198,7 @@ export const loadPathQuery = (directory: string | null, sdk: OpencodeClient) =>
export async function bootstrapDirectory(input: {
directory: string
+ active: boolean
sdk: OpencodeClient
store: Store
setStore: SetStoreFunction
@@ -250,7 +251,7 @@ export async function bootstrapDirectory(input: {
if (next) input.vcsCache.setStore("value", next)
}),
),
- () => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
+ input.active && (() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])))),
() =>
retry(() =>
input.sdk.permission.list().then((x) => {
@@ -304,7 +305,7 @@ export async function bootstrapDirectory(input: {
}),
),
() => Promise.resolve(input.loadSessions(input.directory)),
- () => input.queryClient.fetchQuery(loadMcpQuery(input.directory, input.sdk)),
+ input.active && (() => input.queryClient.fetchQuery(loadMcpQuery(input.directory, input.sdk))),
() =>
input.queryClient.fetchQuery(loadProvidersQuery(input.directory, input.sdk)).catch((err) => {
const project = getFilename(input.directory)
diff --git a/packages/app/src/context/global-sync/child-store.test.ts b/packages/app/src/context/global-sync/child-store.test.ts
index d140b3e766..875c74122e 100644
--- a/packages/app/src/context/global-sync/child-store.test.ts
+++ b/packages/app/src/context/global-sync/child-store.test.ts
@@ -6,6 +6,7 @@ import type { State } from "./types"
import type { QueryOptionsApi } from "../server-sync"
let createChildStoreManager: typeof import("./child-store").createChildStoreManager
+const mcpQueries: Array<() => { enabled?: () => boolean }> = []
const child = () => createStore({} as State)
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
@@ -50,10 +51,13 @@ beforeAll(async () => {
mock.module("@tanstack/solid-query", () => ({
useQueries: () => [
{ isLoading: false, data: { state: "", config: "", worktree: "", directory: "", home: "" } },
- { isLoading: false, data: {} },
{ isLoading: false, data: [] },
{ isLoading: false, data: provider },
],
+ useQuery: (options: () => { enabled?: () => boolean }) => {
+ mcpQueries.push(options)
+ return { isLoading: false, data: {} }
+ },
}))
createChildStoreManager = (await import("./child-store")).createChildStoreManager
@@ -73,6 +77,7 @@ describe("createChildStoreManager", () => {
isBooting: () => false,
isLoadingSessions: () => false,
onBootstrap() {},
+ onActivate() {},
onDispose() {},
translate: (key) => key,
queryOptions: queryOptionsApi,
@@ -103,6 +108,7 @@ describe("createChildStoreManager", () => {
onBootstrap(directory) {
bootstraps.push(directory)
},
+ onActivate() {},
onDispose() {},
translate: (key) => key,
queryOptions: queryOptionsApi,
@@ -121,4 +127,44 @@ describe("createChildStoreManager", () => {
dispose()
}
})
+
+ test("starts observing MCP only when an existing directory becomes active", () => {
+ let manager: ReturnType | undefined
+ const offset = mcpQueries.length
+ const activated: string[] = []
+
+ const dispose = createOwner((owner) => {
+ manager = createChildStoreManager({
+ owner,
+ isBooting: () => false,
+ isLoadingSessions: () => false,
+ onBootstrap() {},
+ onActivate(directory) {
+ activated.push(directory)
+ },
+ onDispose() {},
+ translate: (key) => key,
+ queryOptions: queryOptionsApi,
+ global: { provider },
+ })
+ })
+
+ try {
+ if (!manager) throw new Error("manager required")
+
+ manager.child("/project", { bootstrap: false })
+ const query = mcpQueries[offset]
+ if (!query) throw new Error("mcp query required")
+ expect(query().enabled?.()).toBe(false)
+
+ manager.child("/project", { active: true })
+ expect(query().enabled?.()).toBe(true)
+ expect(activated).toEqual(["/project"])
+
+ manager.child("/project", { active: true })
+ expect(activated).toEqual(["/project"])
+ } finally {
+ dispose()
+ }
+ })
})
diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts
index db6dd31010..9d4a4c5701 100644
--- a/packages/app/src/context/global-sync/child-store.ts
+++ b/packages/app/src/context/global-sync/child-store.ts
@@ -1,4 +1,4 @@
-import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
+import { createRoot, createSignal, 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 { VcsInfo } from "@opencode-ai/sdk/v2/client"
@@ -14,7 +14,7 @@ import {
type VcsCache,
} from "./types"
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
-import { useQueries } from "@tanstack/solid-query"
+import { useQueries, useQuery } from "@tanstack/solid-query"
import { QueryOptionsApi } from "../server-sync"
import { directoryKey, type DirectoryKey } from "./utils"
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
@@ -24,6 +24,7 @@ export function createChildStoreManager(input: {
isBooting: (directory: string) => boolean
isLoadingSessions: (directory: string) => boolean
onBootstrap: (directory: string) => void
+ onActivate: (directory: string) => void
onDispose: (directory: string) => void
translate: (key: string, vars?: Record) => string
queryOptions: QueryOptionsApi
@@ -39,6 +40,8 @@ export function createChildStoreManager(input: {
const pins = new Map()
const ownerPins = new WeakMap