<div align="center">

# pi-tool-display

[![License](https://img.shields.io/github/license/MasuRii/pi-tool-display?style=for-the-badge)](LICENSE)
[![Platform](https://img.shields.io/badge/Platform-macOS%20%7C%20Linux%20%7C%20Windows-blue?style=for-the-badge)]()

OpenCode-style tool rendering for the Selesai coding agent.

</div>

> **Selesai-owned fork.** This copy is vendored from upstream
> [`MasuRii/pi-tool-display`](https://github.com/MasuRii/pi-tool-display) `v0.5.0`, anchored locally as
> the tag `vendor/pi-tool-display/v0.5.0`. Selesai maintains it; there is no active upstream sync and
> Selesai does not publish this copy to npm. The original work is MIT-licensed and the upstream
> attribution is preserved in `LICENSE` and in the historical changelog entries.

`pi-tool-display` keeps tool calls compact, adds richer diff rendering for file edits, and improves a
few core chat UI details such as thinking labels and the native user prompt box. It is **render-only**:
it changes how tools look, never what a tool is or what the model sees.

## Features

- **Compact built-in rendering** for `read`, `grep`, `find`, `ls`, `bash`, `powershell`, `edit`, and
  `write`
- **Native MCP rendering** for `mcp__<server>__<tool>` tools, with `hidden`, `summary`, and `preview`
  output modes
- **Opt-in custom tool overrides** by exact tool name, with `generic` and `mcp` kinds
- **Codemode display** that lists every recorded nested call with a per-tool summary line and a bounded
  saved result, then reuses codemode's own script, output, full-output-path, and timer rendering
- **Render-only registration** through one `pi.registerToolRenderer` resolver; no tool is
  re-registered and no prompt, schema, or execution behavior is copied
- **Adaptive edit/write diffs** with split or unified layouts, syntax highlighting, inline emphasis,
  and narrow-pane width clamping
- **Write diffs that survive reload and resume** through a bounded, display-only
  `details.toolDisplay` record
- **Workspace-scoped projected pending edit/write previews** while partial tool calls are streaming
- **Progressive collapsed diff hints**, hashline-anchor gutters, and width-safe framing
- **Four presets**: `opencode`, `balanced`, `verbose`, and `boxed`
- **Boxed frames that honor the host's `outputPad`**
- **Thinking labels** during streaming and final message rendering
- **Optional native user message box**
- **Capability-aware settings** for MCP and RTK controls
- The `/tool-display` command for live configuration

## Installation

This extension is bundled with Selesai and loaded automatically. There is no separate install step for
this fork, and the upstream npm/git installation commands do not apply to this copy.

The extension is copied to the packaged extensions directory by Selesai's build. Runtime configuration
lives in the agent directory, not in the repository.

## Usage

### Interactive settings

Open the settings modal:

```text
/tool-display
```

The modal exposes the day-to-day controls most people change regularly:

- preset profile
- read output mode
- grep/find/ls output mode
- MCP output mode (when MCP is available)
- codemode display
- preview line count
- bash collapsed line count
- diff layout mode
- native user message box toggle
- boxed tool calls

Advanced options remain in `config.json`.

### Direct commands

```text
/tool-display show                    # Show the effective config summary
/tool-display reset                   # Reset to the default boxed preset
/tool-display preset opencode         # Apply opencode preset
/tool-display preset balanced         # Apply balanced preset
/tool-display preset verbose          # Apply verbose preset
/tool-display preset boxed            # Apply boxed preset (ASCII border per tool call)
```

### Render-only model

The extension makes exactly one `pi.registerToolRenderer` call. Its resolver receives a tool name and the
next resolver in the chain, and returns pi-tool-display's renderers only when the tool is one it is
configured to render; otherwise it returns `next()`.

Consequences:

- The session keeps its own tool definitions, including prompt metadata, parameter schemas,
  `prepareArguments`, `execute`, and host options such as `read.autoResizeImages`,
  `read.imageCaptionModel`, and the project/agent shell settings.
- The resolver runs in the interactive TUI and in HTML export. In both places it replaces the
  presentation of a tool it is configured to render; it never replaces the tool or its execution.
- Tools the extension does not render, or every tool while the extension is disabled, keep the
  renderers supplied by `next()` — another extension's resolver or the tool definition itself.

There is no decoration or consumer API. Another extension that wants pi-tool-display presentation for
its own tool has two supported routes:

1. register its own `pi.registerToolRenderer` resolver, or
2. add the exact tool name to `customToolOverrides` and let this extension's resolver render it.

Neither route re-registers or mutates a tool definition.

### Ownership switch

`registerToolOverrides.<tool>` now means **render this tool**; it no longer means "register and execute
our copy of this tool". Set an entry to `false` to leave that tool's rendering to the host or to another
extension. Changes made through `/tool-display` apply to new tool rows without a reload; hand edits to
`config.json` are picked up on `/reload`.

### Write diffs across reload and resume

The renderer needs the file's previous content to draw a write diff. The extension captures it before
execution in the `tool_call` hook and stores a bounded, display-only record under
`details.toolDisplay` in the `tool_result` hook. The record survives reload and resume; the old
in-memory-only behavior did not.

The record keeps at most 64 KiB of previous content. A captured previous file larger than 64 KiB
stores an explicit `previousContentOmitted` marker with its byte size instead. The same omission marker
is used when the preview reader cannot know the previous content at all (for example a file above the
1 MB preview read limit, an unreadable file, or an old session with a true-only record). In those cases
the renderer shows the new content as a preview, not as a fabricated full-overwrite diff. Tool-result
details are not sent to the model.

## Presets

| Preset | Read Output | Search Output | MCP Output | Bash Output | Preview Lines | Bash Lines |
|--------|-------------|---------------|------------|--------------|---------------|------------|
| `opencode` | hidden | hidden | hidden | opencode | 8 | 10 |
| `balanced` | summary | count | summary | summary | 8 | 10 |
| `verbose` | preview | preview | preview | preview | 12 | 20 |
| `boxed` (default) | hidden | hidden | hidden | opencode | 8 | 10 |

- **`boxed`** (default): ASCII-bordered box around each tool call and its output
- **`balanced`**: compact summaries with line counts and match totals; bash shows line count only
- **`verbose`**: larger previews for read/search/MCP/bash output

Presets keep `powershell` enabled and keep `codemodeDisplay` on. The preset names and the shape of
existing config keys are unchanged.

### Bash and PowerShell output modes

| Mode | Behavior |
|------|----------|
| `opencode` | Classic collapsed output using `bashCollapsedLines` limit with expansion hint |
| `summary` | Shows only line count (e.g., "↳ 3 lines returned") — no output displayed |
| `preview` | Shows actual output lines using `previewLines` limit |

`powershell` renders through the same shell renderer as `bash` and follows `bashOutputMode` and
`bashCollapsedLines`. The shell tool itself, including `shellPath` and `commandPrefix`, is the host's.

### Codemode display

With `codemodeDisplay` on (the default), a codemode result follows the same `boxedToolCalls` setting as other tools and:

- uses codemode's own `renderCall` for the highlighted script;
- lists **every recorded nested call**, collapsed or expanded, with the same call formatter used by the
  direct renderer for that tool (or the raw saved arguments when the summary cannot be parsed);
- shows the saved arguments in full when expanded, with a truncation marker when storage cut them;
- renders each nested call's saved result with the shared built-in, custom, or native MCP renderer and
  the configured output mode; a hidden mode that draws nothing reveals a labelled bounded preview when
  expanded;
- shows nested errors and markers for truncated previews, omitted details, omitted script-budget
  results, and unsaved images;
- shows the script output, full-output path, timer, and model-call cost total from codemode's own
  renderer.

Nested calls that pi-tool-display does not render — an unconfigured third-party tool or a disabled
built-in — use a generic bounded preview. The resolver API does not expose another extension's renderer
for an arbitrary tool name, so this is a fallback, not parity with that tool's direct rendering. Nested
codemode calls do not recurse.

With `codemodeDisplay` off, the host codemode renderer is used instead (collapsed it shows the last
eight calls; expanded it shows all, still without saved results).

Saved codemode display data is bounded:

- arguments: 24 KiB per call and 256 KiB per script, with `argsBytes`/`argsTruncated` markers;
- result text: 40 lines and 8 KiB per call, whichever comes first;
- result `details`: kept only when their JSON is at most 8 KiB, otherwise an omitted marker with the
  byte size;
- results: 128 KiB of text plus details per script; later results are marked omitted when the budget
  is used;
- images: a count only, never image data, and `structuredContent` is never copied.

These are display-only records. Script values, the model-visible result, and the host codemode renderer
are unchanged.

## Configuration

Runtime configuration is stored at:

```text
~/.selesai/agent/extensions/pi-tool-display/config.json
```

The agent directory follows `SELESAI_CODING_AGENT_DIR` and, when set, `PI_CODING_AGENT_DIR`. A starter
template is included at `config/config.example.json`. This is a single user-level file; there is no
project layer.

### Configuration options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `debug` | boolean | `false` | Opt-in file logging for extension diagnostics; missing values are treated as `false` |
| `enabled` | boolean | `true` | Load the extension; `false` leaves every renderer to the host |
| `registerToolOverrides` | object | all `true` | Which built-in tools this extension renders |
| `customToolOverrides` | object | `{}` | Exact-name opt-in rendering rules for non-built-in tools |
| `enableNativeUserMessageBox` | boolean | `true` | Enable bordered user prompt rendering |
| `readOutputMode` | string | `"hidden"` | `hidden`, `summary`, or `preview` |
| `searchOutputMode` | string | `"hidden"` | `hidden`, `count`, or `preview` |
| `mcpOutputMode` | string | `"hidden"` | `hidden`, `summary`, or `preview` |
| `previewLines` | number | `8` | Lines shown in collapsed preview mode |
| `expandedPreviewMaxLines` | number | `4000` | Max preview lines when fully expanded |
| `bashOutputMode` | string | `"opencode"` | `opencode` (collapse), `summary` (line count), or `preview` (show lines) |
| `bashCollapsedLines` | number | `10` | Lines shown for collapsed bash/powershell output (opencode mode) |
| `diffViewMode` | string | `"auto"` | `auto`, `split`, or `unified` |
| `diffIndicatorMode` | string | `"bars"` | `bars` (vertical indicators), `classic` (+/- markers), or `none` |
| `diffSplitMinWidth` | number | `120` | Minimum width before auto mode prefers split diffs |
| `diffCollapsedLines` | number | `24` | Diff lines shown before collapsing |
| `diffWordWrap` | boolean | `true` | Wrap long diff lines when needed |
| `showTruncationHints` | boolean | `false` | Show truncation indicators for compacted output |
| `showRtkCompactionHints` | boolean | `false` | Show RTK compaction hints when RTK metadata exists |
| `boxedToolCalls` | boolean | `true` | Wrap each tool call and its result in an ASCII-bordered box |
| `codemodeDisplay` | boolean | `true` | Show codemode's nested calls and bounded saved results; `false` keeps the host codemode renderer |

Every key that existed before `0.6.0-selesai` is still accepted. The legacy
`registerReadToolOverride` key is still accepted as the fallback for `registerToolOverrides.read`.

### Tool ownership

Use `registerToolOverrides` to control which built-in tools this extension renders:

```json
{
  "registerToolOverrides": {
    "read": true,
    "grep": true,
    "find": true,
    "ls": true,
    "bash": true,
    "powershell": true,
    "edit": true,
    "write": true
  }
}
```

Set an entry to `false` to let the host or another extension render that tool. No tool is
re-registered, so this switch never affects which tool executes.

> Changes made through `/tool-display` take effect for new tool rows without a reload. A hand edit to
> `config.json` requires `/reload`.

### Custom tool overrides

Use `customToolOverrides` when another extension registers a tool and you want `pi-tool-display` to
render its call/result output. Matching is by exact top-level tool name only. Custom overrides are
explicit opt-in: an unlisted or disabled non-native tool keeps its owner's renderers. Native `mcp__*`
tools are the exception — without an enabled custom entry they use automatic MCP formatting.

```json
{
  "customToolOverrides": {
    "ide_find_symbol": {
      "enabled": true,
      "kind": "generic",
      "outputMode": "summary"
    },
    "custom_mcp_gateway": {
      "enabled": true,
      "kind": "mcp",
      "outputMode": "preview"
    }
  }
}
```

Each entry supports:

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | boolean | `true` | Whether `pi-tool-display` should render this custom tool |
| `kind` | string | `"generic"` | `generic` for compact call/result output, or `mcp` for MCP proxy-style arguments |
| `outputMode` | string | `"summary"` | `hidden`, `summary`, or `preview` for this custom tool's result output |

Boolean shorthand is also accepted:

```json
{
  "customToolOverrides": {
    "ide_find_symbol": true,
    "noisy_tool_to_leave_alone": false
  }
}
```

Notes:

- Built-in tool names (`read`, `grep`, `find`, `ls`, `bash`, `powershell`, `edit`, `write`) are
  ignored here; use `registerToolOverrides` for those.
- `generic` call rendering shows the tool name and argument count, then compacts the result according
  to `outputMode`.
- `mcp` call rendering understands MCP proxy-style arguments such as `tool`, `server`, `search`,
  `describe`, and `connect`.
- Native MCP tools (`mcp__*`) are rendered automatically and follow `mcpOutputMode`; they do not need
  a custom entry, and a disabled custom entry leaves them on automatic MCP formatting. `codemode` is
  not classified as MCP.

### Example config

```json
{
  "debug": false,
  "registerToolOverrides": {
    "read": true,
    "grep": true,
    "find": true,
    "ls": true,
    "bash": true,
    "powershell": true,
    "edit": true,
    "write": true
  },
  "customToolOverrides": {
    "ide_find_symbol": {
      "enabled": true,
      "kind": "generic",
      "outputMode": "summary"
    },
    "custom_mcp_gateway": {
      "enabled": true,
      "kind": "mcp",
      "outputMode": "preview"
    }
  },
  "enableNativeUserMessageBox": true,
  "readOutputMode": "summary",
  "searchOutputMode": "count",
  "mcpOutputMode": "summary",
  "previewLines": 12,
  "expandedPreviewMaxLines": 4000,
  "bashOutputMode": "opencode",
  "bashCollapsedLines": 15,
  "diffViewMode": "auto",
  "diffIndicatorMode": "bars",
  "diffSplitMinWidth": 120,
  "diffCollapsedLines": 24,
  "diffWordWrap": true,
  "showTruncationHints": false,
  "showRtkCompactionHints": false,
  "boxedToolCalls": true,
  "codemodeDisplay": true
}
```

### Debug logging

Debug logging is disabled by default. Set `debug` to `true` in the extension root `config.json` only
when collecting diagnostics; missing or non-`true` values are treated as `false`. When enabled,
diagnostics are appended to `debug/debug.log` under a runtime-created `debug/` directory, and no debug
output is written to the terminal.

## Rendering notes

### Edit and write diffs

`edit` and `write` results use the same diff renderer. In `auto` mode the extension chooses split or
unified layout based on available width. On narrow panes it clamps rendered lines and shortens
collapsed hint text so the diff stays readable instead of spilling past the terminal width.

While tool arguments are still streaming, partial `edit` and `write` calls can show projected pending
previews. Deterministic edits render as `pending edit` diffs against current file contents, writes
render as `pending overwrite` or `pending create`, and unresolved projections show a clear preview
notice instead of guessing. Preview file reads are scoped to the active workspace so pending previews
avoid reading paths outside the current project.

When diff input includes Pi anchored read lines such as `12#AB:content`, the renderer treats the anchor
as line metadata and displays the `LINE#HASH` label in the gutter while keeping the content aligned for
split, unified, and compact diff layouts.

### Boxed frames

`boxedToolCalls` wraps each rendered call and result in the startup-style ASCII frame. The frame applies
the host's `outputPad` inside its borders and keeps the caller's padding when the host renders an
unboxed row. Widths too narrow for the title use a plain top border, and widths too narrow for the frame
fall back to plain content instead of overflowing.

### Thinking labels

Thinking blocks are labeled during streaming and on final messages. Before the next model turn, the
extension sanitizes those presentation labels out of the stored assistant context so they do not
accumulate or pollute future prompts.

### Native user message box

When enabled, user prompts render inside a bordered box using Pi's native user message component. The
renderer preserves markdown content more safely and normalizes ANSI/background handling to avoid odd
nested background artifacts.

## Capability detection

The extension checks the current Pi environment and adjusts behavior automatically:

- **MCP tooling unavailable at startup**: MCP settings can be hidden from the modal, but the configured
  MCP output mode is preserved because native `mcp__*` tools may register later.
- **RTK optimizer unavailable**: RTK hint settings are hidden and RTK compaction hints are disabled.

This keeps the UI aligned with the current environment while still allowing dynamically registered
native MCP tools to be styled when they appear.

## Troubleshooting

### Reload safety

`/reload` is supported. On reload the extension disposes its registered cleanup callbacks (including
the bash spinner timers and the native user-message/thinking-label patches) and the old resolver is
replaced by the newly loaded extension instance. No manual cleanup is needed.

### A tool is not using this extension's rendering

1. For a built-in, check `registerToolOverrides.<tool>` and run `/tool-display show`.
2. For another extension's tool, add its exact name under `customToolOverrides`; name/schema heuristics
   are not used.
3. For a native MCP tool, confirm the name starts with `mcp__`; it follows `mcpOutputMode` without a
   custom entry.
4. Run `/reload` after hand-editing `config.json`, and after changing `enabled` from `false`.

### Built-in options or shell settings look wrong

They are not controlled here. pi-tool-display does not construct or execute built-in tools; Selesai's
session passes `read.autoResizeImages`, `read.imageCaptionModel`, `shellPath`, and `commandPrefix` to
the real host tools.

### Config not loading

If your settings are not being applied:

1. Check that the tool-display config exists at
   `~/.selesai/agent/extensions/pi-tool-display/config.json` (the agent directory respects
   `SELESAI_CODING_AGENT_DIR` and `PI_CODING_AGENT_DIR`).
2. Make sure the JSON is valid.
3. Run `/tool-display show` to inspect the effective config summary.

### MCP or RTK settings missing

Those controls only appear when the corresponding capability is available in the current Pi
environment.

## Project structure

```text
pi-tool-display/
├── index.ts                         # Extension entrypoint for Pi auto-discovery
├── src/
│   ├── index.ts                     # Bootstrap and extension registration
│   ├── agent-dir.ts                 # Selesai/Pi agent-directory resolution
│   ├── bash-display.ts              # Bash/powershell call rendering and spinner
│   ├── capabilities.ts              # MCP/RTK capability detection
│   ├── codemode-display.ts          # Boxed codemode row and nested-call/result display
│   ├── config-modal.ts              # /tool-display settings UI and command handling
│   ├── config-store.ts              # Config load/save and normalization
│   ├── diff-renderer.ts             # Edit/write diff rendering engine
│   ├── disposable.ts                # Reload-safe cleanup registry for timers and patches
│   ├── extension-lifecycle.ts       # Shared reload-shutdown hook
│   ├── pending-diff-preview.ts      # Partial edit/write preview projection helpers
│   ├── presets.ts                   # Preset definitions and matching
│   ├── render-utils.ts              # Shared rendering helpers
│   ├── thinking-label.ts            # Thinking label formatting and context sanitization
│   ├── tool-call-box.ts             # outputPad-aware ASCII frame
│   ├── tool-metadata.ts             # Tool identity and MCP-name detection
│   ├── tool-overrides.ts            # The render-only resolver and write-diff record
│   ├── types.ts                     # Shared config and type definitions
│   ├── user-message-box-*.ts        # Native user message box pipeline
│   ├── write-display-utils.ts       # Write summary helpers
│   └── zellij-modal.ts              # Modal UI primitives
├── config/
│   └── config.example.json          # Starter config template
└── tests/                           # node:test suite run against the repo host
```

## Development

Run inside `src/extensions/pi-tool-display`:

```bash
# Type check
npx tsc -p tsconfig.json --noEmit

# Run the package suite
npm test

# Lint (type check) plus tests
npm run check
```

The package imports the repository host through `@selesai/code` and resolves `@earendil-works/pi-tui`
from the repository root. Run a root `npm run build` after host changes so `@selesai/code` points at a
fresh `dist/index.js`.

## Origin and license

- Upstream: [`MasuRii/pi-tool-display`](https://github.com/MasuRii/pi-tool-display), origin tag
  `vendor/pi-tool-display/v0.5.0` (commit `91cef7580078371f8dc49a8607222807ad6a424d`).
- Selesai fork version: `0.6.0-selesai`.
- Original author and copyright: MasuRii. The MIT license text in [`LICENSE`](LICENSE) is unchanged.
- Fork-maintenance record: [`selesai-in-doc/pi-tool-display.md`](../../../selesai-in-doc/pi-tool-display.md).
