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",