[Bug]: html_render pages never show for OpenCode 2 threads because Code Mode wraps the call in `execute`#16745

opened22d agobyEehfreema

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Use an OpenCode 2 provider instance (Code Mode is on by default, so T3's MCP tools are reachable only through OpenCode's execute tool).
  2. In a thread, ask the agent to show something visually, for example: "Build a small HTML chart and publish it with html_render."
  3. The agent runs execute with a script that calls tools["t3-code-<thread>"].html_preview(...) and tools["t3-code-<thread>"].html_render(...).
  4. Look at the thread on web, desktop, or mobile.

The simplest form that still fails is a script whose output is exactly html_render's result:

js
1const out = await tools["t3-code-<thread>"].html_render({ html, title: "Agent roster", height: 1508 });
2return out;

Expected behavior

The published page shows inline above the agent's reply, as it does for Claude, Codex, Cursor, Pi and OpenCode 1. Deleting the thread deletes the page.

Actual behavior

No page is shown. The thread only has a generic execute work row, which folds away when the turn completes. html_render still tells the model "Shown to the reader above your reply. Don't mention or describe the page", so the agent's reply refers to a page the user never sees.

Cause, at main @ bfec2387b8:

  • Clients find a page only through htmlRenderFromToolItem (packages/shared/src/toolOutput.ts:168-175). That requires a completed dynamic_tool item whose tool name resolves to html_render.
  • OpenCode 2 runs nested Code Mode calls inside execute and reports only that one tool: no events or parts for the nested calls, and its metadata.toolCalls lists { tool, status, input } without results. The adapter projects it as dynamic_tool with toolName: "execute" (OpenCodeToolItems.ts:155-167), so the name check fails even when the script returns the reference. In the example above, the execute item's stored output is exactly {"htmlRender":{"attachmentId":"…-html","title":"Agent roster and redundancy","height":1508,"heights":[…]},"message":"Shown to the reader…"}, and nothing renders.
  • The same check decides which pages thread deletion removes (threadHtmlRenderAttachmentIds in apps/server/src/attachmentStore.ts:118-135, used by ProjectionStore.getThreadAttachmentIds). Pages published from execute are never cleaned up.

In one OpenCode 2 thread, html_render published four pages and none was shown. For the other three, the script returned something else (one returned the html_preview result), so no turn item carries their htmlRender reference at all.

OpenCode2AdapterV2 registers T3's server through mcp.add without codemode (OpenCode2AdapterV2.ts:3265-3275), so OpenCode's default (true) applies. Setting codemode: false would make each call a direct tool again. It would also put every T3 tool schema into every OpenCode 2 request, the same prefix bloat as #16651, so that is probably not the fix.

Impact

Major degradation or frequent failure

Version or commit

0.0.46-nightly.20261006.2752; also main @ bfec2387b8

Environment

macOS arm64, T3 Code nightly, OpenCode 2.0.24 spawned by T3, opencode-go/muse-spark-1.3-contributor; also opencode-go/deepseek-v4.1-flash

Logs or stack traces

shell
1# Turn item for the call above (statev2.sqlite, orchestration_v2_projection_turn_items)
2type=dynamic_tool status=completed toolName=execute
3output={"htmlRender":{"attachmentId":"<thread>-901e8b83-…-html","title":"Agent roster and redundancy","height":1508,…},"message":"Shown to the reader above your reply. …"}
4
5# Pages on disk for that thread (attachments dir); none appears in the thread
6<thread>-089a2fbf-…-html.html
7<thread>-1ca8810f-…-html.html
8<thread>-901e8b83-…-html.html
9<thread>-e2e8474c-…-html.html

Workaround

None in T3 Code. The page can be opened by hand from the attachments directory.

Related: #16140 / #16142 (OpenCode 2 threads get T3's MCP tools at all), #15736 (Code Mode catalog flapping, a different bug), #16651 (why turning Code Mode off is costly).

