feat(capabilities): add ToolDisplayCapability for tool rename + diff event injection - #351
Conversation
…event injection Global decorator capability (aligned with ToolInterceptCapability) that renames assembled tools via pydantic-ai RenamedToolset and injects DiffContentItem-rich ToolCallProgressEvent after tool execution — enabling protocol clients (OpenCode TUI via name whitelist, ACP/Zed via FileEditToolCallContent) to render file-change diffs for third-party capabilities like viking without modifying sub-capability sources. Three orthogonal switches: - rename_mode + name_map: tool renaming (default off-wrapping when empty) - emit_diff + emit_diff_for: diff event injection, filtered by tool name - no-op decorator when both disabled Protocol distinction via assembly-time config: opencode uses rename_mode=true+emit_diff=true, ACP uses rename_mode=false+emit_diff=true, self-emitting capabilities (fsspec) use emit_diff=false.
|
I have verified the critical findings directly against the pinned pydantic-ai API, the ACP converter, and the manifest schema. Here is my synthesized review. Review:
|
… resolve display→original for emit_diff_for Three blocking bugs from PR #351 review, fixed via TDD (red→green): 1. name_map orientation: RenamedToolset expects {new: original} but config uses {original: display}. Now inverts before construction. Verified by test calling get_tools() and asserting display names. 2. tool_call_id empty for capability tools: tool_wrapping.py only populates ctx.deps.tool_call_id for legacy direct tools, not AbstractCapability tools. wrap_tool_execute now sets it from call.tool_call_id before emitting, mirroring tool_wrapping.py:113-114. 3. Namespace incoherence in rename+diff combo: after rename, call.tool_name is the display name but emit_diff_for contains originals. Now reverse-resolves display→original via name_map before matching. Also: type ctx as RunContext[Any] (drop getattr+Any), add __post_init__ list→set coercion for emit_diff_for, add logfire span, fix example YAML to match real schema (agents: dict, url: field, identifier: model format).
…ermination event_processor._process_tool_progress now converts DiffContentItem to unified diff text using lineterm='' + '\n'.join + trailing '\n', matching the format opencode's createTwoFilesPatch produces. Previous code used splitlines(keepends=True) which left the last line without '\n' when content lacked a trailing newline — causing the TUI diff parser to fail with 'Added line count did not match for hunk'. Also: fromfile/tofile both use the file path (no '(old)' suffix), matching createTwoFilesPatch(filePath, filePath, ...). Accumulated in EventProcessorContext.tool_diffs and merged into ToolStateCompleted.metadata.diff by _process_tool_complete. Adds 3 tests: write diff, edit diff, parseable unified diff format.
…h mapping Two changes to make viking_write visible in opencode TUI (option A): 1. event_processor._process_tool_complete: set metadata.diagnostics=[] when diff content exists. The Write component checks 'diagnostics !== undefined' to show a code block of the written content (from props.input.content). Empty diagnostics renders no error messages — just the code block with syntax highlighting. 2. converters._PARAM_NAME_MAP: add 'uri' → 'filePath' so viking tools' 'uri' parameter displays as 'filePath' in the TUI (title, syntax highlighting via filetype()). Aligns with existing 'path'/'file_path' mappings. Together: viking_write now shows a code block of the written content instead of 'Preparing write...', with the viking:// URI as the title.

Summary
New global decorator capability
ToolDisplayCapabilitythat enables protocol clients (OpenCode TUI, ACP/Zed) to render file-change diff views for third-party capabilities (e.g. viking) — without modifying sub-capability sources and with zero protocol changes.Motivation
viking
write/edittools write to a remote store and return plain text. OpenCode TUI renders diffs by a hardcoded tool-name whitelist; ACP renders viaFileEditToolCallContent. Neither matched viking tools since they are custom-named and carry no diff info.Approach
Aligned with the existing
ToolInterceptCapabilitypattern: a standaloneAbstractCapabilityoverridingget_wrapper_toolset()+wrap_tool_execute(), acting as a global middleware over all assembled tools (no child-capability composition).Three orthogonal switches:
rename_mode+name_map: rename tools via pydantic-ai officialRenamedToolset→ hits the TUI whitelist (viking_write→write)emit_diff+emit_diff_for: after real execution, injectToolCallProgressEventwithDiffContentItem(old/new from args) → flows through EventBus → ACPFileEditToolCallContent(proven path, same as fsspec toolset)Protocol distinction via assembly-time config: opencode
rename_mode:true+emit_diff:true, ACPrename_mode:false+emit_diff:true, self-emitting capabilities (fsspec)emit_diff:false.Files
src/agentpool/capabilities/tool_display_capability.py— the capability (+_parse_diff_fieldshelper)pyproject.tomltest_visionfailure unrelated — ALLOW_MODEL_REQUESTS=False)docs/explanation/tool-display-capability.md+ 3-scenario example YAML + AGENTS.md entryVerification
ruff check/ruff format --check/mypy --strictall greentest_vision, pre-existing, needs real model — confirmed failing on main too)