Add shared instance metadata context
This commit is contained in:
@@ -8,6 +8,7 @@ import InstanceTabs from "./components/instance-tabs"
|
|||||||
import InstanceDisconnectedModal from "./components/instance-disconnected-modal"
|
import InstanceDisconnectedModal from "./components/instance-disconnected-modal"
|
||||||
import InstanceShell from "./components/instance/instance-shell2"
|
import InstanceShell from "./components/instance/instance-shell2"
|
||||||
import { RemoteAccessOverlay } from "./components/remote-access-overlay"
|
import { RemoteAccessOverlay } from "./components/remote-access-overlay"
|
||||||
|
import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context"
|
||||||
import { initMarkdown } from "./lib/markdown"
|
import { initMarkdown } from "./lib/markdown"
|
||||||
|
|
||||||
import { useTheme } from "./lib/theme"
|
import { useTheme } from "./lib/theme"
|
||||||
@@ -351,22 +352,25 @@ const App: Component = () => {
|
|||||||
{(instance) => {
|
{(instance) => {
|
||||||
const isActiveInstance = () => activeInstanceId() === instance.id
|
const isActiveInstance = () => activeInstanceId() === instance.id
|
||||||
const isVisible = () => isActiveInstance() && !showFolderSelection()
|
const isVisible = () => isActiveInstance() && !showFolderSelection()
|
||||||
return (
|
return (
|
||||||
<div class="flex-1 min-h-0 overflow-hidden" style={{ display: isVisible() ? "flex" : "none" }}>
|
<div class="flex-1 min-h-0 overflow-hidden" style={{ display: isVisible() ? "flex" : "none" }}>
|
||||||
<InstanceShell
|
<InstanceMetadataProvider instance={instance}>
|
||||||
instance={instance}
|
<InstanceShell
|
||||||
escapeInDebounce={escapeInDebounce()}
|
instance={instance}
|
||||||
paletteCommands={paletteCommands}
|
escapeInDebounce={escapeInDebounce()}
|
||||||
onCloseSession={(sessionId) => handleCloseSession(instance.id, sessionId)}
|
paletteCommands={paletteCommands}
|
||||||
onNewSession={() => handleNewSession(instance.id)}
|
onCloseSession={(sessionId) => handleCloseSession(instance.id, sessionId)}
|
||||||
handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)}
|
onNewSession={() => handleNewSession(instance.id)}
|
||||||
handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)}
|
handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)}
|
||||||
onExecuteCommand={executeCommand}
|
handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)}
|
||||||
tabBarOffset={instanceTabBarHeight()}
|
onExecuteCommand={executeCommand}
|
||||||
/>
|
tabBarOffset={instanceTabBarHeight()}
|
||||||
|
/>
|
||||||
|
</InstanceMetadataProvider>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, For, Show } from "solid-js"
|
import { Component, For, Show, createMemo } from "solid-js"
|
||||||
import type { Instance } from "../types/instance"
|
import type { Instance } from "../types/instance"
|
||||||
import { useInstanceMetadata } from "../lib/hooks/use-instance-metadata"
|
import { useOptionalInstanceMetadataContext } from "../lib/contexts/instance-metadata-context"
|
||||||
import InstanceServiceStatus from "./instance-service-status"
|
import InstanceServiceStatus from "./instance-service-status"
|
||||||
|
|
||||||
interface InstanceInfoProps {
|
interface InstanceInfoProps {
|
||||||
@@ -9,10 +9,19 @@ interface InstanceInfoProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
||||||
const { isLoading: isLoadingMetadata } = useInstanceMetadata(() => props.instance)
|
const metadataContext = useOptionalInstanceMetadataContext()
|
||||||
|
const isLoadingMetadata = metadataContext?.isLoading ?? (() => false)
|
||||||
|
const instanceAccessor = metadataContext?.instance ?? (() => props.instance)
|
||||||
|
const metadataAccessor = metadataContext?.metadata ?? (() => props.instance.metadata)
|
||||||
|
|
||||||
const metadata = () => props.instance.metadata
|
const currentInstance = () => instanceAccessor()
|
||||||
const binaryVersion = () => props.instance.binaryVersion || metadata()?.version
|
const metadata = () => metadataAccessor()
|
||||||
|
const binaryVersion = () => currentInstance().binaryVersion || metadata()?.version
|
||||||
|
const environmentVariables = () => currentInstance().environmentVariables
|
||||||
|
const environmentEntries = createMemo(() => {
|
||||||
|
const env = environmentVariables()
|
||||||
|
return env ? Object.entries(env) : []
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
@@ -23,7 +32,7 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||||||
<div>
|
<div>
|
||||||
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">Folder</div>
|
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">Folder</div>
|
||||||
<div class="text-xs text-primary font-mono break-all px-2 py-1.5 rounded border bg-surface-secondary border-base">
|
<div class="text-xs text-primary font-mono break-all px-2 py-1.5 rounded border bg-surface-secondary border-base">
|
||||||
{props.instance.folder}
|
{currentInstance().folder}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -72,24 +81,24 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.instance.binaryPath}>
|
<Show when={currentInstance().binaryPath}>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">
|
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">
|
||||||
Binary Path
|
Binary Path
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs font-mono break-all px-2 py-1.5 rounded border bg-surface-secondary border-base text-primary">
|
<div class="text-xs font-mono break-all px-2 py-1.5 rounded border bg-surface-secondary border-base text-primary">
|
||||||
{props.instance.binaryPath}
|
{currentInstance().binaryPath}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.instance.environmentVariables && Object.keys(props.instance.environmentVariables).length > 0}>
|
<Show when={environmentEntries().length > 0}>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1.5">
|
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1.5">
|
||||||
Environment Variables ({Object.keys(props.instance.environmentVariables!).length})
|
Environment Variables ({environmentEntries().length})
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<For each={Object.entries(props.instance.environmentVariables!)}>
|
<For each={environmentEntries()}>
|
||||||
{([key, value]) => (
|
{([key, value]) => (
|
||||||
<div class="flex items-center gap-2 px-2 py-1.5 rounded border bg-surface-secondary border-base">
|
<div class="flex items-center gap-2 px-2 py-1.5 rounded border bg-surface-secondary border-base">
|
||||||
<span class="text-xs font-mono font-medium flex-1 text-primary" title={key}>
|
<span class="text-xs font-mono font-medium flex-1 text-primary" title={key}>
|
||||||
@@ -105,11 +114,7 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<InstanceServiceStatus
|
<InstanceServiceStatus initialInstance={props.instance} class="space-y-3" />
|
||||||
instanceId={props.instance.id}
|
|
||||||
initialInstance={props.instance}
|
|
||||||
class="space-y-3"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Show when={isLoadingMetadata()}>
|
<Show when={isLoadingMetadata()}>
|
||||||
<div class="text-xs text-muted py-1">
|
<div class="text-xs text-muted py-1">
|
||||||
@@ -132,21 +137,19 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||||||
<div class="space-y-1 text-xs">
|
<div class="space-y-1 text-xs">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-secondary">Port:</span>
|
<span class="text-secondary">Port:</span>
|
||||||
<span class="text-primary font-mono">{props.instance.port}</span>
|
<span class="text-primary font-mono">{currentInstance().port}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-secondary">PID:</span>
|
<span class="text-secondary">PID:</span>
|
||||||
<span class="text-primary font-mono">{props.instance.pid}</span>
|
<span class="text-primary font-mono">{currentInstance().pid}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-secondary">Status:</span>
|
<span class="text-secondary">Status:</span>
|
||||||
<span
|
<span class={`status-badge ${currentInstance().status}`}>
|
||||||
class={`status-badge ${props.instance.status}`}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class={`status-dot ${props.instance.status === "ready" ? "ready" : props.instance.status === "starting" ? "starting" : props.instance.status === "error" ? "error" : "stopped"} ${props.instance.status === "ready" || props.instance.status === "starting" ? "animate-pulse" : ""}`}
|
class={`status-dot ${currentInstance().status === "ready" ? "ready" : currentInstance().status === "starting" ? "starting" : currentInstance().status === "error" ? "error" : "stopped"} ${currentInstance().status === "ready" || currentInstance().status === "starting" ? "animate-pulse" : ""}`}
|
||||||
/>
|
/>
|
||||||
{props.instance.status}
|
{currentInstance().status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { For, Show, createMemo, createSignal, type Component } from "solid-js"
|
import { For, Show, createMemo, createSignal, type Component } from "solid-js"
|
||||||
import Switch from "@suid/material/Switch"
|
import Switch from "@suid/material/Switch"
|
||||||
import type { Instance, RawMcpStatus } from "../types/instance"
|
import type { Instance, RawMcpStatus } from "../types/instance"
|
||||||
import { instances, updateInstance } from "../stores/instances"
|
import { useOptionalInstanceMetadataContext } from "../lib/contexts/instance-metadata-context"
|
||||||
import { useInstanceMetadata } from "../lib/hooks/use-instance-metadata"
|
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
|
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
@@ -10,11 +9,10 @@ const log = getLogger("session")
|
|||||||
type ServiceSection = "lsp" | "mcp"
|
type ServiceSection = "lsp" | "mcp"
|
||||||
|
|
||||||
interface InstanceServiceStatusProps {
|
interface InstanceServiceStatusProps {
|
||||||
instanceId: string
|
|
||||||
initialInstance?: Instance
|
|
||||||
sections?: ServiceSection[]
|
sections?: ServiceSection[]
|
||||||
showSectionHeadings?: boolean
|
showSectionHeadings?: boolean
|
||||||
class?: string
|
class?: string
|
||||||
|
initialInstance?: Instance
|
||||||
}
|
}
|
||||||
|
|
||||||
type ParsedMcpStatus = {
|
type ParsedMcpStatus = {
|
||||||
@@ -44,17 +42,30 @@ function parseMcpStatus(status?: RawMcpStatus): ParsedMcpStatus[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const InstanceServiceStatus: Component<InstanceServiceStatusProps> = (props) => {
|
const InstanceServiceStatus: Component<InstanceServiceStatusProps> = (props) => {
|
||||||
const instance = createMemo(() => instances().get(props.instanceId) ?? props.initialInstance)
|
const metadataContext = useOptionalInstanceMetadataContext()
|
||||||
|
const instance = metadataContext?.instance ?? (() => {
|
||||||
|
if (props.initialInstance) {
|
||||||
|
return props.initialInstance
|
||||||
|
}
|
||||||
|
throw new Error("InstanceServiceStatus requires InstanceMetadataProvider or initialInstance prop")
|
||||||
|
})
|
||||||
|
const isLoading = metadataContext?.isLoading ?? (() => false)
|
||||||
|
const refreshMetadata = metadataContext?.refreshMetadata ?? (async () => Promise.resolve())
|
||||||
const sections = createMemo<ServiceSection[]>(() => props.sections ?? ["lsp", "mcp"])
|
const sections = createMemo<ServiceSection[]>(() => props.sections ?? ["lsp", "mcp"])
|
||||||
const includeLsp = createMemo(() => sections().includes("lsp"))
|
const includeLsp = createMemo(() => sections().includes("lsp"))
|
||||||
const includeMcp = createMemo(() => sections().includes("mcp"))
|
const includeMcp = createMemo(() => sections().includes("mcp"))
|
||||||
const showHeadings = () => props.showSectionHeadings !== false
|
const showHeadings = () => props.showSectionHeadings !== false
|
||||||
const { isLoading } = useInstanceMetadata(instance)
|
|
||||||
|
|
||||||
const metadata = createMemo(() => instance()?.metadata)
|
const metadata = createMemo(() => instance().metadata)
|
||||||
|
const hasLspMetadata = () => metadata()?.lspStatus !== undefined
|
||||||
|
const hasMcpMetadata = () => metadata()?.mcpStatus !== undefined
|
||||||
const lspServers = createMemo(() => metadata()?.lspStatus ?? [])
|
const lspServers = createMemo(() => metadata()?.lspStatus ?? [])
|
||||||
const mcpServers = createMemo(() => parseMcpStatus(metadata()?.mcpStatus))
|
const mcpServers = createMemo(() => parseMcpStatus(metadata()?.mcpStatus))
|
||||||
|
|
||||||
|
const isLspLoading = () => isLoading() || !hasLspMetadata()
|
||||||
|
const isMcpLoading = () => isLoading() || !hasMcpMetadata()
|
||||||
|
|
||||||
|
|
||||||
const [pendingMcpActions, setPendingMcpActions] = createSignal<Record<string, "connect" | "disconnect">>({})
|
const [pendingMcpActions, setPendingMcpActions] = createSignal<Record<string, "connect" | "disconnect">>({})
|
||||||
|
|
||||||
const setPendingMcpAction = (name: string, action?: "connect" | "disconnect") => {
|
const setPendingMcpAction = (name: string, action?: "connect" | "disconnect") => {
|
||||||
@@ -66,26 +77,8 @@ const InstanceServiceStatus: Component<InstanceServiceStatusProps> = (props) =>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshMcpStatus = async () => {
|
|
||||||
const client = instance()?.client
|
|
||||||
if (!client?.mcp?.status) return
|
|
||||||
try {
|
|
||||||
const result = await client.mcp.status()
|
|
||||||
const status = result.data as RawMcpStatus | undefined
|
|
||||||
if (!status) return
|
|
||||||
updateInstance(props.instanceId, {
|
|
||||||
metadata: {
|
|
||||||
...(instance()?.metadata ?? {}),
|
|
||||||
mcpStatus: status,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
log.error("Failed to refresh MCP status", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleMcpServer = async (serverName: string, shouldEnable: boolean) => {
|
const toggleMcpServer = async (serverName: string, shouldEnable: boolean) => {
|
||||||
const client = instance()?.client
|
const client = instance().client
|
||||||
if (!client?.mcp) return
|
if (!client?.mcp) return
|
||||||
const action: "connect" | "disconnect" = shouldEnable ? "connect" : "disconnect"
|
const action: "connect" | "disconnect" = shouldEnable ? "connect" : "disconnect"
|
||||||
setPendingMcpAction(serverName, action)
|
setPendingMcpAction(serverName, action)
|
||||||
@@ -95,7 +88,7 @@ const InstanceServiceStatus: Component<InstanceServiceStatusProps> = (props) =>
|
|||||||
} else {
|
} else {
|
||||||
await client.mcp.disconnect({ path: { name: serverName } })
|
await client.mcp.disconnect({ path: { name: serverName } })
|
||||||
}
|
}
|
||||||
await refreshMcpStatus()
|
await refreshMetadata()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Failed to toggle MCP server", { serverName, action, error })
|
log.error("Failed to toggle MCP server", { serverName, action, error })
|
||||||
} finally {
|
} finally {
|
||||||
@@ -117,8 +110,8 @@ const InstanceServiceStatus: Component<InstanceServiceStatusProps> = (props) =>
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show
|
<Show
|
||||||
when={!isLoading() && lspServers().length > 0}
|
when={!isLspLoading() && lspServers().length > 0}
|
||||||
fallback={renderEmptyState(isLoading() ? "Loading server status..." : "No LSP servers detected.")}
|
fallback={renderEmptyState(isLspLoading() ? "Loading LSP servers..." : "No LSP servers detected.")}
|
||||||
>
|
>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<For each={lspServers()}>
|
<For each={lspServers()}>
|
||||||
@@ -152,8 +145,8 @@ const InstanceServiceStatus: Component<InstanceServiceStatusProps> = (props) =>
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show
|
<Show
|
||||||
when={!isLoading() && mcpServers().length > 0}
|
when={!isMcpLoading() && mcpServers().length > 0}
|
||||||
fallback={renderEmptyState(isLoading() ? "Loading server status..." : "No MCP servers detected.")}
|
fallback={renderEmptyState(isMcpLoading() ? "Loading MCP servers..." : "No MCP servers detected.")}
|
||||||
>
|
>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<For each={mcpServers()}>
|
<For each={mcpServers()}>
|
||||||
@@ -161,7 +154,7 @@ const InstanceServiceStatus: Component<InstanceServiceStatusProps> = (props) =>
|
|||||||
const pendingAction = () => pendingMcpActions()[server.name]
|
const pendingAction = () => pendingMcpActions()[server.name]
|
||||||
const isPending = () => Boolean(pendingAction())
|
const isPending = () => Boolean(pendingAction())
|
||||||
const isRunning = () => server.status === "running"
|
const isRunning = () => server.status === "running"
|
||||||
const switchDisabled = () => isPending() || !instance()?.client
|
const switchDisabled = () => isPending() || !instance().client
|
||||||
const statusDotClass = () => {
|
const statusDotClass = () => {
|
||||||
if (isPending()) return "status-dot animate-pulse"
|
if (isPending()) return "status-dot animate-pulse"
|
||||||
if (server.status === "running") return "status-dot ready animate-pulse"
|
if (server.status === "running") return "status-dot ready animate-pulse"
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
|
|
||||||
const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instance.id))
|
const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instance.id))
|
||||||
|
|
||||||
|
|
||||||
const desktopQuery = useMediaQuery("(min-width: 1280px)")
|
const desktopQuery = useMediaQuery("(min-width: 1280px)")
|
||||||
|
|
||||||
const tabletQuery = useMediaQuery("(min-width: 768px)")
|
const tabletQuery = useMediaQuery("(min-width: 768px)")
|
||||||
@@ -860,7 +859,6 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
label: "LSP Servers",
|
label: "LSP Servers",
|
||||||
render: () => (
|
render: () => (
|
||||||
<InstanceServiceStatus
|
<InstanceServiceStatus
|
||||||
instanceId={props.instance.id}
|
|
||||||
initialInstance={props.instance}
|
initialInstance={props.instance}
|
||||||
sections={["lsp"]}
|
sections={["lsp"]}
|
||||||
showSectionHeadings={false}
|
showSectionHeadings={false}
|
||||||
@@ -873,7 +871,6 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
label: "MCP Servers",
|
label: "MCP Servers",
|
||||||
render: () => (
|
render: () => (
|
||||||
<InstanceServiceStatus
|
<InstanceServiceStatus
|
||||||
instanceId={props.instance.id}
|
|
||||||
initialInstance={props.instance}
|
initialInstance={props.instance}
|
||||||
sections={["mcp"]}
|
sections={["mcp"]}
|
||||||
showSectionHeadings={false}
|
showSectionHeadings={false}
|
||||||
|
|||||||
72
packages/ui/src/lib/contexts/instance-metadata-context.tsx
Normal file
72
packages/ui/src/lib/contexts/instance-metadata-context.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { Component, JSX, createContext, createEffect, createMemo, createSignal, useContext, type Accessor } from "solid-js"
|
||||||
|
import type { Instance } from "../../types/instance"
|
||||||
|
import { instances } from "../../stores/instances"
|
||||||
|
import { loadInstanceMetadata, hasMetadataLoaded } from "../hooks/use-instance-metadata"
|
||||||
|
|
||||||
|
interface InstanceMetadataContextValue {
|
||||||
|
isLoading: Accessor<boolean>
|
||||||
|
instance: Accessor<Instance>
|
||||||
|
metadata: Accessor<Instance["metadata"] | undefined>
|
||||||
|
refreshMetadata: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const InstanceMetadataContext = createContext<InstanceMetadataContextValue | null>(null)
|
||||||
|
|
||||||
|
interface InstanceMetadataProviderProps {
|
||||||
|
instance: Instance
|
||||||
|
children: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InstanceMetadataProvider: Component<InstanceMetadataProviderProps> = (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 (
|
||||||
|
<InstanceMetadataContext.Provider value={contextValue}>
|
||||||
|
{props.children}
|
||||||
|
</InstanceMetadataContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js"
|
|
||||||
import type { Instance, RawMcpStatus } from "../../types/instance"
|
import type { Instance, RawMcpStatus } from "../../types/instance"
|
||||||
import { fetchLspStatus, updateInstance } from "../../stores/instances"
|
import { fetchLspStatus, updateInstance } from "../../stores/instances"
|
||||||
import { getLogger } from "../../lib/logger"
|
import { getLogger } from "../../lib/logger"
|
||||||
@@ -6,87 +5,66 @@ import { getLogger } from "../../lib/logger"
|
|||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
const pendingMetadataRequests = new Set<string>()
|
const pendingMetadataRequests = new Set<string>()
|
||||||
|
|
||||||
export function useInstanceMetadata(instanceAccessor: Accessor<Instance | undefined>) {
|
function hasMetadataLoaded(metadata?: Instance["metadata"]): boolean {
|
||||||
const [isLoading, setIsLoading] = createSignal(true)
|
if (!metadata) return false
|
||||||
|
return "project" in metadata && "mcpStatus" in metadata && "lspStatus" in metadata
|
||||||
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
export async function loadInstanceMetadata(instance: Instance, options?: { force?: boolean }): Promise<void> {
|
||||||
const instance = instanceAccessor()
|
const client = instance.client
|
||||||
if (!instance) {
|
if (!client) {
|
||||||
setIsLoading(false)
|
log.warn("[metadata] Skipping fetch; client missing", { instanceId: instance.id })
|
||||||
return
|
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
|
if (projectResult.status === "fulfilled") {
|
||||||
const client = instance.client
|
nextMetadata.project = project ?? undefined
|
||||||
const hasMetadata = Boolean(instance.metadata)
|
|
||||||
|
|
||||||
if (!client) {
|
|
||||||
setIsLoading(false)
|
|
||||||
pendingMetadataRequests.delete(instanceId)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasMetadata) {
|
if (mcpResult.status === "fulfilled") {
|
||||||
setIsLoading(false)
|
nextMetadata.mcpStatus = mcpStatus ?? nextMetadata.mcpStatus ?? {}
|
||||||
pendingMetadataRequests.delete(instanceId)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingMetadataRequests.has(instanceId)) {
|
if (lspResult.status === "fulfilled") {
|
||||||
setIsLoading(true)
|
nextMetadata.lspStatus = lspStatus ?? []
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let cancelled = false
|
if (!nextMetadata?.version && instance.binaryVersion) {
|
||||||
pendingMetadataRequests.add(instanceId)
|
nextMetadata.version = instance.binaryVersion
|
||||||
setIsLoading(true)
|
}
|
||||||
|
|
||||||
void (async () => {
|
updateInstance(instance.id, { metadata: nextMetadata })
|
||||||
try {
|
} catch (error) {
|
||||||
const [projectResult, mcpResult, lspResult] = await Promise.allSettled([
|
log.error("Failed to load instance metadata", error)
|
||||||
client.project.current(),
|
} finally {
|
||||||
client.mcp.status(),
|
pendingMetadataRequests.delete(instance.id)
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { hasMetadataLoaded }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user