feat(transcript): native Code Mode presentation (1/8) (#789)

## Increment over existing execute display
Execute already renders as a tool card through the generic renderer.
This PR adds a specialized presentation of its script and native
nested-call metadata (names, inputs, statuses), plus truncation
information. It reuses the existing aggregate output and image surfaces.
It does not introduce Code Mode execution or mission orchestration
(#673).

## Purpose
First PR in the V2 product integration series. Present execute scripts,
nested tool calls and progress, failure and native truncation
information. Preserve shared native output/image rendering and copying.

## Contract and implementation
Verified upstream Code Mode contracts at tags 2.0.7 and 2.0.18: code
input, metadata.toolCalls (tool/status/input), metadata.error. No API
calls or runtime minimum changes. Small dedicated parser and renderer,
scoped CSS, nine locales. Subsequent PRs will stack on this branch.

## Local validation
- UI typecheck passed.
- Four isolated Chromium scenarios passed: native events, history
reload, nested errors, images, collapse and narrow layout.
- Rendered capture inspected at 380px.
- git diff --check passed.

## Gatekeeper
Independent review pending; findings will be fixed and re-reviewed until
zero. User requested all PRs remain unmerged. Do not merge.

Size note: existing tool-call.css is above the source warning threshold;
changed only by adding the scoped import.
This commit is contained in:
Pascal André 2026-10-02 13:08:14 +02:00 • committed by GitHub
parent 0983db3c30
commit 97c361c33f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 217 additions and 1 deletions

View file

@ -26,6 +26,7 @@
- Tags and numeric/context/token labels also use rounded geometry via `--chip-radius` (`--pill-radius` is an alias). Register badge variants in `styles/components/badges.css`; use `.badge-shape` for utility-styled labels rather than adding a local radius.
- The message-content popup and Chat settings share `components/transcript-visibility.ts` and the semantic icons from `components/message-content-icons.ts`; tool presentation metadata lives independently of renderers in `components/tool-call/tool-presentation.ts`. Popup styles live in `styles/components/transcript-filters.css`.
- Tool-result images retain native `state.content` and render through the shared `components/tool-call/output-images.tsx` surface, including MCP and specialized tools. Keep image bytes out of text projections (copy/search/speech); image styles live in `styles/messaging/tool-call/images.css`.
- Code Mode reads native `metadata.toolCalls` in `tool-call/renderers/execute-data.ts`; keep call-list styling in `styles/messaging/tool-call/execute.css`, with native image/output ownership in the shared tool shell.
- Session timeline placement spans the transcript and composer via the session-owned mount; keep its rail layout in `styles/messaging/session-timeline-rail.css`. Hide the rail and force conversation-header actions into their overflow menu below 420 CSS px of the `session-center` container on all platforms. Keep that width-only force separate from measured header density so trial layouts can expand again above the breakpoint; filters anchor and return focus to the visible menu. Docked drawers reserve a minimum conversation width of 390 CSS px before overlaying. The composer worktree selector becomes a compact arrow-only cell below 460 CSS px of that conversation container (not the remaining composer width); keep its accessible label and styles in `styles/components/prompt-context-controls.css`. Timeline visibility must not depend on measured header density, changing status controls or viewport height; preserve the saved visibility preference.
- Timeline geometry uses the cached structural index independently of transcript/excerpt loading. Aggregate tool markers retain the outline's representative tool name so their icon does not fall back to `other`; preserve that metadata through projection and persisted-index revalidation. Hover/focus previews render bounded Markdown, prefetch visible-nearby excerpts and prioritize the hovered marker through `stores/timeline-previews.ts`; never mount whole message/tool cards or fetch transcript windows for hover. Keep the viewport-bounded surface in `styles/messaging/timeline-preview.css`.
- Document any new styling conventions or directory additions in this file so future changes remain consistent.

View file

@ -0,0 +1,31 @@
# V2 product integration series
Base: `dev@a77e37ef` (OpenCode client/plugin 2.0.18; technical minimum 2.0.7).
The user requested eight individually reviewed PRs, all kept open and unmerged.
Implementation is coordinated in one conversation. Each branch builds on the
preceding reviewed branch; each PR describes only its own delta.
| Order | Branch | Deliverable |
| --- | --- | --- |
| 1 | `feat/v2-01-code-mode` | Native execute script/call presentation |
| 2 | `feat/v2-02-web-results` | Web search results and provider form |
| 3 | `feat/v2-03-skills` | Explicit native skill attachments |
| 4 | `feat/v2-04-web-settings` | Web search configuration and credentials |
| 5 | `feat/v2-05-provider-accounts` | Individual provider accounts |
| 6 | `feat/v2-06-plugin-updates` | Native plugin check/update controls |
| 7 | `feat/v2-07-mcp-code-mode` | Per-server default/on/off Code Mode |
| 8 | `feat/v2-08-usage` | Native usage dashboard |
Every PR needs focused tests, rendered evidence for UI, and an independent
gatekeeper review published on GitHub. Findings are corrected and re-reviewed
until none remain. No PR is merged. Native mutations use isolated test data;
the shared user daemon is not a test target. Runtime requirements follow
demonstrated contracts, not the dependency pin. Track any adjustments and
capability boundaries in the relevant PR rather than silently shrinking scope.
## Contract notes
- Execute: OpenCode 2.0.7 and 2.0.18 `packages/core/src/codemode/tool.ts` expose
`input.code`, `metadata.toolCalls[]` (`tool`, `status`, optional `input`),
and `metadata.error`. Individual calls have no result/error text field.
Native text/file content remains owned by the shared tool output pipeline.

View file

@ -0,0 +1,37 @@
import type { ToolState } from "../../../types/tool-state"
import { readToolStatePayload } from "../utils"
export interface ExecuteCall {
tool: string
status: "running" | "completed" | "error"
input?: Record<string, unknown>
}
function record(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value)
}
// Read historical and live metadata defensively. Do not infer success for an
// unknown status or unpack output.files (native images use the shared surface).
export function executeData(state: ToolState | undefined) {
const { input, metadata } = readToolStatePayload(state)
const calls: ExecuteCall[] = []
if (Array.isArray(metadata.toolCalls)) {
for (const value of metadata.toolCalls) {
if (!record(value) || typeof value.tool !== "string"
|| !["running", "completed", "error"].includes(String(value.status))) continue
calls.push({ tool: value.tool, status: value.status as ExecuteCall["status"],
...(record(value.input) ? { input: value.input } : {}) })
}
}
return { code: typeof input.code === "string" ? input.code : "", calls,
failed: metadata.error === true, truncated: metadata.truncated === true,
outputPath: typeof metadata.outputPath === "string" ? metadata.outputPath : undefined }
}
export function executeSummary(calls: readonly ExecuteCall[]): string {
const counts = new Map<string, number>()
for (const call of calls) counts.set(call.tool, (counts.get(call.tool) ?? 0) + 1)
return [...counts].slice(0, 4).map(([tool, count]) => count > 1 ? `${tool} ×${count}` : tool).join(", ")
+ (counts.size > 4 ? ` +${counts.size - 4}` : "")
}

View file

@ -0,0 +1,49 @@
import { Index, Show, createMemo } from "solid-js"
import type { ToolRenderer } from "../types"
import { defaultRenderer } from "./default"
import { executeData, executeSummary } from "./execute-data"
import { ensureMarkdownContent, formatUnknownForCopy, formatUnknownForRender, limitToolOutputForRender, limitToolTitleForRender } from "../utils"
import { getDefaultToolSearchText } from "../search-text"
export const executeRenderer: ToolRenderer = {
tools: ["execute"],
getTitle({ toolState, t }) {
const data = executeData(toolState())
const summary = executeSummary(data.calls)
return limitToolTitleForRender(`${t("toolCall.execute.script")}${summary ? ` · ${summary}` : ""}`)
},
getSearchText(context) {
const data = executeData(context.toolState)
return [...getDefaultToolSearchText(context), data.code,
...data.calls.flatMap(call => [call.tool, formatUnknownForCopy(call.input)?.text ?? ""])]
},
getOutputChrome: defaultRenderer.getOutputChrome,
renderBody(context) {
const data = createMemo(() => executeData(context.toolState()))
const markdown = (text: string, language: string, key: string) => context.renderMarkdown({
content: ensureMarkdownContent(limitToolOutputForRender(text), language, true) ?? "",
cacheKey: `execute-${key}`, disableHighlight: context.toolState()?.status === "running",
wrap: context.outputWrapEnabled?.() ?? true,
})
return <div class="tool-call-execute">
<Show when={data().code}>{code => <section aria-label={context.t("toolCall.execute.script")}>
{markdown(code(), "javascript", "script")}
</section>}</Show>
<Show when={data().calls.length}>
<section aria-label={context.t("toolCall.execute.calls")}>
<ol class="execute-calls"><Index each={data().calls.slice(0, 200)}>{(call, index) =>
<li><details><summary>
<span class="execute-call-name">{call().tool}</span>
<span class="execute-call-status" data-status={call().status}>{context.t(`toolCall.status.${call().status}`)}</span>
</summary><Show when={call().input}>{input => markdown(formatUnknownForRender(input())?.text ?? "", "json", `call-${index}`)}</Show>
</details></li>
}</Index></ol>
<Show when={data().calls.length > 200}><p>{context.t("toolCall.execute.more", { count: data().calls.length - 200 })}</p></Show>
</section>
</Show>
<Show when={data().failed}><p class="execute-call-status" data-status="error">{context.t("toolCall.status.error")}</p></Show>
{defaultRenderer.renderBody(context)}
<Show when={data().truncated}><p>{context.t("toolCall.execute.truncated")} <span>{data().outputPath}</span></p></Show>
</div>
},
}

View file

@ -4,6 +4,7 @@ import { applyPatchRenderer } from "./renderers/apply-patch"
import { bashRenderer } from "./renderers/bash"
import { defaultRenderer } from "./renderers/default"
import { editRenderer } from "./renderers/edit"
import { executeRenderer } from "./renderers/execute"
import { invalidRenderer } from "./renderers/invalid"
import { patchRenderer } from "./renderers/patch"
import { questionRenderer } from "./renderers/question"
@ -21,7 +22,7 @@ const renderers: Record<string, ToolRenderer> = {
bash: bashRenderer, read: readRenderer, write: writeRenderer, edit: editRenderer,
patch: patchRenderer, apply_patch: applyPatchRenderer, webfetch: webfetchRenderer,
glob: searchRenderer, grep: searchRenderer, todowrite: todoRenderer, task: taskRenderer,
skill: skillRenderer, question: questionRenderer, invalid: invalidRenderer,
skill: skillRenderer, question: questionRenderer, invalid: invalidRenderer, execute: executeRenderer,
}
export function resolveToolRenderer(toolName: string): ToolRenderer {

View file

@ -1,4 +1,8 @@
export const toolCallMessages = {
"toolCall.execute.script": "Code Mode",
"toolCall.execute.calls": "Verschachtelte Werkzeugaufrufe",
"toolCall.execute.more": "{count} weitere Aufrufe ausgeblendet.",
"toolCall.execute.truncated": "Native Ausgabe gekürzt. Vollständige Ausgabe:",
"toolCall.pending.waitingToRun": "Wartet auf Ausführung...",
"toolCall.error.label": "Fehler:",

View file

@ -1,4 +1,8 @@
export const toolCallMessages = {
"toolCall.execute.script": "Code Mode",
"toolCall.execute.calls": "Nested tool calls",
"toolCall.execute.more": "{count} additional calls omitted.",
"toolCall.execute.truncated": "Native output truncated. Full output:",
"toolCall.pending.waitingToRun": "Waiting to run...",
"toolCall.error.label": "Error:",

View file

@ -1,4 +1,8 @@
export const toolCallMessages = {
"toolCall.execute.script": "Code Mode",
"toolCall.execute.calls": "Llamadas a herramientas anidadas",
"toolCall.execute.more": "Se omitieron {count} llamadas adicionales.",
"toolCall.execute.truncated": "Salida nativa truncada. Salida completa:",
"toolCall.pending.waitingToRun": "Esperando para ejecutar...",
"toolCall.error.label": "Error:",

View file

@ -1,4 +1,8 @@
export const toolCallMessages = {
"toolCall.execute.script": "Code Mode",
"toolCall.execute.calls": "Appels d’outils imbriqués",
"toolCall.execute.more": "{count} appels supplémentaires masqués.",
"toolCall.execute.truncated": "Sortie native tronquée. Sortie complète :",
"toolCall.pending.waitingToRun": "En attente d'exécution...",
"toolCall.error.label": "Erreur :",

View file

@ -1,4 +1,8 @@
export const toolCallMessages = {
"toolCall.execute.script": "Code Mode",
"toolCall.execute.calls": "קריאות כלים מקוננות",
"toolCall.execute.more": "{count} קריאות נוספות הוסתרו.",
"toolCall.execute.truncated": "הפלט המקורי קוצר. הפלט המלא:",
"toolCall.pending.waitingToRun": "ממתין להרצה...",
"toolCall.error.label": "שגיאה:",

View file

@ -1,4 +1,8 @@
export const toolCallMessages = {
"toolCall.execute.script": "Code Mode",
"toolCall.execute.calls": "入れ子のツール呼び出し",
"toolCall.execute.more": "さらに {count} 件の呼び出しを省略しました。",
"toolCall.execute.truncated": "ネイティブ出力が切り詰められました。完全な出力:",
"toolCall.pending.waitingToRun": "実行待ち...",
"toolCall.error.label": "エラー:",

View file

@ -1,4 +1,8 @@
export const toolCallMessages = {
"toolCall.execute.script": "Code Mode",
"toolCall.execute.calls": "नेस्टेड उपकरण कलहरू",
"toolCall.execute.more": "थप {count} कलहरू लुकाइएका छन्।",
"toolCall.execute.truncated": "नेटिभ आउटपुट छोट्याइएको छ। पूरा आउटपुट:",
"toolCall.pending.waitingToRun": "चलाउनको लागि पर्खँदै...",
"toolCall.error.label": "त्रुटि:",

View file

@ -1,4 +1,8 @@
export const toolCallMessages = {
"toolCall.execute.script": "Code Mode",
"toolCall.execute.calls": "Вложенные вызовы инструментов",
"toolCall.execute.more": "Скрыто дополнительных вызовов: {count}.",
"toolCall.execute.truncated": "Нативный вывод сокращён. Полный вывод:",
"toolCall.pending.waitingToRun": "Ожидание запуска…",
"toolCall.error.label": "Ошибка:",

View file

@ -1,4 +1,8 @@
export const toolCallMessages = {
"toolCall.execute.script": "Code Mode",
"toolCall.execute.calls": "İç içe araç çağrıları",
"toolCall.execute.more": "{count} ek çağrı gizlendi.",
"toolCall.execute.truncated": "Yerel çıktı kısaltıldı. Tam çıktı:",
"toolCall.pending.waitingToRun": "Çalıştırılmayı bekliyor...",
"toolCall.error.label": "Hata:",
"toolCall.header.copyTitle": "Araç çağrısı başlığını kopyala",

View file

@ -1,4 +1,8 @@
export const toolCallMessages = {
"toolCall.execute.script": "Code Mode",
"toolCall.execute.calls": "嵌套工具调用",
"toolCall.execute.more": "已省略另外 {count} 个调用。",
"toolCall.execute.truncated": "原生输出已截断。完整输出:",
"toolCall.pending.waitingToRun": "等待运行...",
"toolCall.error.label": "错误:",

View file

@ -2,6 +2,7 @@
@import "./tool-call/todo.css";
@import "./tool-call/task.css";
@import "./tool-call/skill.css";
@import "./tool-call/execute.css";
.tool-call-message {
@apply flex flex-col gap-0 p-0 w-full;

View file

@ -0,0 +1,6 @@
.tool-call-execute { display: grid; gap: 0.5rem; min-width: 0; }
.execute-calls { list-style: none; margin: 0; padding: 0; }
.execute-calls summary { display: flex; gap: 0.75rem; cursor: pointer; padding: 0.35rem 0; }
.execute-call-name { flex: 1; min-width: 0; overflow-wrap: anywhere; font-family: var(--font-mono); }
.execute-call-status { color: var(--text-secondary); flex-shrink: 0; }
.execute-call-status[data-status="error"] { color: var(--status-error); }

View file

@ -82,4 +82,27 @@ render(() => <ConfigProvider><I18nProvider><ThemeProvider>
emit("session.tool.success", { id: "tool", content: content("mixed"), executed: true })
},
reload: () => loadMessages(instanceId, sessionId, { force: true }),
execute: async (phase: string) => {
const input = { code: "const results = await Promise.all([tools.paint({ prompt: 'sky' }), tools.fail({})]);\nreturn results" }
const metadata = { toolCalls: [
{ tool: "paint", status: phase === "running" ? "running" : "completed", input: { prompt: "sky" } },
{ tool: "fail", status: phase === "running" ? "running" : "error", input: { test: true } },
], ...(phase === "running" ? {} : { error: true, truncated: true, outputPath: "/fixture/output.txt" }) }
if (phase === "progress") {
metadata.toolCalls.push({ tool: "third", status: "running", input: { test: true } })
emit("session.tool.progress", { id: "tool", metadata })
return
}
if (phase === "running") {
emit("session.step.started", { agent: "build", model, started: 1 })
emit("session.tool.input.started", { id: "tool", name: "execute" })
emit("session.tool.called", { id: "tool", input })
emit("session.tool.progress", { id: "tool", metadata })
return
}
messages = [{ ...completed("mixed", "execute"), content: [{ id: "tool", type: "tool", name: "execute",
time: { created: 1, completed: time }, state: { status: "completed", input, metadata, content: content("mixed") } }] }]
if (phase === "history") await loadMessages(instanceId, sessionId, { force: true })
else emit("session.tool.success", { id: "tool", content: content("mixed"), metadata, executed: true })
},
}

View file

@ -88,3 +88,30 @@ test("failed and unsupported image sources have a fallback and a later replaceme
}
assert.equal(await page.evaluate(() => (window as any).unexpectedNavigation), undefined)
}))
test("Code Mode renders native progress, nested errors, script, images and historical output", async () => withPage(async page => {
await page.evaluate(() => (window as any).fixture.execute("running"))
await page.locator(".execute-calls").waitFor()
assert.equal(await page.locator('.execute-call-status[data-status="running"]').count(), 2)
const firstCall = page.locator(".execute-calls summary").first()
await firstCall.click()
await firstCall.focus()
await page.evaluate(() => (window as any).fixture.execute("progress"))
assert.equal(await page.locator(".execute-calls li").count(), 3)
assert.equal(await page.locator(".execute-calls details").first().getAttribute("open"), "")
assert.equal(await firstCall.evaluate(el => el === document.activeElement), true)
await page.evaluate(() => (window as any).fixture.execute("complete"))
await decodedImages(page, 2)
await page.locator('.execute-call-status[data-status="completed"]').waitFor()
assert.equal(await page.locator('.execute-calls [data-status="error"]').count(), 1)
await page.locator(".execute-calls summary").first().click()
await page.getByText('"sky"', { exact: false }).first().waitFor()
assert.match(await page.locator(".tool-call-execute").innerText(), /Promise\.all/)
assert.match(await page.locator(".tool-call-execute").innerText(), /\/fixture\/output.txt/)
await page.evaluate(() => (window as any).fixture.execute("history"))
await decodedImages(page, 2)
assert.equal(await page.locator('.execute-calls [data-status="error"]').count(), 1)
await page.setViewportSize({ width: 380, height: 1000 })
assert.equal(await page.locator("main").evaluate(el => el.scrollWidth <= el.clientWidth), true)
if (process.env.CODENOMAD_EXECUTE_CAPTURE) await page.screenshot({ path: process.env.CODENOMAD_EXECUTE_CAPTURE, fullPage: true })
}))