@anthodev/pi-tools-style 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,30 +2,27 @@
2
2
 
3
3
  <p align="center"><strong>Give Pi tool activity clear, theme-aware structure without changing how tools run.</strong></p>
4
4
 
5
- <p align="center">
6
- <a href="https://github.com/Anthodev/pi-tools-style/actions/workflows/ci.yml?query=branch%3Adevelop"><img alt="Tests" src="https://img.shields.io/github/actions/workflow/status/Anthodev/pi-tools-style/ci.yml?branch=develop&amp;style=for-the-badge&amp;label=tests&amp;labelColor=101418"></a>
7
- <a href="https://github.com/Anthodev/pi-tools-style/releases/latest"><img alt="Latest stable release" src="https://img.shields.io/github/v/release/Anthodev/pi-tools-style?style=for-the-badge&amp;label=release&amp;labelColor=101418&amp;color=9ccbfb"></a>
8
- <a href="https://github.com/Anthodev/pi-tools-style/blob/develop/LICENSE"><img alt="MIT License" src="https://img.shields.io/github/license/Anthodev/pi-tools-style?style=for-the-badge&amp;labelColor=101418&amp;color=b9c8da"></a>
9
- </p>
5
+ > [!NOTE]
6
+ > The published npm `0.1.4` release still targets Pi `1.0.x`. Version `0.2.0`, which moves to Pi `1.1.x` and the middleware support, semantic cards, and MCP, web-search, and execution views described on this page.
10
7
 
11
- `pi-tools-style` adds rounded frames, tool-specific icons, and live running indicators to Pi tool calls and direct shell commands. It decorates the output Pi already produces instead of replacing tool definitions or renderers.
8
+ `pi-tools-style` gives Pi tool calls and direct shell commands clear semantic structure: status, duration, and identity in a card head, command and content in framed sections, bounded previews that expand in place with `Ctrl+O`, and colors that follow the active theme. It draws through Pi's public tool-renderer middleware and never registers or replaces a tool.
12
9
 
13
- Built-in tools, MCP integrations, AFT, and third-party extensions keep control of their schemas, permissions, prompts, execution backends, and rendered content. When an output cannot be framed safely, the extension leaves it untouched.
10
+ Built-in tools, MCP servers, and third-party extensions keep control of their schemas, permissions, prompts, execution backends, and content. Cards show only the data a result already contains; outputs that cannot be framed safely pass through untouched.
14
11
 
15
12
  ## Highlights
16
13
 
17
- - **Presentation without ownership** — wraps existing TUI output and never registers or replaces a tool.
18
- - **Theme-aware borders** — assigns semantic Pi theme colors by tool category and follows theme changes live.
14
+ - **Presentation without ownership** — draws semantic cards around tool activity and never registers or replaces a tool.
15
+ - **Public render middleware** — builds on Pi `1.1`'s `registerToolRenderer` for tool cards instead of private render wrappers.
16
+ - **MCP-aware** — proven `server/tool` identity, declared annotation hints, dedicated resource views, and full-output paths shown but never read.
17
+ - **Web-search views** — exact tool recognition, answers and sources rendered from the result, configurable extra tool names, no name heuristics.
19
18
  - **Three icon modes** — uses portable ASCII labels by default, richer Nerd Font glyphs when enabled, or icon-free titles.
20
- - **Live running state** — animates the title while a model tool call is still executing, then removes the spinner on completion.
21
19
  - **Terminal-correct sizing** — measures visible ANSI width before padding, truncating, or drawing borders.
22
- - **Renderer-friendly composition** — preserves Pi, AFT, MCP, and extension-provided output inside the frame.
23
20
  - **Image-safe fallback** — skips Kitty, iTerm2, and Sixel rows instead of corrupting terminal image sequences.
24
- - **Reload-safe state** — keeps wrappers and runtime configuration idempotent across `/reload`.
21
+ - **Reload-safe state** — keeps registration and runtime configuration idempotent across `/reload`.
25
22
 
26
23
  ## Installation
27
24
 
28
- The current release targets Pi `0.84.x` and requires Node.js 22 or newer.2
25
+ This checkout targets Pi `>=1.1.0 <2`, verified with `1.1.0`, and requires Node.js `>=22.19.0`.
29
26
 
30
27
  To install it, use the following command:
31
28
 
@@ -45,31 +42,44 @@ Pi stores the package in user settings and references the directory directly, so
45
42
 
46
43
  ## What it looks like
47
44
 
48
- ASCII mode stays readable in any terminal:
45
+ Cards stay collapsed to a bounded preview and expand in place with `Ctrl+O`. The following frames are captured verbatim from provider-free offline replays at a 120-column terminal; they prove rendering, not filesystem execution. A `bash` call renders as a framed card with the tool's identity, state, and duration in the head, the supplied command directly below the head above an `Output` section, and a completion mark (shown expanded with `Ctrl+O`):
49
46
 