Model: Claude Opus 5.5. Harness: Claude Code in T3 Code.

Note

Grok responding on behalf of Julius.

Thanks for the detailed report. I confirmed it against main (checked at 10f39eb9).

Root cause

  • htmlRenderFromToolItem (packages/shared/src/toolOutput.ts:167-175) returns a page only when resolveT3McpToolId(item.toolName) === "html_render". It ignores the output otherwise.
  • OpenCode 2 Code Mode runs T3 tools inside execute, and OpenCodeToolItems.ts:155-167 (apps/server/src/orchestration-v2/Adapters/) projects that as dynamic_tool with toolName: "execute". The name check fails even when the output contains a valid htmlRender reference.
  • OpenCode2AdapterV2.ts:3264-3275 calls client.mcp.add(...) with a RemoteConfig that has no codemode field, so OpenCode's default (Code Mode on) applies.
  • Thread deletion uses the same check: threadHtmlRenderAttachmentIds (apps/server/src/attachmentStore.ts:122-135, called from ProjectionStore.ts:4618 and :5961). That means pages published through execute are orphaned when their thread is deleted.

Proposed fix

  • Don't depend on the wrapping tool's name. The server's html_render MCP handler already knows the thread, so it should record each published page for that thread directly, for example as a thread attachment or a projection entry. Clients would then render it and deletion would clean it up, whatever provider or wrapper ran the call. This also covers scripts that never return the html_render result.
  • Smaller interim option: in htmlRenderFromToolItem, also accept an htmlRender reference from a completed execute item. Keep the existing parseThreadSegmentFromAttachmentId thread-ownership check so a script can't point at another thread's page.
  • Turning Code Mode off (codemode: false) isn't recommended, because every request would then carry every T3 tool schema (see #16651).

Workaround T3 Code has no in-app workaround yet. Published pages are still on disk as <thread>-…-html.html in the attachments directory, so you can open them by hand. Other providers (Claude, Codex, OpenCode 1) render pages normally.

Related, not duplicates: #16140/#16142 (OpenCode 2 MCP wiring, fixed), #15736 (Code Mode catalog flapping), #16651 (schema bloat).

Synced from GitHub

Additional reproduction: Devin through ACP Registry

I reproduced the same user-visible failure on the latest nightly with Devin running through T3's acpRegistry harness, but with a different failure mode from OpenCode's execute wrapper.

Environment

  • T3 Code: 0.0.46-nightly.20261007.2774 (611132c171f3)
  • Provider instance: acpRegistry_devin
  • macOS arm64

Observed behavior

  • html_preview succeeds with zero console errors.
  • html_render succeeds and writes the HTML file under ~/.t3/userdata/attachments/.
  • The projected turn item has the correct recognizable tool name: mcp__t3-code__html_render.
  • Nothing renders in the thread, even after reload/reopen.

The important difference is the persisted output shape. The ACP adapter stores the tool result as:

json
1[
2 {
3 "type": "content",
4 "content": {
5 "type": "text",
6 "text": "{\"htmlRender\":{\"attachmentId\":\"<thread>-<uuid>-html\",\"title\":\"...\",\"height\":408,\"heights\":[...]},\"message\":\"Shown to the reader above your reply...\"}"
7 }
8 }
9]

compactDynamicToolOutput() cannot extract this result. In readResult()'s array branch, it only reads block.text:

ts
1const text = Predicate.isObject(block) ? block.text : undefined;

For the ACP Registry shape, the JSON is at block.content.text, so readResult() receives undefined, htmlRenderFromToolItem() gets no htmlRender reference, and the client never creates an inline render row. The current tests cover { type: "text", text: json } and { content: [{ text: { text: json } }] }, but not [{ type: "content", content: { type: "text", text: json } }].

A narrow compatibility fix would unwrap both block.text and block.content, then extract .text from either object shape. For example:

