Add MCP toggle control
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Component, Show, For, createSignal, createEffect, onCleanup } from "solid-js"
|
import { Component, Show, For, createSignal, createEffect, onCleanup } from "solid-js"
|
||||||
|
import Switch from "@suid/material/Switch"
|
||||||
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"
|
||||||
@@ -49,6 +50,7 @@ const pendingMetadataRequests = new Set<string>()
|
|||||||
|
|
||||||
const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
||||||
const [isLoadingMetadata, setIsLoadingMetadata] = createSignal(true)
|
const [isLoadingMetadata, setIsLoadingMetadata] = createSignal(true)
|
||||||
|
const [pendingMcpActions, setPendingMcpActions] = createSignal<Record<string, "connect" | "disconnect">>({})
|
||||||
|
|
||||||
const metadata = () => props.instance.metadata
|
const metadata = () => props.instance.metadata
|
||||||
const binaryVersion = () => props.instance.binaryVersion || metadata()?.version
|
const binaryVersion = () => props.instance.binaryVersion || metadata()?.version
|
||||||
@@ -58,6 +60,63 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||||||
}
|
}
|
||||||
const lspServers = () => metadata()?.lspStatus ?? []
|
const lspServers = () => metadata()?.lspStatus ?? []
|
||||||
|
|
||||||
|
const setPendingMcpAction = (name: string, action?: "connect" | "disconnect") => {
|
||||||
|
setPendingMcpActions((prev) => {
|
||||||
|
const next = { ...prev }
|
||||||
|
if (action) {
|
||||||
|
next[name] = action
|
||||||
|
} else {
|
||||||
|
delete next[name]
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshMcpStatus = async () => {
|
||||||
|
const client = props.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.instance.id, {
|
||||||
|
metadata: {
|
||||||
|
...(props.instance.metadata ?? {}),
|
||||||
|
mcpStatus: status,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to refresh MCP status", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMcpServer = async (serverName: string, shouldEnable: boolean) => {
|
||||||
|
const client = props.instance.client
|
||||||
|
if (!client?.mcp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const action: "connect" | "disconnect" = shouldEnable ? "connect" : "disconnect"
|
||||||
|
setPendingMcpAction(serverName, action)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (shouldEnable) {
|
||||||
|
await client.mcp.connect({ path: { name: serverName } })
|
||||||
|
} else {
|
||||||
|
await client.mcp.disconnect({ path: { name: serverName } })
|
||||||
|
}
|
||||||
|
await refreshMcpStatus()
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to toggle MCP server", { serverName, action, error })
|
||||||
|
} finally {
|
||||||
|
setPendingMcpAction(serverName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const instance = props.instance
|
const instance = props.instance
|
||||||
const instanceId = instance.id
|
const instanceId = instance.id
|
||||||
@@ -257,40 +316,70 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<For each={mcpServers()}>
|
<For each={mcpServers()}>
|
||||||
{(server) => (
|
{(server) => {
|
||||||
<div class="px-2 py-1.5 rounded border bg-surface-secondary border-base">
|
const pendingAction = pendingMcpActions()[server.name]
|
||||||
<div class="flex items-center justify-between gap-2">
|
const isPending = Boolean(pendingAction)
|
||||||
<span class="text-xs text-primary font-medium truncate">{server.name}</span>
|
const isRunning = server.status === "running"
|
||||||
<div class="flex items-center gap-1.5 flex-shrink-0 text-xs text-secondary">
|
const switchDisabled = isPending || !props.instance.client
|
||||||
<div
|
|
||||||
class={`status-dot ${
|
const statusDotClass = () => {
|
||||||
server.status === "running"
|
if (isPending) {
|
||||||
? "ready animate-pulse"
|
return "status-dot animate-pulse"
|
||||||
: server.status === "error"
|
}
|
||||||
? "error"
|
if (server.status === "running") {
|
||||||
: "stopped"
|
return "status-dot ready animate-pulse"
|
||||||
}`}
|
}
|
||||||
/>
|
if (server.status === "error") {
|
||||||
<span>
|
return "status-dot error"
|
||||||
{
|
}
|
||||||
server.status === "running"
|
return "status-dot stopped"
|
||||||
? "Connected"
|
}
|
||||||
: server.status === "error"
|
|
||||||
? "Error"
|
const statusDotStyle = () => (isPending ? { background: "var(--status-warning)" } : undefined)
|
||||||
: "Disabled"
|
|
||||||
}
|
return (
|
||||||
</span>
|
<div class="px-2 py-1.5 rounded border bg-surface-secondary border-base">
|
||||||
</div>
|
<div class="flex items-center justify-between gap-2">
|
||||||
</div>
|
<span class="text-xs text-primary font-medium truncate">{server.name}</span>
|
||||||
<Show when={server.error}>
|
<div class="flex items-center gap-3 flex-shrink-0">
|
||||||
{(error) => (
|
<div class="flex items-center gap-1.5 text-xs text-secondary">
|
||||||
<div class="text-[11px] mt-1 break-words" style={{ color: "var(--status-error)" }}>
|
<div class={statusDotClass()} style={statusDotStyle()} />
|
||||||
{error()}
|
</div>
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<Switch
|
||||||
|
checked={isRunning}
|
||||||
|
disabled={switchDisabled}
|
||||||
|
color="success"
|
||||||
|
size="small"
|
||||||
|
inputProps={{ "aria-label": `Toggle ${server.name} MCP server` }}
|
||||||
|
onChange={(_, checked) => {
|
||||||
|
if (switchDisabled) return
|
||||||
|
void toggleMcpServer(server.name, Boolean(checked))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Show when={isPending}>
|
||||||
|
<svg class="animate-spin h-3 w-3" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</Show>
|
<Show when={server.error}>
|
||||||
</div>
|
{(error) => (
|
||||||
)}
|
<div class="text-[11px] mt-1 break-words" style={{ color: "var(--status-error)" }}>
|
||||||
|
{error()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user