diff options
| author | Adam Malczewski <[email protected]> | 2026-06-25 10:55:51 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-25 10:55:51 +0900 |
| commit | 38db3827870960f466be89afbc49f91238d46144 (patch) | |
| tree | 24cb1b896dfadc31e72552dbe67f00530881242e /src/features/mcp/ui | |
| parent | 17ce47987e673b6618454d033885b17b2a01912e (diff) | |
| download | dispatch-web-38db3827870960f466be89afbc49f91238d46144.tar.gz dispatch-web-38db3827870960f466be89afbc49f91238d46144.zip | |
feat: workspaces shell + cwd-lsp rename + mcp/settings/system-prompt features + app wiring
- workspaces: URL-driven conversation grouping (home listing at /, routing,
store, http adapter, WorkspaceCard) wired into the App.svelte shell
- rename features/workspace -> features/cwd-lsp (the cwd/lsp status feature)
- new features: mcp (status view), settings (chat-limit field), system-prompt
(prompt builder), all rendered via the generic surface host
- chat: store + ChatView updates
- tabs: tabs-store updates
- app wiring: ErrorModal (full-screen error surface), app/App.svelte + store.svelte
This commit makes HEAD typecheck clean for the first time: the prior HEAD
(c95cc77) imported features/settings from app/App.svelte but never committed
the feature, so only the full working tree was green.
Diffstat (limited to 'src/features/mcp/ui')
| -rw-r--r-- | src/features/mcp/ui/McpStatusView.svelte | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/src/features/mcp/ui/McpStatusView.svelte b/src/features/mcp/ui/McpStatusView.svelte new file mode 100644 index 0000000..9ac40ba --- /dev/null +++ b/src/features/mcp/ui/McpStatusView.svelte @@ -0,0 +1,131 @@ +<script lang="ts"> + import { untrack } from "svelte"; + import { + type Badge, + type LoadMcpStatus, + type McpServerView, + summarizeMcpServers, + viewMcpServers, + } from "../logic/view-model"; + + let { + cwd, + canView, + load, + }: { + /** The active conversation's cwd — the trigger to (re)load when it changes. */ + cwd: string | null; + /** Whether a real conversation is focused. */ + canView: boolean; + load: LoadMcpStatus; + } = $props(); + + const badgeClass: Record<Badge, string> = { + success: "badge-success", + warning: "badge-warning", + error: "badge-error", + neutral: "badge-ghost", + }; + + let servers = $state<readonly McpServerView[]>([]); + let loading = $state(false); + let error = $state<string | null>(null); + let loadedCwd = $state<string | null>(null); + let hasLoaded = $state(false); + let summary = $state(""); + + async function refresh() { + if (!canView) return; + loading = true; + error = null; + const result = await load(); + loading = false; + if (result === null) return; + hasLoaded = true; + if (result.ok) { + servers = viewMcpServers(result.servers); + summary = summarizeMcpServers(result.servers); + loadedCwd = result.cwd; + } else { + error = result.error; + } + } + + // (Re)load on mount and whenever the conversation's cwd changes. The MCP GET + // lazily spawns/connects servers, so we avoid a redundant fetch when `cwd` + // resolves to the value we already loaded for. + $effect(() => { + const target = cwd; + const can = canView; + untrack(() => { + if (!can) return; + if (!hasLoaded || target !== loadedCwd) void refresh(); + }); + }); +</script> + +<div class="flex flex-col gap-2"> + <div class="flex items-center justify-between gap-2"> + <span class="text-xs opacity-70"> + {#if loading} + Resolving… + {:else if hasLoaded && loadedCwd !== null} + {summary} + {:else} + MCP servers + {/if} + </span> + <button + type="button" + class="btn btn-ghost btn-xs" + disabled={!canView || loading} + onclick={() => refresh()} + aria-label="Refresh MCP server status" + > + {#if loading} + <span class="loading loading-spinner loading-xs"></span> + {:else} + Refresh + {/if} + </button> + </div> + + {#if !canView} + <p class="text-xs opacity-60">Open or start a conversation to see its MCP servers.</p> + {:else if error} + <p class="text-xs text-error">{error}</p> + {:else if hasLoaded && loadedCwd === null} + <p class="text-xs opacity-60"> + Set a working directory in the Model panel to enable MCP servers. + </p> + {:else if hasLoaded && servers.length === 0 && !loading} + <p class="text-xs opacity-60">No MCP servers configured for this directory.</p> + {:else} + <ul class="flex flex-col gap-2"> + {#each servers as server (server.id)} + <li class="flex flex-col gap-1 rounded-box bg-base-200 p-2 text-sm"> + <div class="flex items-center justify-between gap-2"> + <span class="font-medium font-mono">{server.id}</span> + <span class="badge badge-sm {badgeClass[server.badge]} gap-1"> + {#if server.busy} + <span class="loading loading-spinner loading-xs"></span> + {/if} + {server.statusLabel} + </span> + </div> + <div class="flex items-center justify-between gap-2 text-xs opacity-60"> + {#if server.configSource} + <span class="font-mono" title="Config source">{server.configSource}</span> + {:else} + <span></span> + {/if} + <span title="Discovered tools">{server.toolCount} tool{server.toolCount === 1 ? "" : "s"}</span> + </div> + {#if server.error} + <span class="font-mono text-xs text-error">{server.error}</span> + {/if} + </li> + {/each} + </ul> + {/if} +</div> |
