Skip to content
Back to skills

491 Tool Renderers 334d2bcb

ASecurity

**Date:** 2026-01-07 **Scope:** UI / Renderers **Status:** Canonical

  • 9 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added June 1, 2026
testingpythonbashtesting

Works with

  • terminal

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned June 1, 2026

npx -y skills add tools-only/X-Skills --skill 491-tool_renderers_334d2bcb --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 491 Tool Renderers 334d2bcb?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 491 Tool Renderers 334d2bcb
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-491-tool-renderers-334d2bcb/badge)](https://www.skillsdirectory.com/skills/tools-only-491-tool-renderers-334d2bcb)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# Tool Renderer Architecture

**Date:** 2026-01-07
**Scope:** UI / Renderers
**Status:** Canonical

## Overview

Tool renderers transform raw tool output into structured NeXTSTEP-style panels. All renderers follow a uniform 4-zone layout pattern and share common infrastructure via `BaseToolRenderer`.

**Location:** `src/tunacode/ui/renderers/tools/`

## The 4-Zone Layout

Every tool panel follows this structure:

```
┌──────────────────────────────────────────────────┐
│ [bold]tool_name[/]   status info                 │  Zone 1: Header
│ param1: value1   param2: value2                  │  Zone 2: Params
│ ──────────                                       │  Separator
│ main content                                     │  Zone 3: Viewport
│ ──────────                                       │  Separator
│ info1   info2   123ms                            │  Zone 4: Status
└──────────────────────────────────────────────────┘
  09:41:23                                           Subtitle (timestamp)
```

### Zone Responsibilities

| Zone | Purpose | Example |
|------|---------|---------|
| Header | Tool identity + result summary | `mydir   45 files  12 dirs` |
| Params | Input parameters used | `hidden: off  max: 100` |
| Viewport | Primary content (padded to min height) | Tree, diff, matches |
| Status | Truncation info, metrics, timing | `[26/100 lines]  145ms` |

## Base Classes

### BaseToolRenderer[T]

Abstract base class implementing the template method pattern. Subclasses provide tool-specific logic; the base handles composition.

```python
from tunacode.ui.renderers.tools import BaseToolRenderer, RendererConfig

class MyToolRenderer(BaseToolRenderer[MyToolData]):
    def parse_result(self, args, result) -> MyToolData | None:
        """Parse raw output into structured data."""
        ...

    def build_header(self, data, duration_ms) -> Text:
        """Zone 1: tool name + summary."""
        ...

    def build_params(self, data) -> Text | None:
        """Zone 2: input parameters."""
        ...

    def build_viewport(self, data) -> RenderableType:
        """Zone 3: main content."""
        ...

    def build_status(self, data, duration_ms) -> Text:
        """Zone 4: metrics and timing."""
        ...
```

### Optional Overrides

```python
def get_border_color(self, data) -> str:
    """Panel border color. Default: success green."""
    return self.config.warning_color if data.has_error else self.config.success_color

def get_status_text(self, data) -> str:
    """Title status text. Default: 'done'."""
    return f"exit {data.exit_code}" if data.exit_code != 0 else "done"
```

## Helper Functions

Shared utilities eliminate duplication across renderers:

```python
from tunacode.ui.renderers.tools import truncate_line, truncate_content, pad_lines

# Truncate single line with ellipsis
line = truncate_line("very long line...", max_width=60)

# Truncate multi-line content, returns (content, shown, total)
content, shown, total = truncate_content(raw_output, max_lines=10)

# Pad to minimum viewport height
lines = pad_lines(content.split("\n"), min_lines=4)
```

## Preview Script

Use the preview script to render tool panels without running the full TUI:

```bash
uv run python scripts/preview_tool_panels.py --list
uv run python scripts/preview_tool_panels.py --all --width 92
uv run python scripts/preview_tool_panels.py --scenario grep-basic --scenario bash-error
```

The script uses `tool_panel_smart()` with canned outputs to exercise common scenarios
(success, truncation, diagnostics, and error fallback).

## Registry Pattern

Renderers self-register using the `@tool_renderer` decorator:

```python
from tunacode.ui.renderers.tools import tool_renderer

@tool_renderer("my_tool")
def render_my_tool(args, result, duration_ms=None):
    """Render my_tool output."""
    return _renderer.render(args, result, duration_ms)
```

### Lookup Functions

```python
from tunacode.ui.renderers.tools import list_renderers

# List all registered renderers
names = list_renderers()  # ["bash", "list_dir", ...]
```

## Creating a New Renderer

1. Create `src/tunacode/ui/renderers/tools/my_tool.py`
2. Define a dataclass for parsed data
3. Implement `BaseToolRenderer[MyData]`
4. Create module-level instance and render function
5. Register with `@tool_renderer`
6. Export from `__init__.py`
7. Add to `UNIFIED_RENDERERS` in `tests/test_base_renderer.py`

### Minimal Example

```python
"""Renderer for my_tool output."""

from dataclasses import dataclass
from typing import Any

from rich.console import RenderableType
from rich.text import Text

from tunacode.ui.renderers.tools.base import (
    BaseToolRenderer,
    RendererConfig,
    pad_lines,
    tool_renderer,
    truncate_content,
)


@dataclass
class MyToolData:
    """Parsed my_tool result."""
    name: str
    content: str
    count: int


class MyToolRenderer(BaseToolRenderer[MyToolData]):
    def parse_result(self, args, result) -> MyToolData | None:
        if not result:
            return None
        return MyToolData(name="example", content=result, count=len(result.splitlines()))

    def build_header(self, data, duration_ms) -> Text:
        header = Text()
        header.append(data.name, style="bold")
        header.append(f"   {data.count} items", style="dim")
        return header

    def build_params(self, data) -> Text | None:
        return None  # No params to display

    def build_viewport(self, data, max_line_width: int) -> RenderableType:
        content, _, _ = truncate_content(data.content, max_width=max_line_width)
        lines = pad_lines(content.split("\n"))
        return Text("\n".join(lines))

    def build_status(self, data, duration_ms, max_line_width: int) -> Text:
        items = []
        if duration_ms:
            items.append(f"{duration_ms:.0f}ms")
        return Text("  ".join(items), style="dim")


_renderer = MyToolRenderer(RendererConfig(tool_name="my_tool"))


@tool_renderer("my_tool")
def render_my_tool(args, result, duration_ms, max_line_width) -> RenderableType | None:
    return _renderer.render(args, result, duration_ms, max_line_width)
```

## Width Handling

Panel widths are calculated centrally in `panel_widths.py` (PR #244). This follows Gate 5: explicit widths instead of `expand=True` indirection.

### Width Flow

```
TextualReplApp.on_tool_result_display()
  → computes max_line_width from viewport
  → calls tool_panel_smart(args, result, duration_ms, max_line_width)
    → renderer receives max_line_width as parameter
    → renderer uses max_line_width for content truncation
    → tool_panel_frame_width(max_line_width) computes Panel width
```

### Key Function

```python
from tunacode.ui.renderers.panel_widths import tool_panel_frame_width

# Compute explicit panel frame width
frame_width = tool_panel_frame_width(max_line_width)
Panel(content, width=frame_width)  # explicit, verifiable
```

**Why not `expand=True`?** The `expand` parameter expands to `scrollable_content_region`, not terminal width. RichLog's internal padding subtracts 4-8 chars, making panels narrower than expected. Explicit widths are verifiable.

## Constants

Defined in `tunacode.constants`:

| Constant | Value | Purpose |
|----------|-------|---------|
| `MAX_PANEL_LINES` | 20 | Max lines in generic panels |
| `TOOL_VIEWPORT_LINES` | 8 | Max lines in tool viewport |
| `MIN_VIEWPORT_LINES` | 3 | Minimum viewport height |
| `MIN_TOOL_PANEL_LINE_WIDTH` | 4 | Minimum tool panel line width |
| `TOOL_PANEL_HORIZONTAL_INSET` | 8 | Width reserved for borders/padding |

Defined in `panel_widths.py`:

| Function | Returns | Purpose |
|----------|---------|---------|
| `tool_panel_frame_width(max_line_width)` | `max_line_width + TOOL_PANEL_HORIZONTAL_INSET` | Panel frame width |

Tool panel line width is derived from the available viewport width and passed into renderers.

Defined in `base.py`:

| Constant | Value | Purpose |
|----------|-------|---------|
| `BOX_HORIZONTAL` | `─` | Separator character |
| `SEPARATOR_WIDTH` | 10 | Separator line width |

## Current Renderers

| Tool | Status | Renderer Class |
|------|--------|----------------|
| `list_dir` | Unified | `ListDirRenderer` |
| `bash` | Unified | `BashRenderer` |
| `read_file` | Legacy | (function) |
| `update_file` | Legacy | (function) |
| `glob` | Legacy | (function) |
| `grep` | Legacy | (function) |
| `web_fetch` | Legacy | (function) |
| `research_codebase` | Legacy | (function) |

## Testing & Enforcement

Renderers using `BaseToolRenderer` are enforced via `tests/test_base_renderer.py`:

```python
UNIFIED_RENDERERS = {
    "list_dir": ListDirRenderer,
    "bash": BashRenderer,
}
```

Tests verify:
- Each renderer inherits from `BaseToolRenderer`
- Each renderer is registered in the registry

When migrating a renderer, add it to `UNIFIED_RENDERERS` dict.

## Verification Checklist

When creating or modifying a renderer:

- [ ] Inherits from `BaseToolRenderer`
- [ ] Avoid horizontal pre-truncation; rely on Rich wrapping for width changes
- [ ] Uses shared helpers (no local `_truncate_line`)
- [ ] Registered with `@tool_renderer`
- [ ] Added to `UNIFIED_RENDERERS` in test file
- [ ] Exported from `__init__.py`

Files in this skill

  • README.md820 B
  • skill.md9.3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.