summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/app/src')
-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
-rw-r--r--packages/app/src/context/global-sync.tsx10
-rw-r--r--packages/app/src/pages/session.tsx17
6 files changed, 208 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>
+ )
+}
diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx
index 10607b1d2..7a9dc8dc4 100644
--- a/packages/app/src/context/global-sync.tsx
+++ b/packages/app/src/context/global-sync.tsx
@@ -12,6 +12,8 @@ import {
type ProviderListResponse,
type ProviderAuthResponse,
type Command,
+ type McpStatus,
+ type LspStatus,
createOpencodeClient,
} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile } from "solid-js/store"
@@ -41,6 +43,10 @@ type State = {
todo: {
[sessionID: string]: Todo[]
}
+ mcp: {
+ [name: string]: McpStatus
+ }
+ lsp: LspStatus[]
limit: number
message: {
[sessionID: string]: Message[]
@@ -85,6 +91,8 @@ function createGlobalSync() {
session_status: {},
session_diff: {},
todo: {},
+ mcp: {},
+ lsp: [],
limit: 5,
message: {},
part: {},
@@ -149,6 +157,8 @@ function createGlobalSync() {
session: () => loadSessions(directory),
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
+ mcp: () => sdk.mcp.status().then((x) => setStore("mcp", x.data ?? {})),
+ lsp: () => sdk.lsp.status().then((x) => setStore("lsp", x.data ?? [])),
}
await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
.then(() => setStore("ready", true))
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index d7eaccc2a..019cc305c 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -49,6 +49,7 @@ import { checksum } from "@opencode-ai/util/encode"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogSelectFile } from "@/components/dialog-select-file"
import { DialogSelectModel } from "@/components/dialog-select-model"
+import { DialogSelectMcp } from "@/components/dialog-select-mcp"
import { useCommand } from "@/context/command"
import { useNavigate, useParams } from "@solidjs/router"
import { UserMessage } from "@opencode-ai/sdk/v2"
@@ -56,6 +57,9 @@ import { useSDK } from "@/context/sdk"
import { usePrompt } from "@/context/prompt"
import { extractPromptFromParts } from "@/utils/prompt"
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
+import { StatusBar } from "@/components/status-bar"
+import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
+import { SessionLspIndicator } from "@/components/session-lsp-indicator"
export default function Page() {
const layout = useLayout()
@@ -275,6 +279,15 @@ export default function Page() {
onSelect: () => dialog.show(() => <DialogSelectModel />),
},
{
+ id: "mcp.toggle",
+ title: "Toggle MCPs",
+ description: "Toggle MCPs",
+ category: "MCP",
+ keybind: "mod+;",
+ slash: "mcp",
+ onSelect: () => dialog.show(() => <DialogSelectMcp />),
+ },
+ {
id: "agent.cycle",
title: "Cycle agent",
description: "Switch to the next agent",
@@ -921,6 +934,10 @@ export default function Page() {
</DragDropProvider>
</div>
</Show>
+ <StatusBar>
+ <SessionLspIndicator />
+ <SessionMcpIndicator />
+ </StatusBar>
</div>
)
}