From f75829ed5a84bc479d83e6f42de22634b83b249f Mon Sep 17 00:00:00 2001 From: britz Date: Wed, 19 Aug 2026 22:58:15 +0530 Subject: [PATCH] feat(session-ui): display tool input context on error (#43434) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a tool fails, show the relevant input context (command, file path, URL, query, etc.) above the error message so users can immediately see what the tool was trying to do before the failure occurred. This addresses the UX issue where failed tool operations only showed a generic error message without any indication of what was being attempted. Changes: - Add getToolContextLabel() helper to extract relevant input per tool type - Add optional `input` prop to ToolErrorCard - Render context label/value above error body when available - Add i18n keys for all context labels (command, file, url, etc.) - Style the context display with monospace font for the value 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- .../src/components/message-part.tsx | 1 + .../src/components/tool-error-card.css | 44 +++++++++- .../src/components/tool-error-card.tsx | 84 +++++++++++++++++++ packages/ui/src/i18n/en.ts | 8 ++ 4 files changed, 133 insertions(+), 4 deletions(-) diff --git a/packages/session-ui/src/components/message-part.tsx b/packages/session-ui/src/components/message-part.tsx index d2a00b9d44c0..6951ab1e1e7c 100644 --- a/packages/session-ui/src/components/message-part.tsx +++ b/packages/session-ui/src/components/message-part.tsx @@ -1595,6 +1595,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { onOpenChange={props.onToolOpenChange ? handleToolOpenChange : undefined} subtitle={taskSubtitle()} href={taskHref()} + input={input()} onSubtitleClick={(event) => { if (!data.navigateToSession) return if (event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return diff --git a/packages/session-ui/src/components/tool-error-card.css b/packages/session-ui/src/components/tool-error-card.css index 4ff1e3dde9b5..69420c0961ed 100644 --- a/packages/session-ui/src/components/tool-error-card.css +++ b/packages/session-ui/src/components/tool-error-card.css @@ -10,8 +10,7 @@ color: var(--v2-text-text-muted); } - [data-slot="collapsible-arrow"], - [data-slot="collapsible-arrow-icon"] { + [data-slot="collapsible-arrow"], [data-slot="collapsible-arrow-icon"] { color: var(--v2-text-text-faint); } @@ -35,6 +34,32 @@ user-select: text; } + [data-slot="tool-error-card-context"] { + display: flex; + align-items: baseline; + gap: 6px; + margin-bottom: 4px; + font-size: 12px; + line-height: 1.4; + } + + [data-slot="tool-error-card-context-label"] { + color: var(--v2-text-text-muted); + flex-shrink: 0; + font-weight: 500; + } + + [data-slot="tool-error-card-context-value"] { + color: var(--v2-text-text-base); + font-family: var(--v2-font-family-mono); + font-size: 11px; + word-break: break-all; + white-space: pre-wrap; + max-height: 60px; + overflow: hidden; + text-overflow: ellipsis; + } + > [data-component="collapsible"].tool-collapsible[data-open="true"] [data-slot="tool-error-card-content"] { padding-right: 40px; } @@ -49,8 +74,7 @@ will-change: opacity; } - &:hover [data-slot="tool-error-card-copy"], - &:focus-within [data-slot="tool-error-card-copy"] { + &:hover [data-slot="tool-error-card-copy"], &:focus-within [data-slot="tool-error-card-copy"] { opacity: 1; pointer-events: auto; } @@ -83,6 +107,18 @@ body:not([data-new-layout]) [data-component="card"][data-kind="tool-error-card"] color: var(--icon-weaker); } + [data-slot="tool-error-card-context"] { + color: var(--text-base); + } + + [data-slot="tool-error-card-context-label"] { + color: var(--text-weak); + } + + [data-slot="tool-error-card-context-value"] { + color: var(--text-base); + } + [data-slot="tool-error-card-content"] :where(*)::selection, [data-slot="tool-error-card-content"] :where(*)::-moz-selection { background: var(--surface-critical-base); diff --git a/packages/session-ui/src/components/tool-error-card.tsx b/packages/session-ui/src/components/tool-error-card.tsx index 2251641857a6..1ea619ad2249 100644 --- a/packages/session-ui/src/components/tool-error-card.tsx +++ b/packages/session-ui/src/components/tool-error-card.tsx @@ -7,6 +7,74 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" import { useI18n } from "@opencode-ai/ui/context/i18n" +function getToolContextLabel(tool: string, input: Record, t: (key: string) => string): { label: string; value: string } | undefined { + switch (tool) { + case "bash": + case "shell": { + const command = typeof input.command === "string" ? input.command : undefined + if (command) return { label: t("ui.toolErrorCard.context.command"), value: command } + break + } + case "read": { + const filePath = typeof input.filePath === "string" ? input.filePath : undefined + if (filePath) return { label: t("ui.toolErrorCard.context.file"), value: filePath } + break + } + case "edit": { + const filePath = typeof input.filePath === "string" ? input.filePath : undefined + if (filePath) return { label: t("ui.toolErrorCard.context.file"), value: filePath } + break + } + case "write": { + const filePath = typeof input.filePath === "string" ? input.filePath : undefined + if (filePath) return { label: t("ui.toolErrorCard.context.file"), value: filePath } + break + } + case "webfetch": { + const url = typeof input.url === "string" ? input.url : undefined + if (url) return { label: t("ui.toolErrorCard.context.url"), value: url } + break + } + case "websearch": { + const query = typeof input.query === "string" ? input.query : undefined + if (query) return { label: t("ui.toolErrorCard.context.query"), value: query } + break + } + case "glob": { + const pattern = typeof input.pattern === "string" ? input.pattern : undefined + const path = typeof input.path === "string" ? input.path : undefined + if (pattern) return { label: t("ui.toolErrorCard.context.pattern"), value: path ? `${pattern} (${path})` : pattern } + break + } + case "grep": { + const pattern = typeof input.pattern === "string" ? input.pattern : undefined + const path = typeof input.path === "string" ? input.path : undefined + if (pattern) return { label: t("ui.toolErrorCard.context.pattern"), value: path ? `${pattern} (${path})` : pattern } + break + } + case "list": { + const path = typeof input.path === "string" ? input.path : undefined + if (path) return { label: t("ui.toolErrorCard.context.directory"), value: path } + break + } + case "task": { + const description = typeof input.description === "string" ? input.description : undefined + if (description) return { label: t("ui.toolErrorCard.context.task"), value: description } + break + } + case "patch": + case "apply_patch": { + const files = Array.isArray(input.files) ? input.files : undefined + if (files?.length) { + const fileNames = files.map((f: any) => typeof f === "string" ? f : f?.path).filter(Boolean) + if (fileNames.length) return { label: t("ui.toolErrorCard.context.files"), value: fileNames.join(", ") } + } + break + } + } + return undefined +} + export interface ToolErrorCardProps extends Omit, "children" | "variant"> { tool: string error: string @@ -17,6 +85,7 @@ export interface ToolErrorCardProps extends Omit, "c subtitle?: string href?: string onSubtitleClick?: (event: MouseEvent) => void + input?: Record } export function ToolErrorCard(props: ToolErrorCardProps) { @@ -37,6 +106,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) { "subtitle", "href", "onSubtitleClick", + "input", ]) const setOpen = (value: boolean) => { if (props.open === undefined) setState("open", value) @@ -86,6 +156,12 @@ export function ToolErrorCard(props: ToolErrorCardProps) { return parts.slice(1).join(": ").trim() || cleaned() }) + const context = createMemo(() => { + const input = split.input as Record | undefined + if (!input) return undefined + return getToolContextLabel(split.tool, input, i18n.t) + }) + const copy = async () => { const text = cleaned() if (!text) return @@ -153,6 +229,14 @@ export function ToolErrorCard(props: ToolErrorCardProps) { + + {(ctx) => ( +
+ {ctx().label} + {ctx().value} +
+ )} +
{(value) => {value()}} diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index aa0ec9c57351..d5a38605c958 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -170,6 +170,14 @@ export const dict: Record = { "ui.basicTool.called": "Called `{{tool}}`", "ui.toolErrorCard.failed": "Failed", "ui.toolErrorCard.copyError": "Copy error", + "ui.toolErrorCard.context.command": "Command", + "ui.toolErrorCard.context.file": "File", + "ui.toolErrorCard.context.url": "URL", + "ui.toolErrorCard.context.query": "Query", + "ui.toolErrorCard.context.pattern": "Pattern", + "ui.toolErrorCard.context.directory": "Directory", + "ui.toolErrorCard.context.task": "Task", + "ui.toolErrorCard.context.files": "Files", "ui.common.file.one": "file", "ui.common.file.other": "files",