diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 171c4b448..55b95fffe 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -425,14 +425,14 @@ export namespace SessionPrompt { extra: { bypassAgentCheck: true }, messages: msgs, async metadata(input) { - await Session.updatePart({ + part = (await Session.updatePart({ ...part, type: "tool", state: { ...part.state, ...input, }, - } satisfies MessageV2.ToolPart) + } satisfies MessageV2.ToolPart)) as MessageV2.ToolPart }, async ask(req) { await PermissionNext.ask({ @@ -493,7 +493,7 @@ export namespace SessionPrompt { start: part.state.status === "running" ? part.state.time.start : Date.now(), end: Date.now(), }, - metadata: part.metadata, + metadata: "metadata" in part.state ? part.state.metadata : undefined, input: part.state.input, }, } satisfies MessageV2.ToolPart) 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) { ) } - return + return ( + + ) }} @@ -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 = () => @@ -1653,7 +1677,7 @@ ToolRegistry.register({ {titleContent()} - + e.stopPropagation()} > - {description()} + {subtitle()} - {description()} + {subtitle()} 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, "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 = { 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) {
{name()} - {subtitle()} + {subtitle()}} + > + e.stopPropagation()} + > + {subtitle()} + +