summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src/components/tool-error-card.tsx
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2026-03-10 18:52:32 +0000
committerGitHub <[email protected]>2026-03-10 13:52:32 -0500
commitf77e5cf8fb9d9babcfc1b3ba046c0ba571489647 (patch)
treed4424aac3e3bb6438e2af61050daefbdc2e7702b /packages/ui/src/components/tool-error-card.tsx
parente6cdc21f2ddd7264a75eb389693d4d62367ae38c (diff)
downloadopencode-f77e5cf8fb9d9babcfc1b3ba046c0ba571489647.tar.gz
opencode-f77e5cf8fb9d9babcfc1b3ba046c0ba571489647.zip
feat(ui): restyle Card and improve tool error cards (#16888)
Co-authored-by: Adam <[email protected]>
Diffstat (limited to 'packages/ui/src/components/tool-error-card.tsx')
-rw-r--r--packages/ui/src/components/tool-error-card.tsx112
1 files changed, 112 insertions, 0 deletions
diff --git a/packages/ui/src/components/tool-error-card.tsx b/packages/ui/src/components/tool-error-card.tsx
new file mode 100644
index 000000000..13873ae90
--- /dev/null
+++ b/packages/ui/src/components/tool-error-card.tsx
@@ -0,0 +1,112 @@
+import { type ComponentProps, createMemo, createSignal, Show, splitProps } from "solid-js"
+import { Card, CardDescription } from "./card"
+import { Collapsible } from "./collapsible"
+import { Icon } from "./icon"
+import { IconButton } from "./icon-button"
+import { Tooltip } from "./tooltip"
+import { useI18n } from "../context/i18n"
+
+export interface ToolErrorCardProps extends Omit<ComponentProps<typeof Card>, "children" | "variant"> {
+ tool: string
+ error: string
+}
+
+export function ToolErrorCard(props: ToolErrorCardProps) {
+ const i18n = useI18n()
+ const [open, setOpen] = createSignal(true)
+ const [copied, setCopied] = createSignal(false)
+ const [split, rest] = splitProps(props, ["tool", "error"])
+ const name = createMemo(() => {
+ const map: Record<string, string> = {
+ read: "ui.tool.read",
+ list: "ui.tool.list",
+ glob: "ui.tool.glob",
+ grep: "ui.tool.grep",
+ webfetch: "ui.tool.webfetch",
+ websearch: "ui.tool.websearch",
+ codesearch: "ui.tool.codesearch",
+ bash: "ui.tool.shell",
+ apply_patch: "ui.tool.patch",
+ question: "ui.tool.questions",
+ }
+ const key = map[split.tool]
+ if (!key) return split.tool
+ return i18n.t(key)
+ })
+ const cleaned = createMemo(() => split.error.replace(/^Error:\s*/, "").trim())
+ const tail = createMemo(() => {
+ const value = cleaned()
+ const prefix = `${split.tool} `
+ if (value.startsWith(prefix)) return value.slice(prefix.length)
+ return value
+ })
+
+ const subtitle = createMemo(() => {
+ const parts = tail().split(": ")
+ if (parts.length <= 1) return "Failed"
+ const head = (parts[0] ?? "").trim()
+ if (!head) return "Failed"
+ return head[0] ? head[0].toUpperCase() + head.slice(1) : "Failed"
+ })
+
+ const body = createMemo(() => {
+ const parts = tail().split(": ")
+ if (parts.length <= 1) return cleaned()
+ return parts.slice(1).join(": ").trim() || cleaned()
+ })
+
+ const copy = async () => {
+ const text = cleaned()
+ if (!text) return
+ await navigator.clipboard.writeText(text)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+
+ return (
+ <Card {...rest} data-kind="tool-error-card" data-open={open() ? "true" : "false"} variant="error">
+ <Collapsible class="tool-collapsible" data-open={open() ? "true" : "false"} open={open()} onOpenChange={setOpen}>
+ <Collapsible.Trigger>
+ <div data-component="tool-trigger">
+ <div data-slot="basic-tool-tool-trigger-content">
+ <span data-slot="basic-tool-tool-indicator" data-component="tool-error-card-icon">
+ <Icon name="circle-ban-sign" size="small" style={{ "stroke-width": 1.5 }} />
+ </span>
+ <div data-slot="basic-tool-tool-info">
+ <div data-slot="basic-tool-tool-info-structured">
+ <div data-slot="basic-tool-tool-info-main">
+ <span data-slot="basic-tool-tool-title">{name()}</span>
+ <span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <Collapsible.Arrow />
+ </div>
+ </Collapsible.Trigger>
+ <Collapsible.Content>
+ <div data-slot="tool-error-card-content">
+ <Show when={open()}>
+ <div data-slot="tool-error-card-copy">
+ <Tooltip value={copied() ? i18n.t("ui.message.copied") : "Copy error"} placement="top" gutter={4}>
+ <IconButton
+ icon={copied() ? "check" : "copy"}
+ size="normal"
+ variant="ghost"
+ onMouseDown={(e) => e.preventDefault()}
+ onClick={(e) => {
+ e.stopPropagation()
+ copy()
+ }}
+ aria-label={copied() ? i18n.t("ui.message.copied") : "Copy error"}
+ />
+ </Tooltip>
+ </div>
+ </Show>
+ <Show when={body()}>{(value) => <CardDescription>{value()}</CardDescription>}</Show>
+ </div>
+ </Collapsible.Content>
+ </Collapsible>
+ </Card>
+ )
+}