ts
1const content = Predicate.isObject(block) ? (block.text ?? block.content) : block;
2const text = Predicate.isObject(content) ? (content.text ?? content) : content;
3const result = readResult(text, budget, depth + 1);

This supports the broader fix proposed above: published pages should ideally be recorded directly against the thread, independent of provider-specific result envelopes. Until then, Devin-over-ACP users can successfully publish HTML files but cannot see them inline.

Synced from GitHub

Additional reproductions: acpRegistry_codebuddy_code, plus a second gap beyond the tool name

Reproduced on 0.0.46-nightly.20261008.2801 (macOS arm64) by reading orchestration_v2_projection_turn_items in ~/.t3/userdata/statev2.sqlite for every provider that ran html_render, and replaying each persisted output through readResult's logic:

provider_instance_idrecorded toolNamename gate (resolveT3McpToolId)recorded output shapecompactDynamicToolOutput().htmlRender
codex (native)t3-code.html_render✅{htmlRender:{…}}✅ renders in the thread
acpRegistry_devinmcp__t3-code__html_render✅ (see below)[{type:'content',content:{type:'text',text:'<json>'}}]❌
opencodeexecute❌{htmlRender:{…}}✅ only the name is lost
acpRegistry_codebuddy_codeTool / DeferExecuteTool❌{type:'text',text:'<json>'} bare object❌ would fail even with the right name

Two details that look new relative to the discussion so far:

  1. acpRegistry_codebuddy_code fails both gates, and its output shape is not covered by the narrow fix proposed above. The agent wraps every injected MCP call in its own ToolSearch → DeferExecuteTool layer, so the wire title/kind is just Tool/DeferExecuteTool; the real name only survives inside rawInput.toolName (mcp__t3-code__html_render). Its output is a bare {type:'text',text:'<json>'} object — readResult's object branch looks for structuredContent/content, finds neither, and treats the block itself as data, so data.htmlRender is never read. Unwrapping block.text ?? block.content in the array branch won't reach this shape.

  2. Devin-over-ACP's name gate only passes by accident. extractMcpToolCallIdentity (apps/server/src/provider/acp/AcpRuntimeModel.ts) checks meta.is_mcp_tool_call + rawInput.server/rawInput.tool, meta.serverId/meta.toolName, claudeCode/goose meta, and the wire data.title. Devin's real name lives in meta['cognition.ai/inferenceToolName'] (which normalizeDevinToolCall copies into tool.title) and in rawInput.server_name/tool_name — none of which are candidates. The item therefore lands in the default branch: toolName = presentation title (which resolveT3McpToolId happens to regex-match), and output recorded verbatim with no acpMcpToolCallOutput normalization. Any less canonical title would fail the name gate too.

Both reinforce the systemic direction proposed above: recording the published page against the thread at html_render call time fixes every one of these rows regardless of naming or envelope. If the interim narrow fix lands first, the readResult array branch should recurse into block.content (covers [{type:'content',content:{…}}]) and the object branch should try value.text when no content/structuredContent key exists (covers the bare {type:'text',text} block).

Synced from GitHub

Additional reproduction: Grok Build (grok) wraps html_render in a UseTool MCP envelope

Same user-visible failure on the latest nightly, with a Grok-specific envelope. Codex on the same machine, the same evening, renders the page.

What happened

Asked a Grok thread to publish a page with html_render. html_preview succeeded. html_render succeeded and wrote the HTML file. The thread never showed the page. One attempt painted a frame titled "October 8" that said "Image unavailable". A Codex thread running the same tool showed the page inline.

Diagnosis

Read orchestration_v2_projection_turn_items for the two threads. The Grok item that used the recognizable tool name still does not expose htmlRender where htmlRenderFromToolItem looks.

Grok, inline use_tool call (variant: "UseTool"):

  • toolName: t3-code.html_render (the name gate passes)
  • input keys: tool_name, tool_input, variant. The document is at input.tool_input.html, not input.html
  • output:
