Repository navigation
[Bug]: html_render pages never show for OpenCode 2 threads because Code Mode wraps the call in execute #16745
Description
Activity
Note
Grok responding on behalf of Julius.
Thanks for the detailed report. I confirmed it against
main(checked at10f39eb9).Root cause
htmlRenderFromToolItem(packages/shared/src/toolOutput.ts:167-175) returns a page only whenresolveT3McpToolId(item.toolName) === "html_render". It ignores the output otherwise.- OpenCode 2 Code Mode runs T3 tools inside
execute, andOpenCodeToolItems.ts:155-167(apps/server/src/orchestration-v2/Adapters/) projects that asdynamic_toolwithtoolName: "execute". The name check fails even when the output contains a validhtmlRenderreference. OpenCode2AdapterV2.ts:3264-3275callsclient.mcp.add(...)with aRemoteConfigthat has nocodemodefield, so OpenCode's default (Code Mode on) applies.- Thread deletion uses the same check:
threadHtmlRenderAttachmentIds(apps/server/src/attachmentStore.ts:122-135, called fromProjectionStore.ts:4618and:5961). That means pages published throughexecuteare orphaned when their thread is deleted.
Proposed fix
- Don't depend on the wrapping tool's name. The server's
html_renderMCP 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 thehtml_renderresult. - Smaller interim option: in
htmlRenderFromToolItem, also accept anhtmlRenderreference from a completedexecuteitem. Keep the existingparseThreadSegmentFromAttachmentIdthread-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 [Bug]: Pi sessions declare every t3-code tool on every turn #16651).
Workaround
T3 Code has no in-app workaround yet. Published pages are still on disk as<thread>-…-html.htmlin 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).
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
on Oct 7, 2026 Additional reproduction: Devin through ACP Registry
I reproduced the same user-visible failure on the latest nightly with Devin running through T3's
acpRegistryharness, but with a different failure mode from OpenCode'sexecutewrapper.Environment
- T3 Code:
0.0.46-nightly.20261007.2774(611132c171f3) - Provider instance:
acpRegistry_devin - macOS arm64
Observed behavior
html_previewsucceeds with zero console errors.html_rendersucceeds 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:
[ { "type": "content", "content": { "type": "text", "text": "{\"htmlRender\":{\"attachmentId\":\"<thread>-<uuid>-html\",\"title\":\"...\",\"height\":408,\"heights\":[...]},\"message\":\"Shown to the reader above your reply...\"}" } } ]compactDynamicToolOutput()cannot extract this result. InreadResult()'s array branch, it only readsblock.text:const text = Predicate.isObject(block) ? block.text : undefined;
For the ACP Registry shape, the JSON is at
block.content.text, soreadResult()receivesundefined,htmlRenderFromToolItem()gets nohtmlRenderreference, 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.textandblock.content, then extract.textfrom either object shape. For example:const content = Predicate.isObject(block) ? (block.text ?? block.content) : block; const text = Predicate.isObject(content) ? (content.text ?? content) : content; const 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.
Reacted by Ethan Hyatt Freeman- T3 Code:
Additional reproductions:
acpRegistry_codebuddy_code, plus a second gap beyond the tool nameReproduced on
0.0.46-nightly.20261008.2801(macOS arm64) by readingorchestration_v2_projection_turn_itemsin~/.t3/userdata/statev2.sqlitefor every provider that ranhtml_render, and replaying each persistedoutputthroughreadResult's logic:provider_instance_id recorded toolNamename gate ( resolveT3McpToolId)recorded outputshapecompactDynamicToolOutput().htmlRendercodex(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:
-
acpRegistry_codebuddy_codefails 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 wiretitle/kindis justTool/DeferExecuteTool; the real name only survives insiderawInput.toolName(mcp__t3-code__html_render). Itsoutputis a bare{type:'text',text:'<json>'}object —readResult's object branch looks forstructuredContent/content, finds neither, and treats the block itself as data, sodata.htmlRenderis never read. Unwrappingblock.text ?? block.contentin the array branch won't reach this shape. -
Devin-over-ACP's name gate only passes by accident.
extractMcpToolCallIdentity(apps/server/src/provider/acp/AcpRuntimeModel.ts) checksmeta.is_mcp_tool_call+rawInput.server/rawInput.tool,meta.serverId/meta.toolName,claudeCode/goosemeta, and the wiredata.title. Devin's real name lives inmeta['cognition.ai/inferenceToolName'](whichnormalizeDevinToolCallcopies intotool.title) and inrawInput.server_name/tool_name— none of which are candidates. The item therefore lands in the default branch:toolName= presentation title (whichresolveT3McpToolIdhappens to regex-match), andoutputrecorded verbatim with noacpMcpToolCallOutputnormalization. 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_rendercall time fixes every one of these rows regardless of naming or envelope. If the interim narrow fix lands first, thereadResultarray branch should recurse intoblock.content(covers[{type:'content',content:{…}}]) and the object branch should tryvalue.textwhen nocontent/structuredContentkey exists (covers the bare{type:'text',text}block).-
Additional reproduction: Grok Build (
grok) wrapshtml_renderin a UseTool MCP envelopeSame 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_previewsucceeded.html_rendersucceeded 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_itemsfor the two threads. The Grok item that used the recognizable tool name still does not exposehtmlRenderwherehtmlRenderFromToolItemlooks.Grok, inline
use_toolcall (variant: "UseTool"):toolName:t3-code.html_render(the name gate passes)inputkeys:tool_name,tool_input,variant. The document is atinput.tool_input.html, notinput.htmloutput:
{ "type": "MCP", "tool_name": "html_render", "server_name": "t3-code", "output": { "OkayOutput": "{\"htmlRender\":{\"attachmentId\":\"<thread>-<uuid>-html\",\"title\":\"October 8\",\"height\":2000,\"heights\":[...]},\"message\":\"Shown to the reader above your reply...\"}" } }OkayOutputis a JSON string. There is nooutput.htmlRenderobject.Codex, same night, native call:
toolName:t3-code.html_renderinputkeys:html,height,titleoutput:{ "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
resolveT3McpToolIdbefore the envelope is even read. The HTML files were still written (three*-html.htmlfiles for this thread under the attachments directory).This is the same product gap as OpenCode's
executewrapper 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_renderhandler would cover this row too. A narrower read of the Grok envelope would also need to parseoutput.output.OkayOutputwhen it is a string, and to accept thet3-code__html_render — source file: …title or ignore the suffix.Steps to reproduce
- Desktop app, Nightly, provider
grok/grok-4.7-build-fast. - Ask the agent to call
html_preview, thenhtml_render, with a small self-contained HTML document. - The tool result reports
htmlRender.attachmentIdand the HTML file is on disk. - The thread does not show the page. Codex
html_renderin another thread on the same app does.
Version
0.0.46-nightly.20261008.2819Environment
macOS 26.5.2, T3 Code (Nightly) desktop app. Provider
grok, modelgrok-4.7-build-fast. Compared with providercodex, modelgpt-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 triageplaybook in-session. Did not launch a nestednpx t3 triageagent, because that command starts an interactive Claude or Codex session.Can confirm on opencode v2 and antigravity ACP.
Before submitting
Area
apps/server
Steps to reproduce
executetool).html_render."executewith a script that callstools["t3-code-<thread>"].html_preview(...)andtools["t3-code-<thread>"].html_render(...).The simplest form that still fails is a script whose output is exactly
html_render's result: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
executework row, which folds away when the turn completes.html_renderstill 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@ bfec238:htmlRenderFromToolItem(packages/shared/src/toolOutput.ts:168-175). That requires a completeddynamic_toolitem whose tool name resolves tohtml_render.executeand reports only that one tool: no events or parts for the nested calls, and itsmetadata.toolCallslists{ tool, status, input }without results. The adapter projects it asdynamic_toolwithtoolName: "execute"(OpenCodeToolItems.ts:155-167), so the name check fails even when the script returns the reference. In the example above, theexecuteitem's stored output is exactly{"htmlRender":{"attachmentId":"…-html","title":"Agent roster and redundancy","height":1508,"heights":[…]},"message":"Shown to the reader…"}, and nothing renders.threadHtmlRenderAttachmentIdsinapps/server/src/attachmentStore.ts:118-135, used byProjectionStore.getThreadAttachmentIds). Pages published fromexecuteare never cleaned up.In one OpenCode 2 thread,
html_renderpublished four pages and none was shown. For the other three, the script returned something else (one returned thehtml_previewresult), so no turn item carries theirhtmlRenderreference at all.OpenCode2AdapterV2registers T3's server throughmcp.addwithoutcodemode(OpenCode2AdapterV2.ts:3265-3275), so OpenCode's default (true) applies. Settingcodemode: falsewould 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 @ bfec238
Environment
macOS arm64, T3 Code nightly, OpenCode 2.0.24 spawned by T3,
opencode-go/muse-spark-1.3-contributor; alsoopencode-go/deepseek-v4.1-flashLogs or stack traces
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.