diff options
| author | David Hill <[email protected]> | 2026-03-10 18:52:32 +0000 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-10 13:52:32 -0500 |
| commit | f77e5cf8fb9d9babcfc1b3ba046c0ba571489647 (patch) | |
| tree | d4424aac3e3bb6438e2af61050daefbdc2e7702b /packages/ui/src/components/tool-error-card.tsx | |
| parent | e6cdc21f2ddd7264a75eb389693d4d62367ae38c (diff) | |
| download | opencode-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.tsx | 112 |
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> + ) +} |