json
1{
2 "type": "MCP",
3 "tool_name": "html_render",
4 "server_name": "t3-code",
5 "output": {
6 "OkayOutput": "{\"htmlRender\":{\"attachmentId\":\"<thread>-<uuid>-html\",\"title\":\"October 8\",\"height\":2000,\"heights\":[...]},\"message\":\"Shown to the reader above your reply...\"}"
7 }
8}

OkayOutput is a JSON string. There is no output.htmlRender object.

Codex, same night, native call:

  • toolName: t3-code.html_render
  • input keys: html, height, title
  • output: { "htmlRender": { "attachmentId", "title", "height", "heights" }, "message": "..." }
  • The page renders.

Two earlier Grok attempts passed the HTML through a file. Those items are stored as:

  • toolName: t3-code__html_render — source file: /tmp/mcp-render.json

That name fails resolveT3McpToolId before the envelope is even read. The HTML files were still written (three *-html.html files for this thread under the attachments directory).

This is the same product gap as OpenCode's execute wrapper and the Devin / CodeBuddy envelopes already on this issue: the page is published, then the client only looks for a provider-specific tool name and a provider-specific result shape. Grok adds one more shape, { type: "MCP", output: { OkayOutput: "<json string>" } }, plus a tool name that sometimes has a "source file" suffix.

Recording the published page on the thread inside the html_render handler would cover this row too. A narrower read of the Grok envelope would also need to parse output.output.OkayOutput when it is a string, and to accept the t3-code__html_render — source file: … title or ignore the suffix.

Steps to reproduce

  1. Desktop app, Nightly, provider grok / grok-4.7-build-fast.
  2. Ask the agent to call html_preview, then html_render, with a small self-contained HTML document.
  3. The tool result reports htmlRender.attachmentId and the HTML file is on disk.
  4. The thread does not show the page. Codex html_render in another thread on the same app does.

Version

0.0.46-nightly.20261008.2819

Environment

macOS 26.5.2, T3 Code (Nightly) desktop app. Provider grok, model grok-4.7-build-fast. Compared with provider codex, model gpt-6.1-sol, on the same install.

Evidence

Grok thread 7614335e-d780-4175-9cde-b141a64c16a9. Codex thread where the page did render: bd78657b-5c88-4c5a-bc36-66c88f67f740. Successful Grok attachment id (file exists, page absent in the thread): 7614335e-d780-4175-9cde-b141a64c16a9-b15e5d09-3373-4768-85b7-9b83c9ba25c9-html.

Workaround

None in the Grok thread. The HTML file can be opened from the attachments directory. Codex renders the same tool normally.

Filed by

grok-4.7-build-fast, following the npx t3 triage playbook in-session. Did not launch a nested npx t3 triage agent, because that command starts an interactive Claude or Codex session.

Synced from GitHub

Adding a workaround-availability data point that came up while triaging this, since it constrains what a user can do in the meantime.

The codemode: false escape hatch can't be targeted from user config

OpenCode v2 documents mcp.servers.<name>.codemode: false as the supported way to keep a server's tools on the provider's native list instead of behind Code Mode — which is exactly what this bug needs. It doesn't work for T3's injected server, because the server name embeds the per-thread UUID:

1server=t3-code-40811cb7-f48c-436e-adc4-9fd510f05d36
2server=t3-code-0f6714f2-ddfe-47cf-b822-458eba4dad2f
3server=t3-code-2e4df450-0e8a-40b3-bf7d-d416649cedd0
4...

35 distinct names across my local opencode.log, one per thread. Since config overrides key on server name, no static opencode.json can target it — the name isn't knowable before the thread exists. A wildcard isn't supported either; matching is whole-value with */? only.

