summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'packages/app/src/components')
-rw-r--r--packages/app/src/components/dialog-select-mcp.tsx91
-rw-r--r--packages/app/src/components/session-lsp-indicator.tsx40
-rw-r--r--packages/app/src/components/session-mcp-indicator.tsx36
-rw-r--r--packages/app/src/components/status-bar.tsx14
4 files changed, 181 insertions, 0 deletions
diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx
new file mode 100644
index 000000000..c29cd827e
--- /dev/null
+++ b/packages/app/src/components/dialog-select-mcp.tsx
@@ -0,0 +1,91 @@
+import { Component, createMemo, createSignal, Show } from "solid-js"
+import { useSync } from "@/context/sync"
+import { useSDK } from "@/context/sdk"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { List } from "@opencode-ai/ui/list"
+import { Switch } from "@opencode-ai/ui/switch"
+
+export const DialogSelectMcp: Component = () => {
+ const sync = useSync()
+ const sdk = useSDK()
+ const [loading, setLoading] = createSignal<string | null>(null)
+
+ const items = createMemo(() =>
+ Object.entries(sync.data.mcp ?? {})
+ .map(([name, status]) => ({ name, status: status.status }))
+ .sort((a, b) => a.name.localeCompare(b.name)),
+ )
+
+ const toggle = async (name: string) => {
+ if (loading()) return
+ setLoading(name)
+ const status = sync.data.mcp[name]
+ if (status?.status === "connected") {
+ await sdk.client.mcp.disconnect({ name })
+ } else {
+ await sdk.client.mcp.connect({ name })
+ }
+ const result = await sdk.client.mcp.status()
+ if (result.data) sync.set("mcp", result.data)
+ setLoading(null)
+ }
+
+ const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
+ const totalCount = createMemo(() => items().length)
+
+ return (
+ <Dialog title="MCPs" description={`${enabledCount()} of ${totalCount()} enabled`}>
+ <List
+ search={{ placeholder: "Search", autofocus: true }}
+ emptyMessage="No MCPs configured"
+ key={(x) => x?.name ?? ""}
+ items={items}
+ filterKeys={["name", "status"]}
+ sortBy={(a, b) => a.name.localeCompare(b.name)}
+ onSelect={(x) => {
+ if (x) toggle(x.name)
+ }}
+ >
+ {(i) => {
+ const mcpStatus = () => sync.data.mcp[i.name]
+ const status = () => mcpStatus()?.status
+ const error = () => {
+ const s = mcpStatus()
+ return s?.status === "failed" ? s.error : undefined
+ }
+ const enabled = () => status() === "connected"
+ return (
+ <div class="w-full flex items-center justify-between gap-x-3">
+ <div class="flex flex-col gap-0.5 min-w-0">
+ <div class="flex items-center gap-2">
+ <span class="truncate">{i.name}</span>
+ <Show when={status() === "connected"}>
+ <span class="text-11-regular text-text-weaker">connected</span>
+ </Show>
+ <Show when={status() === "failed"}>
+ <span class="text-11-regular text-text-weaker">failed</span>
+ </Show>
+ <Show when={status() === "needs_auth"}>
+ <span class="text-11-regular text-text-weaker">needs auth</span>
+ </Show>
+ <Show when={status() === "disabled"}>
+ <span class="text-11-regular text-text-weaker">disabled</span>
+ </Show>
+ <Show when={loading() === i.name}>
+ <span class="text-11-regular text-text-weak">...</span>
+ </Show>
+ </div>
+ <Show when={error()}>
+ <span class="text-11-regular text-text-weaker truncate">{error()}</span>
+ </Show>
+ </div>
+ <div onClick={(e) => e.stopPropagation()}>
+ <Switch checked={enabled()} disabled={loading() === i.name} onChange={() => toggle(i.name)} />
+ </div>
+ </div>
+ )
+ }}
+ </List>
+ </Dialog>
+ )
+}
diff --git a/packages/app/src/components/session-lsp-indicator.tsx b/packages/app/src/components/session-lsp-indicator.tsx
new file mode 100644
index 000000000..98d6d6dfd
--- /dev/null
+++ b/packages/app/src/components/session-lsp-indicator.tsx
@@ -0,0 +1,40 @@
+import { createMemo, Show } from "solid-js"
+import { Icon } from "@opencode-ai/ui/icon"
+import { useSync } from "@/context/sync"
+import { Tooltip } from "@opencode-ai/ui/tooltip"
+
+export function SessionLspIndicator() {
+ const sync = useSync()
+
+ const lspStats = createMemo(() => {
+ const lsp = sync.data.lsp ?? []
+ const connected = lsp.filter((s) => s.status === "connected").length
+ const hasError = lsp.some((s) => s.status === "error")
+ const total = lsp.length
+ return { connected, hasError, total }
+ })
+
+ const tooltipContent = createMemo(() => {
+ const lsp = sync.data.lsp ?? []
+ if (lsp.length === 0) return "No LSP servers"
+ return lsp.map((s) => s.name).join(", ")
+ })
+
+ return (
+ <Show when={lspStats().total > 0}>
+ <Tooltip placement="top" value={tooltipContent()}>
+ <div class="flex items-center gap-1 px-2 cursor-default select-none">
+ <Icon
+ name="code"
+ size="small"
+ classList={{
+ "text-icon-critical-base": lspStats().hasError,
+ "text-icon-success-base": !lspStats().hasError && lspStats().connected > 0,
+ }}
+ />
+ <span class="text-12-regular text-text-weak">{lspStats().connected} LSP</span>
+ </div>
+ </Tooltip>
+ </Show>
+ )
+}
diff --git a/packages/app/src/components/session-mcp-indicator.tsx b/packages/app/src/components/session-mcp-indicator.tsx
new file mode 100644
index 000000000..17a6f2e1a
--- /dev/null
+++ b/packages/app/src/components/session-mcp-indicator.tsx
@@ -0,0 +1,36 @@
+import { createMemo, Show } from "solid-js"
+import { Button } from "@opencode-ai/ui/button"
+import { Icon } from "@opencode-ai/ui/icon"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { useSync } from "@/context/sync"
+import { DialogSelectMcp } from "@/components/dialog-select-mcp"
+
+export function SessionMcpIndicator() {
+ const sync = useSync()
+ const dialog = useDialog()
+
+ const mcpStats = createMemo(() => {
+ const mcp = sync.data.mcp ?? {}
+ const entries = Object.entries(mcp)
+ const enabled = entries.filter(([, status]) => status.status === "connected").length
+ const failed = entries.some(([, status]) => status.status === "failed")
+ const total = entries.length
+ return { enabled, failed, total }
+ })
+
+ return (
+ <Show when={mcpStats().total > 0}>
+ <Button variant="ghost" onClick={() => dialog.show(() => <DialogSelectMcp />)}>
+ <Icon
+ name="mcp"
+ size="small"
+ classList={{
+ "text-icon-critical-base": mcpStats().failed,
+ "text-icon-success-base": !mcpStats().failed && mcpStats().enabled > 0,
+ }}
+ />
+ <span class="text-12-regular text-text-weak">{mcpStats().enabled} MCP</span>
+ </Button>
+ </Show>
+ )
+}
diff --git a/packages/app/src/components/status-bar.tsx b/packages/app/src/components/status-bar.tsx
new file mode 100644
index 000000000..e0e25c60b
--- /dev/null
+++ b/packages/app/src/components/status-bar.tsx
@@ -0,0 +1,14 @@
+import { Show, type ParentProps } from "solid-js"
+import { usePlatform } from "@/context/platform"
+
+export function StatusBar(props: ParentProps) {
+ const platform = usePlatform()
+ return (
+ <div class="h-8 w-full shrink-0 flex items-center justify-between px-2 border-t border-border-weak-base bg-background-base">
+ <Show when={platform.version}>
+ <span class="text-12-regular text-text-weak">v{platform.version}</span>
+ </Show>
+ <div class="flex items-center">{props.children}</div>
+ </div>
+ )
+}