feat(ui): render task prompt/output panes

Task tool calls now show prompt, summary, and output with independent scroll; markdown rendering supports cache keys to avoid collisions.
This commit is contained in:
Shantur Rathore
2026-01-23 22:39:04 +00:00
parent f0b43dbc68
commit 3d3337c7b8
4 changed files with 194 additions and 42 deletions

View File

@@ -1,8 +1,7 @@
import { For, Show, createMemo } from "solid-js"
import type { ToolState } from "@opencode-ai/sdk"
import type { ToolRenderer } from "../types"
import { getDefaultToolAction, getToolIcon, getToolName, readToolStatePayload } from "../utils"
import { getTodoTitle } from "./todo"
import { ensureMarkdownContent, getDefaultToolAction, getToolIcon, getToolName, readToolStatePayload } from "../utils"
import { resolveTitleForTool } from "../tool-title"
interface TaskSummaryItem {
@@ -90,7 +89,29 @@ export const taskRenderer: ToolRenderer = {
const { input } = readToolStatePayload(state)
return describeTaskTitle(input)
},
renderBody({ toolState, messageVersion, partVersion, scrollHelpers }) {
renderBody({ toolState, messageVersion, partVersion, scrollHelpers, renderMarkdown }) {
const promptContent = createMemo(() => {
const state = toolState()
if (!state) return null
const { input } = readToolStatePayload(state)
const prompt = typeof input.prompt === "string" ? input.prompt : null
return ensureMarkdownContent(prompt, undefined, false)
})
const outputContent = createMemo(() => {
const state = toolState()
if (!state) return null
const output = typeof (state as { output?: unknown }).output === "string" ? ((state as { output?: string }).output as string) : null
return ensureMarkdownContent(output, undefined, false)
})
const agentLabel = createMemo(() => {
const state = toolState()
if (!state) return null
const { input } = readToolStatePayload(state)
return typeof input.subagent_type === "string" ? input.subagent_type : null
})
const items = createMemo(() => {
// Track the reactive change points so we only recompute when the part/message changes
messageVersion?.()
@@ -114,41 +135,90 @@ export const taskRenderer: ToolRenderer = {
})
})
if (items().length === 0) return null
return (
<div
class="message-text tool-call-markdown tool-call-task-container"
ref={(element) => scrollHelpers?.registerContainer(element)}
onScroll={scrollHelpers ? (event) => scrollHelpers.handleScroll(event as Event & { currentTarget: HTMLDivElement }) : undefined}
>
<div class="tool-call-task-summary">
<For each={items()}>
{(item) => {
const icon = getToolIcon(item.tool)
const description = describeToolTitle(item)
const toolLabel = getToolName(item.tool)
const status = normalizeStatus(item.status ?? item.state?.status)
const statusIcon = summarizeStatusIcon(status)
const statusLabel = summarizeStatusLabel(status)
const statusAttr = status ?? "pending"
return (
<div class="tool-call-task-item" data-task-id={item.id} data-task-status={statusAttr}>
<span class="tool-call-task-icon">{icon}</span>
<span class="tool-call-task-label">{toolLabel}</span>
<span class="tool-call-task-separator" aria-hidden="true"></span>
<span class="tool-call-task-text">{description}</span>
<Show when={statusIcon}>
<span class="tool-call-task-status" aria-label={statusLabel} title={statusLabel}>
{statusIcon}
</span>
</Show>
<div class="tool-call-task-sections">
<Show when={promptContent()}>
<section class="tool-call-task-section">
<header class="tool-call-task-section-header">
<span class="tool-call-task-section-title">Prompt</span>
<Show when={agentLabel()}>
<span class="tool-call-task-section-meta">Agent: {agentLabel()}</span>
</Show>
</header>
<div class="tool-call-task-section-body">
{renderMarkdown({
content: promptContent()!,
cacheKey: "task:prompt",
disableScrollTracking: true,
disableHighlight: true,
})}
</div>
</section>
</Show>
<Show when={items().length > 0}>
<section class="tool-call-task-section">
<header class="tool-call-task-section-header">
<span class="tool-call-task-section-title">Tasks</span>
<span class="tool-call-task-section-meta">{items().length} item(s)</span>
</header>
<div class="tool-call-task-section-body">
<div
class="message-text tool-call-markdown tool-call-task-container"
ref={(element) => scrollHelpers?.registerContainer(element)}
onScroll={
scrollHelpers ? (event) => scrollHelpers.handleScroll(event as Event & { currentTarget: HTMLDivElement }) : undefined
}
>
<div class="tool-call-task-summary">
<For each={items()}>
{(item) => {
const icon = getToolIcon(item.tool)
const description = describeToolTitle(item)
const toolLabel = getToolName(item.tool)
const status = normalizeStatus(item.status ?? item.state?.status)
const statusIcon = summarizeStatusIcon(status)
const statusLabel = summarizeStatusLabel(status)
const statusAttr = status ?? "pending"
return (
<div class="tool-call-task-item" data-task-id={item.id} data-task-status={statusAttr}>
<span class="tool-call-task-icon">{icon}</span>
<span class="tool-call-task-label">{toolLabel}</span>
<span class="tool-call-task-separator" aria-hidden="true"></span>
<span class="tool-call-task-text">{description}</span>
<Show when={statusIcon}>
<span class="tool-call-task-status" aria-label={statusLabel} title={statusLabel}>
{statusIcon}
</span>
</Show>
</div>
)
}}
</For>
</div>
)
}}
</For>
</div>
{scrollHelpers?.renderSentinel?.()}
{scrollHelpers?.renderSentinel?.()}
</div>
</div>
</section>
</Show>
<Show when={outputContent()}>
<section class="tool-call-task-section">
<header class="tool-call-task-section-header">
<span class="tool-call-task-section-title">Output</span>
<Show when={agentLabel()}>
<span class="tool-call-task-section-meta">Agent: {agentLabel()}</span>
</Show>
</header>
<div class="tool-call-task-section-body">
{renderMarkdown({
content: outputContent()!,
cacheKey: "task:output",
disableScrollTracking: true,
})}
</div>
</section>
</Show>
</div>
)
},