Skip to content

Commit 9d3c42c

Browse files
committed
fix(app): task error state
1 parent f2cad04 commit 9d3c42c

3 files changed

Lines changed: 65 additions & 24 deletions

File tree

packages/opencode/src/session/prompt.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -425,14 +425,14 @@ export namespace SessionPrompt {
425425
extra: { bypassAgentCheck: true },
426426
messages: msgs,
427427
async metadata(input) {
428-
await Session.updatePart({
428+
part = (await Session.updatePart({
429429
...part,
430430
type: "tool",
431431
state: {
432432
...part.state,
433433
...input,
434434
},
435-
} satisfies MessageV2.ToolPart)
435+
} satisfies MessageV2.ToolPart)) as MessageV2.ToolPart
436436
},
437437
async ask(req) {
438438
await PermissionNext.ask({
@@ -493,7 +493,7 @@ export namespace SessionPrompt {
493493
start: part.state.status === "running" ? part.state.time.start : Date.now(),
494494
end: Date.now(),
495495
},
496-
metadata: part.metadata,
496+
metadata: "metadata" in part.state ? part.state.metadata : undefined,
497497
input: part.state.input,
498498
},
499499
} satisfies MessageV2.ToolPart)

packages/ui/src/components/message-part.tsx

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,17 @@ function urls(text: string | undefined) {
344344
})
345345
}
346346

347+
function sessionLink(id: string | undefined, path: string, href?: (id: string) => string | undefined) {
348+
if (!id) return
349+
350+
const direct = href?.(id)
351+
if (direct) return direct
352+
353+
const idx = path.indexOf("/session")
354+
if (idx === -1) return
355+
return `${path.slice(0, idx)}/session/${id}`
356+
}
357+
347358
const CONTEXT_GROUP_TOOLS = new Set(["read", "glob", "grep", "list"])
348359
const HIDDEN_TOOLS = new Set(["todowrite", "todoread"])
349360

