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) {