50
47
  ```text
51
- ╭─ [F] | read / ───────────────────────╮
52
- │ Reading README.md │
53
- ╰──────────────────────────────────────╯
48
+ ╭─── [$] | Bash · ok ──────────────────────────────────────────────────────────────────────────────────────────────────╮
49
+ │ $ echo "First command line" │
50
+ │ echo "Second command line" │
51
+ │ timeout: 10s │
52
+ ├─── Output ───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
53
+ │ First command line │
54
+ │ Second command line │
55
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
54
56
  ```
55
57
 
56
- Nerd Font mode uses tool-specific glyphs and Braille spinner frames:
58
+ The head never repeats the command; `Ctrl+O` reveals any remaining command lines above the same `Output`, with no `Command` divider row.
57
59
 
58
- ```text
59
- ╭─  | read ⠹ ─────────────────────────╮
60
- │ Reading README.md │
61
- ╰──────────────────────────────────────╯
62
- ```
63
-
64
- Once execution finishes, the spinner disappears and the title remains stable:
60
+ A `read` call renders as a framed card too: the supplied path sits directly under the head, above the previewed `Output`:
65
61
 
66
62
  ```text
67
- ╭─ | read ───────────────────────────╮
68
- │ README content │
69
- ╰──────────────────────────────────────╯
63
+ ╭─── [F] | Read · ok ──────────────────────────────────────────────────────────────────────────────────────────────────╮
64
+ │ path: /offline/InputLiteralPath.txt │
65
+ ├─── Output ───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
66
+ │ O01 │
67
+ │ O02 │
68
+ │ O03 │
69
+ │ O04 │
70
+ │ O05 │
71
+ │ O06 │
72
+ │ O07 │
73
+ │ O08 │
74
+ │ O09 │
75
+ │ O10 │
76
+ │ … 2 more lines ctrl+o to expand │
77
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
70
78
  ```
71
79
 
