From 35ed09ff37ec23c5b8dd62de41af76f8d6c0999b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 3 Jul 2026 22:26:54 -0400 Subject: [PATCH] fix(tui): improve MCP error details (#35263) --- packages/tui/src/app.tsx | 4 +- packages/tui/src/component/dialog-mcp.tsx | 132 +++++++++++++++++----- 2 files changed, 108 insertions(+), 28 deletions(-) diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index d5cf93c2c7..a3b2c31973 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -409,8 +409,8 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi else toast.show({ variant: "error", - title: "MCP server failed to connect", - message: `${server.name}: ${status.error}`, + title: `MCP server failed: ${server.name}`, + message: "Open MCPs to view details.", }) } }) diff --git a/packages/tui/src/component/dialog-mcp.tsx b/packages/tui/src/component/dialog-mcp.tsx index 004e4a6e15..003c207be0 100644 --- a/packages/tui/src/component/dialog-mcp.tsx +++ b/packages/tui/src/component/dialog-mcp.tsx @@ -1,12 +1,17 @@ import { createEffect, createMemo, createSignal, onMount, Show } from "solid-js" -import { createStore } from "solid-js/store" import { useData } from "../context/data" import { pipe, sortBy } from "remeda" -import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select" +import { DialogSelect } from "../ui/dialog-select" import { useDialog } from "../ui/dialog" import { useTheme, type Theme } from "../context/theme" -import { TextAttributes } from "@opentui/core" +import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core" import type { McpServer } from "@opencode-ai/sdk/v2" +import { useClipboard } from "../context/clipboard" +import { useToast } from "../ui/toast" +import { useKeyboard, useTerminalDimensions } from "@opentui/solid" +import { useTuiConfig } from "../config" +import { getScrollAcceleration } from "../util/scroll" +import { useBindings } from "../keymap" // Sort by how much attention a server needs: auth prompts first, then failures, // then healthy servers, and intentionally-off servers last. @@ -31,9 +36,8 @@ export function DialogMcp() { const data = useData() const dialog = useDialog() const { theme } = useTheme() - const [expanded, setExpanded] = createStore>({}) const [focused, setFocused] = createSignal() - const [, setRef] = createSignal>() + const [detail, setDetail] = createSignal() onMount(() => { dialog.setSize("large") @@ -66,9 +70,6 @@ export function DialogMcp() { {meta.icon} {meta.label} ), - details: meta.error && expanded[server.name] ? [meta.error] : undefined, - detailsColor: theme.error, - detailsWrap: true, } }), ) @@ -79,24 +80,103 @@ export function DialogMcp() { return server ? statusMeta(server.status, theme).error : undefined }) + const open = (name: string | undefined) => { + const server = servers().find((entry) => entry.name === name) + if (!server || !statusMeta(server.status, theme).error) return + setDetail(server) + } + return ( - setFocused(option.value as string)} - onSelect={(option) => { - const name = option.value as string - const server = servers().find((entry) => entry.name === name) - if (!server || !statusMeta(server.status, theme).error) return - setExpanded(name, (open) => !open) - }} - footer={ - - enter to {expanded[focused()!] ? "hide" : "view"} error - - } - /> + + setFocused(option.value as string)} + onSelect={(option) => open(option.value as string)} + footer={ + + enter to view error + + } + /> + } + > + {(server) => setDetail()} />} + + + ) +} + +function DialogMcpError(props: { server: McpServer; onBack: () => void }) { + const dialog = useDialog() + const clipboard = useClipboard() + const toast = useToast() + const { theme } = useTheme() + const dimensions = useTerminalDimensions() + const tuiConfig = useTuiConfig() + const [copied, setCopied] = createSignal(false) + const error = () => statusMeta(props.server.status, theme).error ?? "Unknown MCP connection error" + const height = createMemo(() => Math.max(3, Math.floor(dimensions().height / 2) - 5)) + let scroll: ScrollBoxRenderable | undefined + + onMount(() => dialog.setSize("large")) + + const copy = () => { + if (!clipboard.write) return + void clipboard + .write(error()) + .then(() => setCopied(true)) + .catch(toast.error) + } + + useBindings(() => ({ + bindings: [{ key: "escape", desc: "Back to MCP servers", group: "Dialog", cmd: props.onBack }], + })) + + useKeyboard((event) => { + if (event.name === "c") return copy() + if (event.name === "up") return scroll?.scrollBy(-1) + if (event.name === "down") return scroll?.scrollBy(1) + if (event.name === "pageup") return scroll?.scrollBy(-height()) + if (event.name === "pagedown") return scroll?.scrollBy(height()) + if (event.name === "home") return scroll?.scrollTo(0) + if (event.name === "end" && scroll) return scroll.scrollTo(scroll.scrollHeight) + }) + + return ( + + + + MCP / {props.server.name} + + + esc back + + + ✗ Failed + + (scroll = element)} + height={height()} + scrollbarOptions={{ visible: false }} + scrollAcceleration={getScrollAcceleration(tuiConfig)} + > + + {error()} + + + + + ↑↓ scroll + + {copied() ? "✓ copied" : "c copy details"} + + + ) }