# Centralized Panel Width Handling

> PR #244 centralized panel width calculations into a single module (panel_widths.py) and removed redundant width logic from 9+ tool renderers.

- Skill: `tools-only/centralized-panel-width-handling` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/centralized-panel-width-handling`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/centralized-panel-width-handling/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-29
- Page: https://skillmd.com/skills/tools-only/centralized-panel-width-handling

---


# Centralized Panel Width Handling

## Summary

PR #244 centralized panel width calculations into a single module (`panel_widths.py`) and removed redundant width logic from 9+ tool renderers. This refactoring applies Gate 5 (Indirection Requires Verification) by replacing `expand=True` indirection with explicit `width=` parameters.

## Context

- File: `src/tunacode/ui/renderers/panel_widths.py`
- PR: #244
- Commit: `f0bfbb1`
- Surfaces: Tool panels now have consistent, verifiable widths

## Root Cause

**Indirection hiding actual behavior.**

Tool renderers were using `Panel(content, expand=True)` and trusting that panels would fill the terminal width. But:

1. `expand=True` expands to `scrollable_content_region`, not terminal width
2. RichLog's internal padding/gutter subtracted 4-8 chars
3. Each renderer had its own width calculation logic
4. No single place controlled the final panel width

This violated Gate 5: we were passing a wish (`expand=True`) instead of verifying the output.

```
BEFORE (indirect - unverifiable):
┌─────────────────────────────────────────────────────────────┐
│ Panel(content, expand=True)                                 │
│   → RichLog.write(panel)                                    │
│   → RichLog decides width based on scrollable_content_region│
│   → Panel renders narrower than expected                    │
│   → No way to verify what width was actually used           │
└─────────────────────────────────────────────────────────────┘

AFTER (direct - verifiable):
┌─────────────────────────────────────────────────────────────┐
│ frame_width = tool_panel_frame_width(max_line_width)        │
│ Panel(content, width=frame_width)                           │
│   → Width is explicit, testable, traceable                  │
│   → panel_widths.py is the single source of truth           │
└─────────────────────────────────────────────────────────────┘
```

## Changes

### New Module: `panel_widths.py`

```python
def tool_panel_frame_width(max_line_width: int) -> int:
    """Return tool panel frame width including borders and padding.

    Preconditions:
        - max_line_width is already clamped to a positive value.
    """
    return max_line_width + TOOL_PANEL_HORIZONTAL_INSET
```

### Simplified Renderers

9 tool renderers were simplified by removing local width calculations:

| Renderer | Lines Removed | Change |
|----------|---------------|--------|
| `glob.py` | ~20 | Removed width calculation |
| `grep.py` | ~15 | Removed width calculation |
| `read_file.py` | ~10 | Uses passed `max_line_width` |
| `update_file.py` | ~15 | Uses passed `max_line_width` |
| `write_file.py` | ~5 | Uses passed `max_line_width` |
| `list_dir.py` | ~5 | Uses passed `max_line_width` |
| `bash.py` | ~3 | Uses passed `max_line_width` |
| `research.py` | ~10 | Uses passed `max_line_width` |
| `web_fetch.py` | ~3 | Uses passed `max_line_width` |

### Base Renderer Changes

`base.py` now consistently passes `max_line_width` through the render chain:

```python
def render(self, args, result, duration_ms, max_line_width) -> RenderableType | None:
    # max_line_width flows to build_viewport() and build_status()
```

### Truncation Logic

`panels.py` simplified `_truncate_content()` - it now trusts the width passed to it rather than computing its own.

## Behavioral Impact

- Tool panels render at consistent, predictable widths
- Width is verifiable: `max_line_width + TOOL_PANEL_HORIZONTAL_INSET`
- Single place to change width policy (`panel_widths.py`)
- Renderers are simpler, focused on content not layout math

## Gate 5 Application

This refactoring demonstrates Gate 5: **Indirection Requires Verification**.

| Before | After |
|--------|-------|
| `expand=True` - who decides? | `width=frame_width` - explicit |
| 9 different width calculations | 1 function in `panel_widths.py` |
| Can't verify actual width | Width is a traceable value |
| Trust the framework | Verify the output |

**Rule:** When delegating a decision (like "expand to fill"), verify the OUTPUT, not just the INPUT. If you can't observe the final value, you're guessing.

## Files Changed

- `src/tunacode/ui/renderers/panel_widths.py` (new)
- `src/tunacode/ui/renderers/tools/*.py` (9 renderers simplified)
- `src/tunacode/ui/renderers/panels.py` (truncation logic)
- `src/tunacode/ui/renderers/tools/base.py` (hook-arrow pattern)
- `src/tunacode/ui/app.py` (cleanup)
- `tests/*.py` (updated)

## Related Cards

- [[tool-renderers]] - Tool renderer architecture
- [[ui-overview]] - UI module documentation
- [[nextstep_panels]] - NeXTSTEP design principles

