fix(tui): improve MCP error details (#35263)
This commit is contained in:
parent
9751615651
commit
35ed09ff37
2 changed files with 108 additions and 28 deletions
|
|
@ -409,8 +409,8 @@ function App(props: { onSnapshot?: () => Promise<string[]>; 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.",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<Record<string, boolean>>({})
|
||||
const [focused, setFocused] = createSignal<string>()
|
||||
const [, setRef] = createSignal<DialogSelectRef<unknown>>()
|
||||
const [detail, setDetail] = createSignal<McpServer>()
|
||||
|
||||
onMount(() => {
|
||||
dialog.setSize("large")
|
||||
|
|
@ -66,9 +70,6 @@ export function DialogMcp() {
|
|||
{meta.icon} {meta.label}
|
||||
</span>
|
||||
),
|
||||
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 (
|
||||
<DialogSelect
|
||||
ref={setRef}
|
||||
title="MCPs"
|
||||
options={options()}
|
||||
preserveSelection
|
||||
onMove={(option) => 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={
|
||||
<Show when={focusedError()}>
|
||||
<text fg={theme.textMuted}>enter to {expanded[focused()!] ? "hide" : "view"} error</text>
|
||||
</Show>
|
||||
}
|
||||
/>
|
||||
<box>
|
||||
<Show
|
||||
when={detail()}
|
||||
fallback={
|
||||
<DialogSelect
|
||||
title="MCPs"
|
||||
options={options()}
|
||||
current={focused()}
|
||||
preserveSelection
|
||||
onMove={(option) => setFocused(option.value as string)}
|
||||
onSelect={(option) => open(option.value as string)}
|
||||
footer={
|
||||
<Show when={focusedError()}>
|
||||
<text fg={theme.textMuted}>enter to view error</text>
|
||||
</Show>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(server) => <DialogMcpError server={server()} onBack={() => setDetail()} />}
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<box paddingLeft={4} paddingRight={4} paddingBottom={1} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||
MCP / {props.server.name}
|
||||
</text>
|
||||
<text fg={theme.textMuted} onMouseUp={props.onBack}>
|
||||
esc back
|
||||
</text>
|
||||
</box>
|
||||
<text fg={theme.error}>✗ Failed</text>
|
||||
<box backgroundColor={theme.backgroundElement} paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
|
||||
<scrollbox
|
||||
ref={(element: ScrollBoxRenderable) => (scroll = element)}
|
||||
height={height()}
|
||||
scrollbarOptions={{ visible: false }}
|
||||
scrollAcceleration={getScrollAcceleration(tuiConfig)}
|
||||
>
|
||||
<text fg={theme.text} wrapMode="word">
|
||||
{error()}
|
||||
</text>
|
||||
</scrollbox>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme.textMuted}>↑↓ scroll</text>
|
||||
<text fg={theme.textMuted} onMouseUp={copy}>
|
||||
{copied() ? "✓ copied" : "c copy details"}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue