diff options
| author | Adam <[email protected]> | 2026-03-12 16:25:36 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-03-12 16:25:49 -0500 |
| commit | 9d3c42c8c49ec56ee890dc2af9c47f19494999fc (patch) | |
| tree | a71cda6b9c27e7cb426bfd78eead45a2fa54afae /packages/ui/src | |
| parent | f2cad046e6c38885b454d01cb28888152a54b375 (diff) | |
| download | opencode-9d3c42c8c49ec56ee890dc2af9c47f19494999fc.tar.gz opencode-9d3c42c8c49ec56ee890dc2af9c47f19494999fc.zip | |
fix(app): task error state
Diffstat (limited to 'packages/ui/src')
| -rw-r--r-- | packages/ui/src/components/message-part.tsx | 62 | ||||
| -rw-r--r-- | packages/ui/src/components/tool-error-card.tsx | 21 |
2 files changed, 62 insertions, 21 deletions
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 5a0f022ea..500c73c5e 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -344,6 +344,17 @@ function urls(text: string | undefined) { }) } +function sessionLink(id: string | undefined, path: string, href?: (id: string) => string | undefined) { + if (!id) return + + const direct = href?.(id) + if (direct) return direct + + const idx = path.indexOf("/session") + if (idx === -1) return + return `${path.slice(0, idx)}/session/${id}` +} + const CONTEXT_GROUP_TOOLS = new Set(["read", "glob", "grep", "list"]) const HIDDEN_TOOLS = new Set(["todowrite", "todoread"]) @@ -1215,6 +1226,7 @@ function ToolFileAccordion(props: { path: string; actions?: JSX.Element; childre } PART_MAPPING["tool"] = function ToolPartDisplay(props) { + const data = useData() const i18n = useI18n() const part = () => props.part as ToolPart if (part().tool === "todowrite" || part().tool === "todoread") return null @@ -1229,6 +1241,21 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { const input = () => part().state?.input ?? emptyInput // @ts-expect-error const partMetadata = () => part().state?.metadata ?? emptyMetadata + const taskId = createMemo(() => { + if (part().tool !== "task") return + const value = partMetadata().sessionId + if (typeof value === "string" && value) return value + }) + const taskHref = createMemo(() => { + if (part().tool !== "task") return + return sessionLink(taskId(), useLocation().pathname, data.sessionHref) + }) + const taskSubtitle = createMemo(() => { + if (part().tool !== "task") return undefined + const value = input().description + if (typeof value === "string" && value) return value + return taskId() + }) const render = createMemo(() => ToolRegistry.render(part().tool) ?? GenericTool) @@ -1248,7 +1275,15 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { </div> ) } - return <ToolErrorCard tool={part().tool} error={error()} defaultOpen={props.defaultOpen} /> + return ( + <ToolErrorCard + tool={part().tool} + error={error()} + defaultOpen={props.defaultOpen} + subtitle={taskSubtitle()} + href={taskHref()} + /> + ) }} </Match> <Match when={true}> @@ -1625,25 +1660,14 @@ ToolRegistry.register({ return raw[0]!.toUpperCase() + raw.slice(1) }) const title = createMemo(() => agentTitle(i18n, type())) - const description = createMemo(() => { + const subtitle = createMemo(() => { const value = props.input.description - if (typeof value === "string") return value - return undefined + if (typeof value === "string" && value) return value + return childSessionId() }) const running = createMemo(() => props.status === "pending" || props.status === "running") - const href = createMemo(() => { - const sessionId = childSessionId() - if (!sessionId) return - - const direct = data.sessionHref?.(sessionId) - if (direct) return direct - - const path = location.pathname - const idx = path.indexOf("/session") - if (idx === -1) return - return `${path.slice(0, idx)}/session/${sessionId}` - }) + const href = createMemo(() => sessionLink(childSessionId(), location.pathname, data.sessionHref)) const titleContent = () => <TextShimmer text={title()} active={running()} /> @@ -1653,7 +1677,7 @@ ToolRegistry.register({ <span data-slot="basic-tool-tool-title" class="capitalize agent-title"> {titleContent()} </span> - <Show when={description()}> + <Show when={subtitle()}> <Switch> <Match when={href()}> <a @@ -1662,11 +1686,11 @@ ToolRegistry.register({ href={href()!} onClick={(e) => e.stopPropagation()} > - {description()} + {subtitle()} </a> </Match> <Match when={true}> - <span data-slot="basic-tool-tool-subtitle">{description()}</span> + <span data-slot="basic-tool-tool-subtitle">{subtitle()}</span> </Match> </Switch> </Show> diff --git a/packages/ui/src/components/tool-error-card.tsx b/packages/ui/src/components/tool-error-card.tsx index 2e9612b2b..ba39ae586 100644 --- a/packages/ui/src/components/tool-error-card.tsx +++ b/packages/ui/src/components/tool-error-card.tsx @@ -10,19 +10,22 @@ export interface ToolErrorCardProps extends Omit<ComponentProps<typeof Card>, "c tool: string error: string defaultOpen?: boolean + subtitle?: string + href?: string } export function ToolErrorCard(props: ToolErrorCardProps) { const i18n = useI18n() const [open, setOpen] = createSignal(props.defaultOpen ?? false) const [copied, setCopied] = createSignal(false) - const [split, rest] = splitProps(props, ["tool", "error", "defaultOpen"]) + const [split, rest] = splitProps(props, ["tool", "error", "defaultOpen", "subtitle", "href"]) const name = createMemo(() => { const map: Record<string, string> = { read: "ui.tool.read", list: "ui.tool.list", glob: "ui.tool.glob", grep: "ui.tool.grep", + task: "Task", webfetch: "ui.tool.webfetch", websearch: "ui.tool.websearch", codesearch: "ui.tool.codesearch", @@ -32,6 +35,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) { } const key = map[split.tool] if (!key) return split.tool + if (!key.includes(".")) return key return i18n.t(key) }) const cleaned = createMemo(() => split.error.replace(/^Error:\s*/, "").trim()) @@ -43,6 +47,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) { }) const subtitle = createMemo(() => { + if (split.subtitle) return split.subtitle const parts = tail().split(": ") if (parts.length <= 1) return "Failed" const head = (parts[0] ?? "").trim() @@ -77,7 +82,19 @@ export function ToolErrorCard(props: ToolErrorCardProps) { <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> + <Show + when={split.href && split.subtitle} + fallback={<span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>} + > + <a + data-slot="basic-tool-tool-subtitle" + class="clickable subagent-link" + href={split.href!} + onClick={(e) => e.stopPropagation()} + > + {subtitle()} + </a> + </Show> </div> </div> </div> |