So from inside T3 Code there is currently no user-side way to un-nest the call. The two workarounds that do exist are both heavier than this bug warrants:

  • Disable Code Mode entirely for the directory, via permissions: [{ "action": "execute", "resource": "*", "effect": "deny" }]. Worth noting this did not take effect for an in-flight session — opencode debug agents showed the rule resolved for every agent, but the live session's catalog still exposed Code Mode execute and the render still failed. I have not verified whether a freshly-started thread picks it up, so treat that as untested rather than a working workaround.
  • Switch provider instance to one that calls T3 MCP tools natively (Codex was reported working above; I saw the same asymmetry locally — my agent replied "Rendered —" while nothing appeared).

Repro confirmation (OpenCode 2.0.26, macOS arm64)

Matches the opencode row in the table above. Thread 40811cb7-f48c-436e-adc4-9fd510f05d36, two renders in one thread, both silent:

turn_itemrecorded toolNamerecorded output (truncated)file on disk
1000010execute{"htmlRender":{"attachmentId":"40811cb7-…"}}14844 bytes ✅
4000008execute{"attachmentId":"40811cb7-f48c-436e-adc4-9fd510f05d36-6576bf49-…-html"}16244 bytes ✅

Server-side the MCP call is unambiguously successful — ~/.t3/userdata/logs/server.trace.ndjson has the span:

1McpServer.@effect/mcp/2025-06-18/tools/call tool="html_render" exit={"_tag":"Success"}

while the projected assistant messages carry "attachments": [].

One detail that may matter for the fix

OpenCode's persisted metadata.toolCalls for the nested call is { tool, status, input } — there is no result field at all, so the attachmentId is only recoverable from the execute output string. Anyone fixing this on the OpenCode side needs to surface nested results there; the data does not exist in the nested record today.

Happy to test a fresh thread with execute denied if that would help pin down whether the permissions route is viable.

Synced from GitHub

Opened #19029 for this, following the fix proposed above: the html_render handler now records each page in its thread, so pages published from Code Mode scripts show up (including scripts that render more than one page or return something else).

Synced from GitHub

Additional confirmation of the Devin-through-ACP case reported above.

Installed environment

  • T3 Code Nightly 0.0.46-nightly.20261014.3118
  • macOS 27.0.1, arm64
  • Provider: acpRegistry_devin

In the affected thread, html_preview succeeded and html_render returned a valid attachment reference, but no inline page appeared.

The saved result has the same shape:

js
1[{
2 type: "content",
3 content: {
4 type: "text",
5 text: JSON.stringify({ htmlRender: { attachmentId, title, height } })
6 }
7}]

Replaying the saved results through a reproduction of the parser fails to extract Devin’s reference while extracting the Claude Code reference successfully. Unwrapping the ACP content layer allows both to be extracted.

The current main parser still reads only block.text in the array branch. This matches the ACP regression test added in #17031.

This verification used saved results and parser replay; neither proposed PR has been tested in the running app on this installation.

Synced from GitHub

Still reproduces on Nightly 0.0.46-nightly.20261015.3158 (one unrelated commit behind main @ 42da166d13) with OpenCode 2.0.29, macOS arm64 desktop, now that #19029 is closed. opencode-go/deepseek-v4.1-flash, full-access, in a normal thread and a delegate_task child thread: 8 completed execute items hold a valid htmlRender reference and none render. Claude's mcp__t3-code__html_render items on the same install render fine. Found via t3 triage, Claude Opus 5.5 (Claude Code).

Synced from GitHub

Can also confirm this reproduces on Windows with the Antigravity provider/harness (ACP):

  • Environment: Windows 11, Antigravity harness (ACP), model gemini-3.8-flash-high
  • Repro: Invoking html_render via the show-math skill.
  • Observed behavior:
    • html_render succeeds and returns {"htmlRender": {"attachmentId": "...", "title": "...", "height": 2000}}.
    • The HTML attachment is properly created on disk under .t3/userdata/attachments/<attachmentId>.html.
    • html_preview renders it in the headless runner with 0 console errors.
    • However, the chat message bubble completely ignores the attachment and leaves the area above the reply blank.

This further confirms that ACP-wrapped tool call outputs across different providers (Devin, Antigravity) are not having their htmlRender attachment references extracted by the main parser for UI mounting.

Synced from GitHub

Confirmed still present on Windows with T3 Code Nightly 0.1.1-nightly.20261018.3335.

Environment:

  • Windows desktop, T3 Code Nightly.
  • OpenCode provider using Code Mode. Observed chart attempts include opencode/glm-5.3-flash-free and opencode/deepseek-v4.1-flash-free.

Observed behavior and evidence:

  • The agent discovers the thread-scoped html_preview and html_render tools.
  • html_preview returns measured dimensions and clean console output.
  • Successful html_render calls return htmlRender references, and the corresponding .html attachments exist on disk.
  • In two inspected OpenCode chart threads, the persisted completed dynamic-tool item has toolName: "execute", despite its output containing the successful htmlRender reference. The user reports no inline page, while the assistant claims it has rendered.

Representative persisted shape, with thread/attachment identifiers and chart content omitted:

json
1{
2 "type": "dynamic_tool",
3 "status": "completed",
4 "toolName": "execute",
5 "output": "{\"htmlRender\":{\"attachmentId\":\"<redacted>-html\",\"title\":\"Analytics dashboard (dummy data)\",\"height\":899,\"heights\":[...]},\"message\":\"Shown to the reader above your reply...\"}"
6}

Read-only inspection of the installed Nightly's server.asar confirms the bundled server still contains:

js
1function htmlRenderFromToolItem(item) {
2 if (resolveT3McpToolId(item.toolName) !== "html_render") return void 0;
3 const output = compactDynamicToolOutput(item.output);
4 return output?.isError ? void 0 : output?.htmlRender;
5}

The bundled client also retains the equivalent tool-name gate before creating an HTML feed entry. This agrees with the original report: a successful result under execute is not recognized as an inline HTML render.

Control: direct html_preview and html_render calls through Pi on the same installation both returned successfully. No settings, app code, or provider configuration were changed during diagnosis. This is a saved-session and installed-code confirmation plus direct Pi tool checks, not a new automated OpenCode UI reproduction.

Separate errors also appeared in one attempt (Unknown identifier 'html' across separate execute calls and OpenAI Chat stream ended without finish_reason), but these do not account for the other completed render calls with existing attachments.

Please retain Code Mode support when addressing this. Tracking successful publications independently of the outer execute result would also cover scripts that return another value or publish multiple pages, with trusted tool attribution and deduplication of direct calls.

Posted with the user's authorization by their coding assistant. No private thread transcripts, local account paths, credentials, or actual attachment/thread IDs included.

Synced from GitHub

Fresh reproduction on the current nightly (0.1.3-nightly.20261026.3740)

Confirming this is still live on the newest build, and adding one observation that may help whoever fixes it.

Environment

  • T3 Code server 0.1.3-nightly.20261026.3740, macOS arm64
  • Provider instance: opencode (Code Mode on, default)
  • Content: a small hand-written SVG donut chart, no library

Clean A/B within one thread

The same thread published the same HTML through the same tool twice, once per provider:

provider instancerecorded toolNameresult
claudeAgenthtml_render (top-level)page shows inline
opencodeexecute (nested in Code Mode)six renders in a row, all silent

The HTML was byte-identical between them — the OpenCode runs copied the Claude run's exact input, title: "Dev week pie chart", height: 322. Nothing about the content distinguishes the passing case from the failing one; only the recorded toolName does.

Server-side call is definitely succeeding

Every OpenCode render returned the full success payload, including the height table, so the page is laid out correctly before it is dropped:

json
1{"htmlRender":{"attachmentId":"<thread>-…-html","title":"Dev week pie chart","height":322,
2"heights":[[320,492],[375,492],[430,492],[520,322],[640,322],[728,322],[860,322],[1000,322],[1144,322]]},
3"message":"Shown to the reader above your reply. Don't mention or describe the page; reply with only what it doesn't already say."}

