diff --git a/packages/ui/src/components/tool-call/markdown-render.tsx b/packages/ui/src/components/tool-call/markdown-render.tsx index 58e356e5..36969901 100644 --- a/packages/ui/src/components/tool-call/markdown-render.tsx +++ b/packages/ui/src/components/tool-call/markdown-render.tsx @@ -23,20 +23,26 @@ export function createMarkdownContentRenderer(params: { const size = options.size || "default" const disableHighlight = options.disableHighlight || false const messageClass = `message-text tool-call-markdown${size === "large" ? " tool-call-markdown-large" : ""}` + const disableScrollTracking = options.disableScrollTracking || false const state = params.toolState() const shouldDeferMarkdown = Boolean(state && (state.status === "running" || state.status === "pending") && disableHighlight) if (shouldDeferMarkdown) { return ( -
params.scrollHelpers.registerContainer(element)} onScroll={params.scrollHelpers.handleScroll}> +
params.scrollHelpers.registerContainer(element, { disableTracking: disableScrollTracking })} + onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll} + >
{options.content}
- {params.scrollHelpers.renderSentinel()} + {params.scrollHelpers.renderSentinel({ disableTracking: disableScrollTracking })}
) } + const cacheKey = typeof options.cacheKey === "string" && options.cacheKey.length > 0 ? options.cacheKey : undefined const markdownPart: TextPart = { - id: params.partId(), + id: cacheKey ? `${params.partId()}:${cacheKey}` : params.partId(), type: "text", text: options.content, version: params.partVersion?.(), @@ -48,7 +54,11 @@ export function createMarkdownContentRenderer(params: { } return ( -
params.scrollHelpers.registerContainer(element)} onScroll={params.scrollHelpers.handleScroll}> +
params.scrollHelpers.registerContainer(element, { disableTracking: disableScrollTracking })} + onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll} + > - {params.scrollHelpers.renderSentinel()} + {params.scrollHelpers.renderSentinel({ disableTracking: disableScrollTracking })}
) } diff --git a/packages/ui/src/components/tool-call/renderers/task.tsx b/packages/ui/src/components/tool-call/renderers/task.tsx index 0ac0ecdd..7d448609 100644 --- a/packages/ui/src/components/tool-call/renderers/task.tsx +++ b/packages/ui/src/components/tool-call/renderers/task.tsx @@ -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 ( -
scrollHelpers?.registerContainer(element)} - onScroll={scrollHelpers ? (event) => scrollHelpers.handleScroll(event as Event & { currentTarget: HTMLDivElement }) : undefined} - > -
- - {(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 ( -
- {icon} - {toolLabel} - - {description} - - - {statusIcon} - - +
+ +
+
+ Prompt + + + +
+
+ {renderMarkdown({ + content: promptContent()!, + cacheKey: "task:prompt", + disableScrollTracking: true, + disableHighlight: true, + })} +
+
+
+ + 0}> +
+
+ Tasks + +
+
+
scrollHelpers?.registerContainer(element)} + onScroll={ + scrollHelpers ? (event) => scrollHelpers.handleScroll(event as Event & { currentTarget: HTMLDivElement }) : undefined + } + > +
+ + {(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 ( +
+ {icon} + {toolLabel} + + {description} + + + {statusIcon} + + +
+ ) + }} +
- ) - }} - -
- {scrollHelpers?.renderSentinel?.()} + {scrollHelpers?.renderSentinel?.()} +
+
+ + + + +
+
+ Output + + + +
+
+ {renderMarkdown({ + content: outputContent()!, + cacheKey: "task:output", + disableScrollTracking: true, + })} +
+
+
) }, diff --git a/packages/ui/src/components/tool-call/types.ts b/packages/ui/src/components/tool-call/types.ts index 22db6903..ca6fbc5c 100644 --- a/packages/ui/src/components/tool-call/types.ts +++ b/packages/ui/src/components/tool-call/types.ts @@ -13,6 +13,16 @@ export interface MarkdownRenderOptions { content: string size?: "default" | "large" disableHighlight?: boolean + /** + * Optional suffix to avoid render-cache collisions when a tool call renders + * multiple markdown regions (e.g. task prompt vs task output). + */ + cacheKey?: string + /** + * When true, do not register this markdown region with tool-call scroll + * tracking (avoids nested scroll + autoscroll interactions). + */ + disableScrollTracking?: boolean } export interface AnsiRenderOptions { diff --git a/packages/ui/src/styles/messaging/tool-call/task.css b/packages/ui/src/styles/messaging/tool-call/task.css index 914690c1..22b07302 100644 --- a/packages/ui/src/styles/messaging/tool-call/task.css +++ b/packages/ui/src/styles/messaging/tool-call/task.css @@ -1,7 +1,69 @@ -.tool-call-task-container { +.tool-call-task-sections { + display: flex; + flex-direction: column; + gap: var(--space-xs); + padding: 0; +} + +.tool-call-task-section { + border: 1px solid var(--border-base); + overflow: hidden; + background-color: transparent; + border-radius: 0; +} + +.tool-call-task-section-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.5rem; + background-color: var(--surface-secondary); + border-bottom: 1px solid var(--border-base); + font-family: var(--font-family-mono); + font-size: 13px; + color: inherit; +} + +.tool-call-task-section-title { + font-weight: var(--font-weight-semibold); +} + +.tool-call-task-section-meta { + font-family: var(--font-family-mono); + color: var(--text-muted); +} + +.tool-call-task-section-body { + background-color: var(--surface-code); +} + +.tool-call-task-section-body .tool-call-markdown { padding: 12px; } +.tool-call-task-container { + padding: 0; +} + +/* Keep task lists compact vs prompt/output panes. */ +.tool-call-task-container.tool-call-markdown { + max-height: calc(var(--tool-call-max-height-compact, calc(25 * 1.4em)) / 2); +} + +/* Prompt + output panes: slightly taller than tasks. */ +.tool-call-task-section-body > .tool-call-markdown:not(.tool-call-task-container) { + max-height: calc(var(--tool-call-max-height-compact, calc(25 * 1.4em)) * 2 / 3); +} + +.tool-call-task-empty { + font-family: var(--font-family-mono); + font-size: var(--font-size-xs); + line-height: var(--line-height-tight); + color: var(--text-muted); + padding: 0.5rem; +} + .tool-call-task-summary { display: flex; flex-direction: column;