@@ -1215,6 +1226,7 @@ function ToolFileAccordion(props: { path: string; actions?: JSX.Element; childre
12151226
}
12161227

12171228
PART_MAPPING["tool"] = function ToolPartDisplay(props) {
1229+
const data = useData()
12181230
const i18n = useI18n()
12191231
const part = () => props.part as ToolPart
12201232
if (part().tool === "todowrite" || part().tool === "todoread") return null
@@ -1229,6 +1241,21 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
12291241
const input = () => part().state?.input ?? emptyInput
12301242
// @ts-expect-error
12311243
const partMetadata = () => part().state?.metadata ?? emptyMetadata
1244+
const taskId = createMemo(() => {
1245+
if (part().tool !== "task") return
1246+
const value = partMetadata().sessionId
1247+
if (typeof value === "string" && value) return value
1248+
})
1249+
const taskHref = createMemo(() => {
1250+
if (part().tool !== "task") return
1251+
return sessionLink(taskId(), useLocation().pathname, data.sessionHref)
1252+
})
1253+
const taskSubtitle = createMemo(() => {
1254+
if (part().tool !== "task") return undefined
1255+
const value = input().description
1256+
if (typeof value === "string" && value) return value
1257+
return taskId()
1258+
})
12321259

12331260
const render = createMemo(() => ToolRegistry.render(part().tool) ?? GenericTool)
12341261

@@ -1248,7 +1275,15 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
12481275
</div>
12491276
)
12501277
}
1251-
return <ToolErrorCard tool={part().tool} error={error()} defaultOpen={props.defaultOpen} />
1278+
return (
1279+
<ToolErrorCard
1280+
tool={part().tool}
1281+
error={error()}
1282+
defaultOpen={props.defaultOpen}
1283+
subtitle={taskSubtitle()}
1284+
href={taskHref()}
1285+
/>
1286+
)
12521287
}}
12531288
</Match>
12541289
<Match when={true}>
@@ -1625,25 +1660,14 @@ ToolRegistry.register({
16251660
return raw[0]!.toUpperCase() + raw.slice(1)
16261661
})
16271662
const title = createMemo(() => agentTitle(i18n, type()))
1628-
const description = createMemo(() => {
1663+
const subtitle = createMemo(() => {
16291664
const value = props.input.description
1630-
if (typeof value === "string") return value
1631-
return undefined
1665+
if (typeof value === "string" && value) return value
1666+
return childSessionId()
16321667
})
16331668
const running = createMemo(() => props.status === "pending" || props.status === "running")
16341669

1635-
const href = createMemo(() => {
1636-
const sessionId = childSessionId()
1637-
if (!sessionId) return
1638-
1639-
const direct = data.sessionHref?.(sessionId)
1640-
if (direct) return direct
1641-
1642-
const path = location.pathname
1643-
const idx = path.indexOf("/session")
1644-
if (idx === -1) return
1645-
return `${path.slice(0, idx)}/session/${sessionId}`
1646-
})
1670+
const href = createMemo(() => sessionLink(childSessionId(), location.pathname, data.sessionHref))
16471671

16481672
const titleContent = () => <TextShimmer text={title()} active={running()} />
16491673

@@ -1653,7 +1677,7 @@ ToolRegistry.register({
16531677
<span data-slot="basic-tool-tool-title" class="capitalize agent-title">
16541678
{titleContent()}
16551679
</span>
1656-
<Show when={description()}>
1680+
<Show when={subtitle()}>
16571681
<Switch>
16581682
<Match when={href()}>
16591683
<a
@@ -1662,11 +1686,11 @@ ToolRegistry.register({
16621686
href={href()!}
16631687
onClick={(e) => e.stopPropagation()}
16641688
>
1665-
{description()}
1689+
{subtitle()}
16661690
</a>
16671691
</Match>
16681692
<Match when={true}>
1669-
<span data-slot="basic-tool-tool-subtitle">{description()}</span>
1693+
<span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>
16701694
</Match>
16711695
</Switch>
16721696
</Show>

packages/ui/src/components/tool-error-card.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,22 @@ export interface ToolErrorCardProps extends Omit<ComponentProps<typeof Card>, "c
1010
tool: string
1111
error: string
1212
defaultOpen?: boolean
13+
subtitle?: string
14+
href?: string
1315
}
1416

1517
export function ToolErrorCard(props: ToolErrorCardProps) {
1618
const i18n = useI18n()
1719
const [open, setOpen] = createSignal(props.defaultOpen ?? false)
1820
const [copied, setCopied] = createSignal(false)
19-
const [split, rest] = splitProps(props, ["tool", "error", "defaultOpen"])
21+
const [split, rest] = splitProps(props, ["tool", "error", "defaultOpen", "subtitle", "href"])
2022
const name = createMemo(() => {
2123
const map: Record<string, string> = {
2224
read: "ui.tool.read",
2325
list: "ui.tool.list",
2426
glob: "ui.tool.glob",
2527
grep: "ui.tool.grep",
28+
task: "Task",
2629
webfetch: "ui.tool.webfetch",
2730
websearch: "ui.tool.websearch",
2831
codesearch: "ui.tool.codesearch",
@@ -32,6 +35,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
3235
}
3336
const key = map[split.tool]
3437
if (!key) return split.tool
38+
if (!key.includes(".")) return key
3539
return i18n.t(key)
3640
})
3741
const cleaned = createMemo(() => split.error.replace(/^Error:\s*/, "").trim())
@@ -43,6 +47,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
4347
})
4448

4549
const subtitle = createMemo(() => {
50+
if (split.subtitle) return split.subtitle
4651
const parts = tail().split(": ")
4752
if (parts.length <= 1) return "Failed"
4853
const head = (parts[0] ?? "").trim()
@@ -77,7 +82,19 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
7782
<div data-slot="basic-tool-tool-info-structured">
7883
<div data-slot="basic-tool-tool-info-main">
7984
<span data-slot="basic-tool-tool-title">{name()}</span>
80-
<span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>
85+
<Show
86+
when={split.href && split.subtitle}
87+
fallback={<span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>}
88+
>
89+
<a
90+
data-slot="basic-tool-tool-subtitle"
91+
class="clickable subagent-link"
92+
href={split.href!}
93+
onClick={(e) => e.stopPropagation()}
94+
>
95+
{subtitle()}
96+
</a>
97+
</Show>
8198
</div>
8299
</div>
83100
</div>

0 commit comments

Comments
 (0)