72
- Direct `!` and `!!` commands use the same frame with a `shell` title. Slash-command output remains unchanged because Pi does not expose one uniform transcript component for slash commands.
80
+ `find`, `grep`, and `ls` follow the same shape: their supplied path, pattern, and options render the same way, above their `Output`.
81
+
82
+ Direct `!` and `!!` commands keep their frame and `shell` title through a guarded internal adapter described under [How it works](#how-it-works), and place the command body directly below the head above `Output` like tool cards. An interrupted command renders an explicit cancelled state rather than an error. Slash-command output remains unchanged because Pi does not expose one uniform transcript component for slash commands.
73
83
 
74
84
  ## Usage
75
85
 
@@ -79,23 +89,35 @@ Boxes start enabled. Manage them from Pi with:
79
89
  /tools-style
80
90
  /tools-style on
81
91
  /tools-style off
92
+ /tools-style config
82
93
  /tools-style icons ascii
83
94
  /tools-style icons nerd-font
84
95
  /tools-style icons off
85
96
  /tstyle
86
97
  ```
87
98
 
88
- `/tools-style` toggles framing. `/tstyle` is a short alias with the same arguments. The explicit `on` and `off` forms set the desired state without depending on its current value.
99
+ `/tools-style` toggles framing. `/tstyle` is a short alias with the same arguments. The explicit `on` and `off` forms set the desired state without depending on its current value. Turning boxes off restores Pi's native rendering for downstream renderers, backgrounds, and padding.
100
+
101
+ `/tools-style config` opens an interactive menu (interactive TUI sessions only) listing the global switch, the icon mode, the shell card switch, and one row per known tool — the eight native tools, MCP calls, third-party tools, and the owned execution tools. Fuzzy search filters the rows; `Enter` or `Space` flips the selected value; `Escape` closes. Per-tool rows store explicit preferences for that exact tool name (absent means follow the global switch); already-rendered rows restyle immediately and every choice persists across restarts and `/reload`. The shell card has its own switch, so `!/!!` styling can be turned off without touching tool calls.
89
102
 
90
- Icon mode persists in `~/.pi/agent/config/tools-style.json`. ASCII remains the package default; Nerd Font mode can be enabled once for terminals that provide the required glyphs. `icons off` keeps frames and the ASCII running spinner, but removes the static icon and its `|` separator from titles.
103
+ ## Settings
104
+
105
+ Settings persist in `~/.pi/agent/config/tools-style.json`. The icon mode, the web-search recognition list, and the per-tool preferences share the file; saving an icon mode preserves the configured list and tool preferences. ASCII remains the package default; Nerd Font mode can be enabled once for terminals that provide the required glyphs. `icons off` keeps frames but drops the icon and its `|` separator from titles, showing the textual status (`pending`, `running`, `done`, `error`, `cancelled`) instead of an animated spinner.
91
106
 
92
107
  ```json
93
108
  {
94
- "iconMode": "nerd-font"
109
+ "enabled": true,
110
+ "iconMode": "nerd-font",
111
+ "shellEnabled": true,
112
+ "webSearchTools": ["mcp__my_web__search"],
113
+ "tools": {
114
+ "read": false,
115
+ "mcp__my_web__search": true
116
+ }
95
117
  }
96
118
  ```
97
119
 
98
- Set `PI_TOOLS_STYLE=0` before startup to load the extension with framing disabled.
120
+ `webSearchTools` accepts exact tool names only; see [Web-search cards](#web-search-cards). `tools` maps exact tool names to explicit `true`/`false` preferences; a name absent from the map follows the global switch. `shellEnabled` styles the direct shell card (`!`/`!!`) independently of tool calls. With `enabled: false` the global switch wins and every card returns to Pi's native rendering while the stored preferences are kept. Set `PI_TOOLS_STYLE=0` before startup to load the extension with framing disabled.
99
121
 
100
122
  ## Icons and running indicators
101
123
 
@@ -117,38 +139,91 @@ Common tools receive dedicated icons, while unknown tools fall back to their sem
117
139
  | Show diagnostics | `[D]` | `` | `lsp_diagnostics`, `lens_diagnostics` |
118
140
  | Unknown tool | `[T]` | `` | third-party fallback |
119
141
 
120
- While a model tool call is active, ASCII mode cycles through `| / - \` and Nerd Font mode cycles through `⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏`. One shared timer drives all active titles and stops after completion, interruption, box disablement, stale transcript removal, or session shutdown.
142
+ While a model tool call is active, the status mark animates: ASCII mode cycles through `| / - \` and Nerd Font mode cycles through `⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏`. Completion replaces the spinner with `ok` or `✓` (`!` or `✗` on error) and a duration such as `1.3s`. One shared timer drives all active titles and stops after completion, interruption, box disablement, stale transcript removal, or session shutdown.
121
143
 
122
- ## Theme-aware palette
144
+ ## Cards and colors
123
145
 
124
- Borders resolve their color from the active Pi theme on every render. Switching themes therefore updates existing tool categories without reloading the extension.
146
+ Card borders and backgrounds follow the active Pi theme, so switching themes updates existing cards without reloading. The color follows the card state, not a tool category:
125
147
 
126
- | Category | Typical tools | Pi theme token |
148
+ | State | Border | Theme tokens |
127
149
  | --- | --- | --- |
128
- | Inspect | reads, searches, reports, diagnostics | `mdLink` |
129
- | Mutate | writes, edits, create/update/delete operations | `warning` |
130
- | Execute | bash, shell, tests, terminal commands | `bashMode` |
131
- | External | web, fetch, MCP, browser, GitHub, Slack | `syntaxType` |
132
- | Orchestrate | workflow, subagent, Ralph, todo | `customMessageLabel` |
133
- | Interact | ask, question, confirm, select | `accent` |
134
- | Other | unknown third-party tools | `borderMuted` |
150
+ | Pending or running | accent | `accent`, `toolPendingBg` |
151
+ | Done | muted | `borderMuted`, `toolSuccessBg` |
152
+ | Error | error | `error`, `toolErrorBg` |
153
+ | Cancelled | warning | `warning` |
154
+
155
+ Direct `!` and `!!` shell cards use the same state-based border and background selection as every other card; there is no separate category color.
156
+
157
+ ## Tool presentations
158
+
159
+ The plugin renders these tool-specific cards:
160
+
161
+ Supplied call arguments render directly under the head without an `Input` or `Command` divider. Other section dividers, including `Output`, `Diff`, and `Calls`, are unchanged.
162
+
163
+ | Tool | Card |
164
+ | --- | --- |
165
+ | `bash`, `powershell` | Framed: identity, state, and duration in the head; the command (first line, supplied options such as `timeout`) directly below the head above `Output` (last ten lines), truncation note, `Full output: <path>` when Pi wrote one |
166
+ | `edit` | Framed: path and the request, kept when the result arrives, above the final `Diff` or error |
167
+ | `write` | Framed: path and content preview above the actual result `Output`; an empty result shows no fabricated confirmation |
168
+ | `read` | Framed: path and any supplied offset or limit above the `Output` preview |
169
+ | `find`, `grep`, `ls` | Framed: supplied pattern, path, and options above the `Output` entries with an expand hint |
170
+ | `ctx_execute`, `ctx_execute_file` | Framed: path, language, code, and options above the `Output` text |
171
+ | `codemode` | Framed: JavaScript, model `Calls`, optional cost total, `Output`, `Full output: <path>` |
172
+ | MCP calls | Framed: proven identity and declared hints in the head, supplied arguments above the `Output` |
173
+ | MCP resources | Framed: resource title in the head, supplied server and URI arguments above the `Output` |
174
+ | Web search | Framed in every state: query and supplied options above the answer, sources, or raw response |
175
+
176
+ Every card shows its target once — in the head or directly below it — and collapses previews behind a `… N more lines` or `… N more entries` hint. Output paths are displayed but never read.
177
+
178
+ ### MCP cards
179
+
180
+ - A tool is treated as proven MCP only when Pi's public tool info reports `builtin:mcp`. An `mcp__*` name or reserved resource name that Pi has not proven is treated as an unresolved MCP call and still gets the MCP card, unless a downstream renderer already draws its own complete shell, in which case it passes through untouched; provenance is re-checked against Pi's tool list on render.
181
+ - The head keeps only the callable identity: `server/tool` when the result details prove both, or `namespace/tool` when a proven namespace prefixes the exposed name; otherwise it falls back to the exposed tool name. A meta line shows the namespace when it is not already in the head, the exposure form when it is not direct, and annotation hints exactly as the server declares them (read-only, destructive, idempotent, open-world). Declared hints are shown, not verified. Supplied arguments are never merged into the head; the complete arguments render directly under the head.
182
+ - Resource tools get their own cards. The head shows the resource title only; supplied arguments render as delivered (schemaless safe JSON) directly under the head, where `…` marks a value that has not arrived rather than a validated required field, and empty arguments are shown literally.
183
+
184
+ ### Web-search cards
185
+
186
+ - Recognition is exact: Pi's native `web_search`, names listed in `webSearchTools`, and MCP tools proven as `web_search` by their namespace or result details. Pi's own tool names are refused even if configured by mistake, and no substring heuristics apply.
187
+ - The card renders only what the result already contains: the answer, sources (eight collapsed with a `… N more sources` hint), provider and usage metadata, warnings, and a `Full output: <path>` reference. It never executes, fetches, or reads anything.
188
+ - Unstructured results keep a six-line text preview instead of a source tree.
189
+ - MCP and web-search rendering is verified offline against recorded tool info and results. The extension does not contact servers or providers and makes no transport or quality promise.
190
+
191
+ ### Execution cards
192
+
193
+ - Ownership follows the exact requested tool name and is case-sensitive: `codemode`, or any name starting with `ctx_execute` (`ctx_execute`, `ctx_execute_file`, and a future `ctx_execute*` suffix). `ctx_batch_execute`, `mcp__server__ctx_execute`, and other casings are not owned. The match needs no provenance, so these names get execution cards even when Pi reports none, and the web exception refuses them even if configured in `webSearchTools`.
194
+ - `ctx_execute` and `ctx_execute_file` show their metadata (path for the file form, language for both), the code, and the remaining options — `timeout` in milliseconds, `background`, `cwd`, `intent` — directly under the head, above `Output`. The path is displayed, never read. The server's own input echo is dropped only on an exact match, so a result that does not match the arguments stays intact; codemode's `Script completed`/`Script failed` wrapper is likewise dropped only when it matches exactly.
195
+ - Collapsed previews are independent: the code keeps the first ten visual lines, `Output` the first five. `Ctrl+O` expands the full content.
196
+ - `codemode` shows its JavaScript directly under the head, each model call (name, arguments, status, duration, cost) in a `Calls` section that keeps the last eight entries behind a `… N more entries` hint and prints full errors when expanded, a `Model calls: $…` total when more than one call has a positive cost, the `Output`, and a `Full output: <path>` reference. A partial result shows only the calls already reported and no output.
197
+ - Turning cards off, a presentation failure, or a width below the card minimum restores the captured native renderer for these tools.
135
198
 
136
199
  ## How it works
137
200
 
138
- Pi `0.84.x` does not expose public render middleware around `ToolExecutionComponent` and `BashExecutionComponent`. `pi-tools-style` therefore installs a narrow wrapper around their existing `render()` methods.
201
+ Pi `1.1` exposes a public render middleware: extensions call `pi.registerToolRenderer()`, and the registered resolvers run in extension load order. `pi-tools-style` registers one resolver that receives `next()` — the downstream remainder of the resolver chain — and draws its own card around the renderer that `next()` returns, capturing that renderer's call and result fragments.
202
+
203
+ Ordering decides the outcome. An earlier registered authoritative non-web renderer wins outright and this plugin is never consulted; a web provider registered after it is intercepted by the web exception, while a web renderer registered before it must delegate web calls through `next()` for that exception to apply. A resolver sees only the renderer handed to it through `next()`, cannot tell which extension produced it, and cannot reorder the chain. Generic third-party renderers are preserved — drawn inside the card, or passed through untouched when they already draw their own complete shell — whereas builtin, MCP, recognized web, and owned execution renderers are replaced by these cards while styling is on and restored to Pi's native rendering when it is turned off. Execution ownership is matched by the exact requested name and is checked first, so those names never take the generic self-shell passthrough and are never promoted to web cards.
204
+
205
+ Direct `!` and `!!` shell commands are not covered by that middleware. They keep a narrow internal decorator on the shell component, guarded so that any incompatible Pi internal change disables only shell framing and reports `Shell styling unavailable: incompatible Pi renderer internals.` The public tool cards keep working.
206
+
207
+ Turning cards off restores Pi's native rendering. Decoration errors fail open to the native layout, terminal image sequences (Kitty, iTerm2, Sixel) pass through unframed, and the extension never calls `pi.registerTool()` or intercepts execution. Reloading re-registers the renderer and refreshes configuration without stacking wrappers or leaving duplicate timers behind.
139
208
 
140
- The wrapper asks the previous renderer to draw at the frame's inner width, removes only paired outer horizontal rules, and then adds ANSI-aware chrome. It does not call `pi.registerTool()`, replace tool definitions, or intercept execution.
209
+ ### Rendering and cache boundaries
141
210
 
142
- Configuration and animation state live behind `Symbol.for()` keys. Reloading updates the active configuration without stacking wrappers or leaving duplicate timers behind. Any incompatibility or decoration error falls back to Pi's original full-width renderer.
211
+ - **Header-only animation** — Owned tool cards refresh their prepared running header without rebuilding or rendering the body again. Direct-shell cards reuse Pi's existing loader cadence. For generic renderers, only a known static `Text`/`Container` tree whose descendants are all static and passive qualifies for the header-only transaction; an opaque or custom-render component keeps the ordinary SDK invalidation path so implicit dynamic rendering stays live. This does not make every generic card cost-free.
212
+ - **Generic cards** — A third-party renderer that does not draw its own complete shell is drawn inside the card: its real call component renders directly under the head (first ten visual lines) and its real result component becomes `Output` (first five), falling back to the safe supplied arguments or extracted text only when the renderer returns no usable component. Interactive components are never sliced, while a noninteractive sibling still keeps its own preview and hint.
213
+ - **Bounded preparation** — Each row retains one current primary body and one current-width layout. Unchanged text, Markdown, and syntax highlighting are reused; ordinary callbacks still re-read arguments and results, including objects mutated in place. Real component or environment invalidation clears derived preparation, while width-only changes relayout the retained view.
214
+ - **Exact previews** — Previews remain bounded after wrapping. Unusual Unicode clusters keep defensive width checks, and fresh large payloads still require full-context highlighting and wrapping. This reduces renderer CPU work, not all terminal painting or input latency.
143
215
 
144
216
  ## Compatibility and limits
145
217
 
146
- - Pi `0.84.x` is the supported target. Private TUI internals may change in later releases.
147
- - The render wrapper is necessarily unsupported until Pi exposes public, composable render middleware.
218
+ - Pi `>=1.1.0 <2` is the supported peer range, verified with `1.1.0` in both `fullscreen` and `regular` TUI modes. Future `1.x` releases are expected to work through the public middleware, but each release is verified, not assumed.
219
+ - The direct-shell decorator relies on Pi component internals. Its compatibility is bounded by verification: when internals change, the guard disables shell framing only, and the peer range alone does not prove it.
220
+ - Pi's HTML exporter processes SGR styling only. OSC 8 hyperlinks are exported as raw control text around the link label, so exported HTML shows escape sequences that the terminal itself would render as links. The extension adds no artificial export mode or private workaround, and no claim is made about link interaction or image rendering in exports.
221
+ - Pi serializes call and result rows independently. An exported card can therefore show a repeated closing border, and a call row exported while still running keeps the state it had at that moment. No retroactive fixups are attempted.
222
+ - Windows runtime behavior is untested; the Windows-specific path test is skipped on other platforms.
148
223
  - Kitty, iTerm2, and Sixel image output remains unboxed by design.
149
224
  - Slash commands remain outside the current scope because their transcript output has no uniform component.
150
- - Another extension can replace this wrapper if it overwrites component rendering without composing with the previous renderer.
151
- - Unsupported renderer shapes fail open rather than affecting tool execution.
225
+ - A card that cannot be framed safely — turned off globally or per name, narrower than the card minimum, a presentation or construction failure, or an unsupported renderer shape — falls back to the captured native rendering rather than affecting tool execution. A downstream renderer that already draws its own complete shell, and terminal-image output, pass through untouched.
226
+ - Frame characters remain selectable text and are included in copied output. Excluding decorative cells would require Pi/TUI integration for fullscreen selection; no portable exclusion path has been identified for native terminal selection in regular mode.
152
227
 
153
228
  ## Development
154
229
 
@@ -160,10 +235,38 @@ npm run check
160
235
  npm pack --dry-run --json
161
236
  ```
162
237
 
163
- `npm run check` runs TypeScript validation and the Vitest suite. Tests cover frame width, ANSI output, terminal images, native and AFT rule removal, category colors, icon modes, persisted settings, spinner lifecycle, real `ToolExecutionComponent` integration, render composition, reload idempotence, fail-open behavior, and the no-`registerTool()` contract.
238
+ Pi selects which extension files a package contributes through that package's `pi.extensions` field (`index.ts` here); activating the working tree means adding the checkout path to Pi's `packages` setting in the user settings and restarting Pi. An already-loaded package picks up changes after `/reload`.
239
+
240
+ `npm run check` runs TypeScript validation and the Vitest suite. Tests cover the builtin, MCP, web-search, and execution semantic views against recorded results, shell command placement and independent previews, frame layout and ANSI width parity, native restoration when cards are disabled, the public middleware resolver and its self-shell exception, the guarded shell decorator, category colors, icon modes, persisted settings, spinner lifecycle, real Pi `1.1` component integration, reload idempotence, fail-open behavior, and the no-`registerTool()` contract.
241
+
242
+ Release-note tests cover exact version sections, Markdown fences and whitespace, duplicate or missing sections, tagged Git snapshots, annotated and lightweight tags, and CLI failure paths.
164
243
 
165
244
  `--legacy-peer-deps` avoids an npm 10.9.8 Arborist failure observed while resolving the development peer dependency graph.
166
245
 
246
+ ## Releases
247
+
248
+ Releases are triggered only by stable `vX.Y.Z` tag pushes. `.github/workflows/publish.yml` validates the tagged commit on Node.js `22.19.0` and `24.x`, packages one npm tarball, publishes it to npm, then creates the GitHub release with that tarball and `SHA256SUMS` as assets.
249
+
250
+ `CHANGELOG.md` is the source of release notes. CI extracts the exact version section from the tagged commit, not the working tree, and preserves its summary, lists, links, and Markdown. Invalid tags, mismatched revisions, or missing, empty, or duplicate sections fail before publication. CI neither generates notes from commit subjects nor edits the changelog.
251
+
252
+ To prepare a release:
253
+
254
+ 1. Update `package.json` and regenerate `package-lock.json`. Move the release entries from `Unreleased` into a dated `## [X.Y.Z] - YYYY-MM-DD` section, preserving verified commit or PR references and the full changelog comparison link.
255
+ 2. Run `PI_CODING_AGENT_DIR="$(mktemp -d)" npm run check` and `npm run pack:check`.
256
+ 3. Seal and push the release-preparation commit on `develop`. With jj, use the sealed commit's explicit SHA, never an empty working-copy `@`.
257
+ 4. Set `VERSION` to the prepared package version and `RELEASE_SHA` to that commit's full SHA, then validate the notes before pushing the tag:
258
+
259
+ ```bash
260
+ jj tag set "v${VERSION}" -r "$RELEASE_SHA"
261
+ notes="$(mktemp)"
262
+ node scripts/generate-release-notes.ts "v${VERSION}" "$RELEASE_SHA" "$notes"
263
+ jj git push --tag "v${VERSION}"
264
+ ```
265
+
266
+ The workflow requires the tag to match the package and lockfile versions and to belong to `develop`. npm publication uses OIDC Trusted Publishing on a GitHub-hosted runner with the protected `npm` environment. Keep the `publish.yml` filename and environment aligned with the trusted publisher configuration. Concurrent release tags are serialized.
267
+
268
+ Publication is not an idempotent update. An already-published npm version or existing GitHub release makes a rerun fail. npm publication precedes GitHub release creation, so a failure in the latter can leave the package already published. Do not move a published tag or delete a release to make a rerun succeed.
269
+
167
270
  ## License
168
271
 
169
272
  MIT
package/index.ts CHANGED
@@ -1,126 +1,169 @@
1
1
  import {
2
2
  BashExecutionComponent,
3
- ToolExecutionComponent,
4
3
  type ExtensionAPI,
5
4
  } from "@earendil-works/pi-coding-agent";
6
5
 
7
6
  import {
8
- installRenderDecorator,
9
- isRenderDecoratorEnabled,
10
- setRenderDecoratorEnabled,
11
- type InstallStatus,
7
+ installShellRenderer,
8
+ type ShellInstallStatus,
12
9
  } from "./src/render-decorator.js";
13
- import { formatToolTitle } from "./src/tool-icon.ts";
14
10
  import {
15
- clearToolSpinners,
16
- toolSpinnerFrame,
17
- } from "./src/tool-spinner.ts";
11
+ installToolRenderer,
12
+ invalidateToolPresentations,
13
+ initializeToolRendererEnabled,
14
+ reportShellIncompatibility,
15
+ isToolRendererEnabled,
16
+ setShellCompatibilityNotifier,
17
+ setToolRendererEnabled,
18
+ setToolRendererImplementation,
19
+ type ToolRendererSession,
20
+ } from "./src/tool-renderer.js";
21
+ import { clearToolSpinners } from "./src/tool-spinner.ts";
22
+ import { createToolView, invalidateToolViewCache } from "./src/tool-presentation.js";
23
+ import { layoutToolView } from "./src/frame.js";
24
+ import { createWebSearchView, invalidateWebSearchViewCache, isWebSearchTool } from "./src/web-search-presentation.js";
18
25
  import {
19
- getIconMode,
26
+ flushSettingsWrites,
27
+ getSettings,
28
+ getWebSearchTools,
20
29
  isIconMode,
30
+ isShellEnabledByConfig,
21
31
  loadSettings,
22
- saveIconMode,
32
+ saveSettings,
23
33
  } from "./src/settings.ts";
