0}
- fallback={renderEmptyState(isLoading() ? "Loading server status..." : "No MCP servers detected.")}
+ when={!isMcpLoading() && mcpServers().length > 0}
+ fallback={renderEmptyState(isMcpLoading() ? "Loading MCP servers..." : "No MCP servers detected.")}
>
@@ -161,7 +154,7 @@ const InstanceServiceStatus: Component = (props) =>
const pendingAction = () => pendingMcpActions()[server.name]
const isPending = () => Boolean(pendingAction())
const isRunning = () => server.status === "running"
- const switchDisabled = () => isPending() || !instance()?.client
+ const switchDisabled = () => isPending() || !instance().client
const statusDotClass = () => {
if (isPending()) return "status-dot animate-pulse"
if (server.status === "running") return "status-dot ready animate-pulse"
diff --git a/packages/ui/src/components/instance/instance-shell2.tsx b/packages/ui/src/components/instance/instance-shell2.tsx
index 7580ac9e..a1a239bf 100644
--- a/packages/ui/src/components/instance/instance-shell2.tsx
+++ b/packages/ui/src/components/instance/instance-shell2.tsx
@@ -132,7 +132,6 @@ const InstanceShell2: Component = (props) => {
const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instance.id))
-
const desktopQuery = useMediaQuery("(min-width: 1280px)")
const tabletQuery = useMediaQuery("(min-width: 768px)")
@@ -860,7 +859,6 @@ const InstanceShell2: Component = (props) => {
label: "LSP Servers",
render: () => (
= (props) => {
label: "MCP Servers",
render: () => (
+ instance: Accessor
+ metadata: Accessor
+ refreshMetadata: () => Promise
+}
+
+const InstanceMetadataContext = createContext(null)
+
+interface InstanceMetadataProviderProps {
+ instance: Instance
+ children: JSX.Element
+}
+
+export const InstanceMetadataProvider: Component = (props) => {
+ const resolvedInstance = createMemo(() => instances().get(props.instance.id) ?? props.instance)
+ const [isLoading, setIsLoading] = createSignal(true)
+
+ const ensureMetadata = async (force = false) => {
+ const current = resolvedInstance()
+ if (!current) {
+ setIsLoading(false)
+ return
+ }
+
+ if (!force && hasMetadataLoaded(current.metadata)) {
+ setIsLoading(false)
+ return
+ }
+
+ setIsLoading(true)
+ await loadInstanceMetadata(current, { force })
+ setIsLoading(false)
+ }
+
+ createEffect(() => {
+ const current = resolvedInstance()
+ // Ensure metadata becomes a dependency so we re-check when store updates
+ void current?.metadata
+ void ensureMetadata()
+ })
+
+ const contextValue: InstanceMetadataContextValue = {
+ isLoading,
+ instance: resolvedInstance,
+ metadata: () => resolvedInstance().metadata,
+ refreshMetadata: () => ensureMetadata(true),
+ }
+
+ return (
+
+ {props.children}
+
+ )
+}
+
+export function useInstanceMetadataContext(): InstanceMetadataContextValue {
+ const ctx = useContext(InstanceMetadataContext)
+ if (!ctx) {
+ throw new Error("useInstanceMetadataContext must be used within InstanceMetadataProvider")
+ }
+ return ctx
+}
+
+export function useOptionalInstanceMetadataContext(): InstanceMetadataContextValue | null {
+ return useContext(InstanceMetadataContext)
+}
diff --git a/packages/ui/src/lib/hooks/use-instance-metadata.ts b/packages/ui/src/lib/hooks/use-instance-metadata.ts
index cd538e29..929f7d86 100644
--- a/packages/ui/src/lib/hooks/use-instance-metadata.ts
+++ b/packages/ui/src/lib/hooks/use-instance-metadata.ts
@@ -1,4 +1,3 @@
-import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js"
import type { Instance, RawMcpStatus } from "../../types/instance"
import { fetchLspStatus, updateInstance } from "../../stores/instances"
import { getLogger } from "../../lib/logger"
@@ -6,87 +5,66 @@ import { getLogger } from "../../lib/logger"
const log = getLogger("session")
const pendingMetadataRequests = new Set()
-export function useInstanceMetadata(instanceAccessor: Accessor) {
- const [isLoading, setIsLoading] = createSignal(true)
+function hasMetadataLoaded(metadata?: Instance["metadata"]): boolean {
+ if (!metadata) return false
+ return "project" in metadata && "mcpStatus" in metadata && "lspStatus" in metadata
+}
- createEffect(() => {
- const instance = instanceAccessor()
- if (!instance) {
- setIsLoading(false)
- return
+export async function loadInstanceMetadata(instance: Instance, options?: { force?: boolean }): Promise {
+ const client = instance.client
+ if (!client) {
+ log.warn("[metadata] Skipping fetch; client missing", { instanceId: instance.id })
+ return
+ }
+
+ if (!options?.force && hasMetadataLoaded(instance.metadata)) {
+ return
+ }
+
+ if (pendingMetadataRequests.has(instance.id)) {
+ return
+ }
+
+ pendingMetadataRequests.add(instance.id)
+
+ try {
+ const [projectResult, mcpResult, lspResult] = await Promise.allSettled([
+ client.project.current(),
+ client.mcp.status(),
+ fetchLspStatus(instance.id),
+ ])
+
+ const project = projectResult.status === "fulfilled" ? projectResult.value.data : undefined
+ const mcpStatus = mcpResult.status === "fulfilled" ? (mcpResult.value.data as RawMcpStatus) : undefined
+ const lspStatus = lspResult.status === "fulfilled" ? lspResult.value ?? [] : undefined
+
+ const nextMetadata: Instance["metadata"] = {
+ ...(instance.metadata ?? {}),
}
- const instanceId = instance.id
- const client = instance.client
- const hasMetadata = Boolean(instance.metadata)
-
- if (!client) {
- setIsLoading(false)
- pendingMetadataRequests.delete(instanceId)
- return
+ if (projectResult.status === "fulfilled") {
+ nextMetadata.project = project ?? undefined
}
- if (hasMetadata) {
- setIsLoading(false)
- pendingMetadataRequests.delete(instanceId)
- return
+ if (mcpResult.status === "fulfilled") {
+ nextMetadata.mcpStatus = mcpStatus ?? nextMetadata.mcpStatus ?? {}
}
- if (pendingMetadataRequests.has(instanceId)) {
- setIsLoading(true)
- return
+ if (lspResult.status === "fulfilled") {
+ nextMetadata.lspStatus = lspStatus ?? []
}
- let cancelled = false
- pendingMetadataRequests.add(instanceId)
- setIsLoading(true)
+ if (!nextMetadata?.version && instance.binaryVersion) {
+ nextMetadata.version = instance.binaryVersion
+ }
- void (async () => {
- try {
- const [projectResult, mcpResult, lspResult] = await Promise.allSettled([
- client.project.current(),
- client.mcp.status(),
- fetchLspStatus(instanceId),
- ])
-
- if (cancelled) {
- return
- }
-
- const project = projectResult.status === "fulfilled" ? projectResult.value.data : undefined
- const mcpStatus = mcpResult.status === "fulfilled" ? (mcpResult.value.data as RawMcpStatus) : undefined
- const lspStatus = lspResult.status === "fulfilled" ? lspResult.value ?? [] : undefined
-
- const nextMetadata: Instance["metadata"] = {
- ...(instance.metadata ?? {}),
- ...(project ? { project } : {}),
- ...(mcpStatus ? { mcpStatus } : {}),
- ...(lspStatus ? { lspStatus } : {}),
- }
-
- if (!nextMetadata?.version && instance.binaryVersion) {
- nextMetadata.version = instance.binaryVersion
- }
-
- updateInstance(instanceId, { metadata: nextMetadata })
- } catch (error) {
- if (!cancelled) {
- log.error("Failed to load instance metadata", error)
- }
- } finally {
- pendingMetadataRequests.delete(instanceId)
- if (!cancelled) {
- setIsLoading(false)
- }
- }
- })()
-
- onCleanup(() => {
- cancelled = true
- })
- })
-
- return {
- isLoading,
+ updateInstance(instance.id, { metadata: nextMetadata })
+ } catch (error) {
+ log.error("Failed to load instance metadata", error)
+ } finally {
+ pendingMetadataRequests.delete(instance.id)
}
}
+
+export { hasMetadataLoaded }
+