Every one of those attachments was published by execute and is therefore not reachable from threadHtmlRenderAttachmentIds, so it should survive deleting the thread — the cleanup leak described in the original report is reproducible too.

Observation: heights is not a reliable success signal

html_render occasionally returned heights: null — three probe renders in a row on this nightly did — while other renders returned the full nine-width table. The success message is identical either way. If any part of the fix keys off the presence of heights, that field looks unreliable independently of this bug.

Not a workaround

preview_open with a file:// URL is rejected (Invalid preview URL (unsupported-protocol: file:)), so the Review pane can't be used to show the page as a fallback either. That, plus the un-targetable codemode: false server name, means an OpenCode thread currently has no user-side route to an inline page at all.

Synced from GitHub

Still happening on desktop nightly 0.1.4-nightly.20261027.3783 (Linux, local server, Devin via acpRegistry, model SWE-2). Nightly 0.1.4-nightly.20261027.3796 has the same packages/shared/src/toolOutput.ts blob (2a80641d13b5e06492e6355c12504a5861bac9fb).

What this session did:

  • html_preview completed and returned a screenshot.
  • html_render completed. Server spans HtmlRender.publish and McpProviderSessions.recordHtmlRender succeeded, and the HTML file was written under the attachments directory.
  • The projected turn item is a dynamic_tool whose toolName and title are both mcp__t3-code__html_render. The output is the ACP envelope:
json
1[{ "type": "content", "content": { "type": "text", "text": "{\"htmlRender\":{...}}" } }]
  • The chat shows the activity row "Rendered an HTML page" and no inline page. That row comes from the tool name. The page is a separate timeline entry.

readResult in packages/shared/src/toolOutput.ts (lines 77–82 on this tag) only reads block.text. This envelope keeps the JSON at block.content.text, so htmlRenderFromToolItem returns undefined and apps/web/src/session-logic.ts never emits an html-render entry.

The adapter recovery in packages/provider-acp/src/server/adapter.ts (the isHtmlRender branch) did not rewrite this output to { htmlRender } either. The stored name is the raw title, which is the fallback dynamic_tool path, not ${server}.${tool}. Devin reports the call as mcp_call_tool with server_name / tool_name / arguments. extractMcpToolCallIdentity looks at rawInput.server / rawInput.tool and data.title, not toolCall.title.

The parser change already suggested here still covers the stored result, including threads that have already finished. Add this envelope next to the others in packages/shared/src/htmlRender.test.ts:

ts
1[
2 "mcp__t3-code__html_render",
3 [{ type: "content", content: { type: "text", text: JSON.stringify(result) } }],
4]

and in readResult, take block.text ?? block.content before reading .text.

Filed from t3 triage. Model: Grok 4.7. Agent: Grok Build.

Synced from GitHub

Note

Written by Claude Opus 5.5 (Claude Code in T3 Code) on behalf of @ethanhfreeman.

For anyone landing here with a fresh repro: #17031 is the fix in progress for both shapes reported recently. @juliusmarminge has taken it over, and it is not merged yet.

  • OpenCode Code Mode (execute whose output carries the htmlRender reference), as in @Schnitzel's A/B: #17031 reads the reference from a completed execute item. It only shows the page when the attachment was published for the item's own thread. It also widens thread-deletion cleanup so these pages stop leaking.
  • ACP content envelope ([{ type: "content", content: { type: "text", text } }]), as in @definitelynotguru's Devin session: #17031's reader unwraps block.content as well as block.text. In that session the item was already named mcp__t3-code__html_render, so the envelope was the only blocker.

Pages are found from the stored turn items, so threads that already finished should show their pages once the fix ships. That's from reading the code; I haven't verified it in a running build.

Still open after #17031: an OpenCode script that publishes a page but never returns html_render's result. Nothing in that item names the page. That case stays tracked here.