Repository navigation
Tool visualization: factory-based tool render with inline inspector #4
Description
Summary
Implement the tool-visualization feature per \docs/design/tool-visualization.md.
The implementation targets the browser-hosted HTML renderer introduced in #32. The earlier scope referring to \SelectableTextBlockChatOutputModels.cs, \ChatMessageDocumentModel.cs, and two-container (\SelectableTextBox\ / \FlowDocument) modes is obsolete — those files have been removed. The sole rendering path is now \ChatOutputHtmlRenderer\ / \ChatOutputHtmlModel\ / \ChatMessageHtmlModel.
Scope
- Add \IToolVisualizerFactory, \CompositeToolVisualizerFactory, and \ToolVisualizationContext.
- \containerType\ is removed from the contract — there is only one renderer.
- \Visualize\ returns HTML string fragments (or
ull\ for the existing fallback).
- Add \Summary\ record: concise label + optional HTML body, rendered as an expanded <details>\ element.
- Add \ToolVisualizationInterpreter\ — translates the factory return value into an HTML string for \ChatOutputHtmlRenderer.RenderContent.
- Refactor the hard-coded tool switch in \ChatOutputHtmlRenderer.RenderContent\ (currently \FunctionCallContent\ → \RenderCollapsible\ and \FunctionResultContent\ → \RenderCollapsible) to go through the factory/interpreter instead.
- Add \WorkspaceVisualizerFactory\ for Phantom.Workspaces embedded tools and filesystem MCP tools.
- Add \CopilotToolVisualizerFactory\ with \gh-command-specific renderers as per the design doc.
- Add the per-content inline inspector affordance: a small clickable element rendered in the HTML that posts a JS message to the host via the WebView bridge. The host opens an Avalonia \AIContentInspectorWindow\ showing full metadata + raw payload.
Constraints
- Single rendering path: HTML emitted through \ChatOutputHtmlRenderer\ / the WebView bridge. No Avalonia inline/block tree involvement.
- Inspector affordance must use the existing JS→host message bridge (\ControllableWebViewControl\ / \ChatOutputBrowserCommands) rather than embed Avalonia controls in the WebView.
- \IToolVisualizerFactory\ and related contracts live in \Phantom.Workspaces.Agent.Gui; no changes required to \Phantom.Workspaces.Llm.Core.
- \ComputeContentKey\ in \ChatOutputHtmlRenderer\ must be kept consistent with any new content shapes added by the factory (so incremental diff still works correctly).
Acceptance
- Tool renderings are produced via the factory rather than inline switch statements in \ChatOutputHtmlRenderer.
- \Summary\ renders as an expanded <details>\ element with the label as <summary>\ and the body as the content.
- Inspector affordance renders on every content block; clicking it opens the full inspector with metadata + raw payload.
- Tests cover factory dispatch, interpreter HTML output, and the JS bridge inspector trigger.
Design doc
\docs/design/tool-visualization.md\ — already updated with the HTML-renderer note. The \IToolVisualizerFactory\ contract section, concrete factory specs, \gh\ tool catalog, and test task list there remain authoritative; the \containerType\ references and two-mode renderer integration sections in that doc are superseded.
Activity
JoshuaRowePhantom commentedon Jun 25, 2026
Additional scope: tool-driven GUI status updates
Some tools want to push a brief status string to the agent status line rather than (or in addition to) producing a chat content block. The canonical example is a
report_intent-style tool: it takes a short description of what the agent is currently doing and updates a status field visible at a glance in the UI, without adding noise to the chat transcript.New
IToolVisualizerFactoryside-channel:StatusUpdateExtend the visualization result interpretation table with a new return type:
StatusUpdateAgentChatStatusLineViewModel; produce no chat content block (or optionally a collapsed one).AgentChatStatusLineViewModelchangesAdd a new
IntentDisplayproperty (nullable string) populated fromStatusUpdate(Intent, ...)results:IsThinkingtransitions tofalse(i.e. the run completes).ToolVisualizationContextchangesNo changes needed —
StatusUpdateis a pure output type returned byVisualize.Routing
StatusUpdatethrough the rendering pipelineChatMessageHtmlModel.Render(or the transformer) callsIToolVisualizerFactory.Visualizefor each content block. When the result is aStatusUpdate:ChatSummaryis non-null, emit a collapsed<details>block (same as the existing generic tool-call fallback) withChatSummaryas the label.AgentChatStatusLineViewModelvia a newIAgentStatusSinkinterface injected intoChatOutputHtmlModel— the same sink pattern already used byIChatOutputHtmlSink.AgentChatOutputControl(orAgentViewModel) implementsIAgentStatusSinkand forwards toAgentChatStatusLineViewModelon the UI thread.report_intenttool visualizerAdd a
ReportIntentToolVisualizerinsideCopilotToolVisualizerFactory(or a dedicatedBuiltinToolVisualizerFactory) that matches:FunctionCallContentwhereName == "report_intent"and arguments contain"intent"stringStatusUpdate(AgentStatusField.Intent, intentText, ChatSummary: null)— updates the status line silently with no chat blockFunctionResultContentfor the matching call — returnsnull(no rendering needed)This means
report_intentcalls become completely invisible in the chat transcript but their current value is always live in the status line while the agent is running.JoshuaRowePhantom commentedon Jun 27, 2026
Design
Approach: Introduce a factory-based visualization layer for tool calls/results, with a status-line side-channel for silent status updates.
Architecture
Core contracts in
ViewModels/Visualization/:IToolVisualizerFactorywithVisualize(ToolVisualizationContext)returningobject?ToolVisualizationContext(AIContent Content)Summary(string Label, string? HtmlBody = null)- renders as<details open>StatusUpdate(AgentStatusField Field, string? Value, string? ChatSummary = null)CompositeToolVisualizerFactory,ToolVisualizationInterpreterWorkspaceVisualizerFactory,CopilotToolVisualizerFactoryIAgentStatusSinkinjected intoChatOutputHtmlModel;AgentChatOutputControlimplements itAgentChatStatusLineViewModelgainsIntentDisplay(nullable string), cleared whenIsThinkingtransitions to falseChatOutputHtmlRenderer.RenderContentdelegates tool content toToolVisualizationInterpreterInspector affordance: every content block HTML includes a small inspect button; clicking posts JS message; host opens
AIContentInspectorWindowJoshuaRowePhantom commentedon Jun 27, 2026
Tests
Phantom.Workspaces.Agent.Gui.TestsCompositeToolVisualizerFactory_ReturnsFirstNonNullCompositeToolVisualizerFactory_ReturnsNull_WhenAllReturnNullCompositeToolVisualizerFactory_RespectsOrderToolVisualizationInterpreter_Summary_RendersDetailsElementSummary("label", "<b>body</b>")→ HTML contains<details open,<summary>label</summary>, bodyToolVisualizationInterpreter_NullResult_UsesGenericCollapsibleFallbackToolVisualizationInterpreter_StatusUpdate_PostsToSinkStatusUpdate(Intent, "doing something", null)→ sink receivesUpdateStatus(Intent, "doing something"), no HTML emittedToolVisualizationInterpreter_StatusUpdate_WithChatSummary_EmitsCollapsedDetailsAndPostsToSinkStatusUpdate(Intent, "val", "summary label")→ HTML is collapsed<details>, sink also receives updateCopilotToolVisualizerFactory_ReportIntent_FunctionCall_ReturnsStatusUpdateFunctionCallContent("call-1", "report_intent", {"intent": "analyzing code"})→StatusUpdate(Intent, "analyzing code", null)CopilotToolVisualizerFactory_ReportIntent_FunctionResult_ReturnsNullFunctionResultContent("call-1", "ok")from a report_intent call → nullAgentChatStatusLineViewModel_IntentDisplay_SetByUpdateStatusIntentDisplayto given valueAgentChatStatusLineViewModel_IntentDisplay_ClearedWhenThinkingStopsChatOutputHtmlRenderer_InspectorAffordance_PresentOnFunctionCallBlockRenderContentfor FunctionCallContent includeschat-inspectbuttonChatOutputHtmlModel_InspectMessage_HandledByBridgeAIContentInspectorWindowopen request raisedFix #4: Factory-based tool visualization with inline inspector
JoshuaRowePhantom commentedon Jun 30, 2026
Verification failed
Checked: Commit
5dae2ed(Fix #4), commited49564(Fix #293), files onfeaturesbranch tip:ViewModels/Visualization/:IToolVisualizerFactory,CompositeToolVisualizerFactory,ToolVisualizationContext,Summary,StatusUpdate,ToolVisualizationInterpreter,WorkspaceVisualizerFactory,CopilotToolVisualizerFactoryViewModels/IAgentStatusSink.cs,ViewModels/AgentChatStatusLineViewModel.cs(IntentDisplay)ViewModels/DocumentModels/ChatOutputHtmlRenderer.cs(factory delegation)Controls/AIContentInspectorWindow.axaml.csControls/AgentChatOutputControl.axaml.csAgent.Gui.Tests/ToolVisualizationTests.csFound: The factory-based visualization system is fully implemented and tested:
IToolVisualizerFactory,CompositeToolVisualizerFactory,ToolVisualizationContext,Summary,StatusUpdate,AgentStatusFieldToolVisualizationInterpreterroutes factory results to HTML or the status sinkWorkspaceVisualizerFactoryandCopilotToolVisualizerFactory(includingreport_intent->StatusUpdate(Intent))AgentChatStatusLineViewModel.IntentDisplayset byUpdateStatus, cleared whenIsThinking-> falseChatOutputHtmlRenderer.RenderContentdelegatesFunctionCallContent/FunctionResultContentthrough the factoryAIContentInspectorWindowexists (window class and AXAML)InspectorRequestedevent declared and fired inAgentChatOutputControlfor JSinspectmessagesMissing: Acceptance criterion 3 — Inspector affordance renders on every content block; clicking it opens the full inspector with metadata + raw payload — is not met:
chat-inspectHTML button was removed by issue Bug: old inline '...' button still visible after #155 - should have been removed #293 (ed49564: "remove old inline inspector affordance button"). The JSinspectmessage is therefore never sent from the WebView.AgentChatOutputControl.InspectorRequestedhas no subscriber anywhere in the codebase —AgentViewModeldoes not wire it up, so even if the JS message were sent, theAIContentInspectorWindowwould never be opened.ChatOutputHtmlModel_InspectMessage_HandledByBridge(specified in the issue test plan) is absent.The
AIContentInspectorWindowand theInspectorRequestedevent declaration are orphaned code with no working trigger.