24
34
  import {
25
- createCategoryBorderStyle,
26
- createToolBorderStyle,
27
35
  setThemeProvider,
28
- type BorderStyle,
29
36
  } from "./src/tool-category.js";
30
37
 
31
- const TOOL_RENDER_KEY = Symbol.for("pi-tools-style:tool-render");
32
- const SHELL_RENDER_KEY = Symbol.for("pi-tools-style:shell-render");
38
+ import { openToolsStyleConfig, type ToolsStyleConfigChange } from "./src/config-menu.js";
33
39
 
34
40
  export interface InstallationResult {
35
- shell: InstallStatus;
36
- tools: InstallStatus;
41
+ shell: ShellInstallStatus;
42
+ session: ToolRendererSession;
37
43
  }
38
44
 
39
- export function installToolsStyle(): InstallationResult {
40
- const enabled = process.env.PI_TOOLS_STYLE !== "0";
41
-
42
- const tools = installRenderDecorator(ToolExecutionComponent, {
43
- enabled,
44
- key: TOOL_RENDER_KEY,
45
- styleBorderFor: toolBorderStyle,
46
- title: toolTitle,
47
- trimOuterRules: true,
48
- });
49
- const shell = installRenderDecorator(BashExecutionComponent, {
50
- enabled,
51
- key: SHELL_RENDER_KEY,
52
- styleBorderFor: () => createCategoryBorderStyle("execute"),
53
- title: () => formatToolTitle("shell", getIconMode()),
54
- trimOuterRules: true,
45
+ export function installToolsStyle(pi: ExtensionAPI): InstallationResult {
46
+ setToolRendererImplementation({
47
+ createToolView: (toolName, snapshot, theme) => snapshot.presentation === "web"
48
+ ? createWebSearchView(toolName, snapshot, theme)
49
+ : createToolView(toolName, snapshot, theme),
50
+ layoutToolView,
51
+ isWebSearchTool: (toolName, toolInfo, details) => isWebSearchTool(toolName, toolInfo, details, getWebSearchTools()),
52
+ invalidateToolView: (state) => {
53
+ invalidateToolViewCache(state);
54
+ invalidateWebSearchViewCache(state);
55
+ },
55
56
  });
57
+ const session = installToolRenderer(pi);
58
+ const shell = installShellRenderer(BashExecutionComponent);
56
59
 
57
- return { shell, tools };
60
+ return { shell, session };
58
61
  }
59
62
 
60
63
  export default function toolsStyleExtension(pi: ExtensionAPI): void {
61
- const installation = installToolsStyle();
64
+ const installation = installToolsStyle(pi);
65
+ // Settings installs memory synchronously; runtime effects precede awaiting that write's durability.
66
+ const applyChange = (change: ToolsStyleConfigChange): Promise<void> => {
67
+ let write: Promise<void>;
68
+ switch (change.kind) {
69
+ case "enabled":
70
+ write = saveSettings({ enabled: change.enabled });
71
+ setToolRendererEnabled(change.enabled);
72
+ if (!change.enabled) clearToolSpinners();
73
+ break;
74
+ case "iconMode":
75
+ write = saveSettings({ iconMode: change.iconMode });
76
+ invalidateToolPresentations();
77
+ break;
78
+ case "tool":
79
+ write = saveSettings({ tools: { [change.name]: change.enabled } });
80
+ invalidateToolPresentations(change.name);
81
+ break;
82
+ case "shellEnabled":
83
+ write = saveSettings({ shellEnabled: change.enabled });
84
+ break;
85
+ }
86
+ if (installation.shell === "unsupported" && isToolRendererEnabled() && isShellEnabledByConfig()) reportShellIncompatibility();
87
+ return write;
88
+ };
62
89
  pi.on("session_start", async (_event, context) => {
63
90
  clearToolSpinners();
91
+ installation.session.reset();
92
+ setShellCompatibilityNotifier(context.hasUI
93
+ ? () => context.ui.notify("Shell styling unavailable: incompatible Pi renderer internals.", "warning")
94
+ : undefined);
64
95
  await loadSettings();
96
+ initializeToolRendererEnabled(getSettings().enabled);
65
97
  setThemeProvider(() => context.ui.theme);
98
+ invalidateToolPresentations();
99
+ if (installation.shell === "unsupported" && isToolRendererEnabled() && isShellEnabledByConfig()) reportShellIncompatibility();
66
100
  });
67
- pi.on("session_shutdown", () => {
101
+ pi.on("session_shutdown", async () => {
102
+ setShellCompatibilityNotifier(undefined);
68
103
  clearToolSpinners();
104
+ installation.session.shutdown();
69
105
  setThemeProvider(undefined);
70
- return Promise.resolve();
106
+ await flushSettingsWrites();
107
+ });
108
+ pi.on("tool_execution_start", (event) => {
109
+ installation.session.executionStarted(event.toolCallId);
110
+ });
111
+ pi.on("tool_execution_end", (event) => {
112
+ installation.session.executionEnded(event.toolCallId);
71
113
  });
72
114
 
73
115
  const command = {
74
116
  description: "Configure tool boxes and icon mode",
75
117
  handler: async (args, context) => {
76
- const [action = "", value, ...extra] = args
77
- .trim()
78
- .toLowerCase()
79
- .split(/\s+/u);
118
+ const [action = "", value, ...extra] = args.trim().toLowerCase().split(/\s+/u);
80
119
  const notifyUsage = (): void => {
81
120
  context.ui.notify(
82
- "Usage: /tools-style [on|off|icons ascii|icons nerd-font|icons off]",
121
+ "Usage: /tools-style [on|off|config|icons ascii|icons nerd-font|icons off]",
83
122
  "warning",
84
123
  );
85
124
  };
86
-
87
- if (action === "icons") {
88
- if (!isIconMode(value) || extra.length > 0) {
125
+ if (action === "config") {
126
+ if (value !== undefined) {
89
127
  notifyUsage();
90
128
  return;
91
129
  }
92
-
93
- await saveIconMode(value);
94
- context.ui.notify(`Tool icons: ${value}.`);
130
+ // RPC hasUI cannot host public custom TUI components.
131
+ if (context.mode !== "tui") {
132
+ context.ui.notify("Tool presentation configuration requires interactive TUI mode.", "warning");
133
+ return;
134
+ }
135
+ await openToolsStyleConfig(pi, context, applyChange);
95
136
  return;
96
137
  }
97
-
98
- if (
99
- (action !== "" && action !== "on" && action !== "off") ||
100
- value !== undefined
101
- ) {
102
- notifyUsage();
103
- return;
138
+ let change: ToolsStyleConfigChange;
139
+ let success: string;
140
+ if (action === "icons") {
141
+ if (!isIconMode(value) || extra.length > 0) {
142
+ notifyUsage();
143
+ return;
144
+ }
145
+ change = { kind: "iconMode", iconMode: value };
146
+ success = `Tool icons: ${value}.`;
147
+ } else {
148
+ if ((action !== "" && action !== "on" && action !== "off") || value !== undefined) {
149
+ notifyUsage();
150
+ return;
151
+ }
152
+ const enabled = resolveRequestedState(action, isToolRendererEnabled());
153
+ change = { kind: "enabled", enabled };
154
+ success = `Tool boxes ${enabled ? "enabled" : "disabled"}.`;
104
155
  }
105
-
106
- if (
107
- installation.tools === "unsupported" ||
108
- installation.shell === "unsupported"
109
- ) {
110
- context.ui.notify(
111
- "Tool boxes unavailable: incompatible Pi renderer internals.",
112
- "warning",
113
- );
156
+ try {
157
+ await applyChange(change);
158
+ } catch {
159
+ try {
160
+ context.ui.notify("Tool presentation choice is active in this session, but not saved.", "error");
161
+ } catch {
162
+ // A detached command UI must not turn a handled write rejection into another failure.
163
+ }
114
164
  return;
115
165
  }
116
-
117
- const current = isRenderDecoratorEnabled(TOOL_RENDER_KEY);
118
- const enabled = resolveRequestedState(action, current);
119
-
120
- setRenderDecoratorEnabled(TOOL_RENDER_KEY, enabled);
121
- setRenderDecoratorEnabled(SHELL_RENDER_KEY, enabled);
122
- if (!enabled) clearToolSpinners();
123
- context.ui.notify(`Tool boxes ${enabled ? "enabled" : "disabled"}.`);
166
+ context.ui.notify(success);
124
167
  },
125
168
  } satisfies Parameters<ExtensionAPI["registerCommand"]>[1];
126
169
 
@@ -137,20 +180,3 @@ function resolveRequestedState(requested: string, current: boolean): boolean {
137
180
  return !current;
138
181
  }
139
182
 
140
- function toolName(component: ToolExecutionComponent): string {
141
- const candidate = (component as unknown as { toolName?: unknown }).toolName;
142
- return typeof candidate === "string" && candidate.length > 0
143
- ? candidate
144
- : "tool";
145
- }
146
-
147
- function toolTitle(component: ToolExecutionComponent): string {
148
- const mode = getIconMode();
149
- const title = formatToolTitle(toolName(component), mode);
150
- const spinner = toolSpinnerFrame(component, mode);
151
- return spinner ? `${title} ${spinner}` : title;
152
- }
153
-
154
- function toolBorderStyle(component: ToolExecutionComponent): BorderStyle {
155
- return createToolBorderStyle(toolName(component));
156
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anthodev/pi-tools-style",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Presentation-only boxes for Pi tool calls and shell commands",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -40,12 +40,12 @@
40
40
  "access": "public"
41
41
  },
42
42
  "peerDependencies": {
43
- "@earendil-works/pi-coding-agent": "0.84.x",
44
- "@earendil-works/pi-tui": "0.84.x"
43
+ "@earendil-works/pi-coding-agent": ">=1.1.0 <2",
44
+ "@earendil-works/pi-tui": ">=1.1.0 <2"
45
45
  },
46
46
  "devDependencies": {
47
- "@earendil-works/pi-coding-agent": "0.84.1",
48
- "@earendil-works/pi-tui": "0.84.1",
47
+ "@earendil-works/pi-coding-agent": "1.1.0",
48
+ "@earendil-works/pi-tui": "1.1.0",
49
49
  "@types/node": "24.12.0",
50
50
  "typescript": "5.9.3",
51
51
  "vitest": "4.1.9"