@anthodev/pi-tools-style 0.1.0 → 0.1.3

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,6 +2,12 @@
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>
10
+
5
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.
6
12
 
7
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.
@@ -17,6 +23,26 @@ Built-in tools, MCP integrations, AFT, and third-party extensions keep control o
17
23
  - **Image-safe fallback** — skips Kitty, iTerm2, and Sixel rows instead of corrupting terminal image sequences.
18
24
  - **Reload-safe state** — keeps wrappers and runtime configuration idempotent across `/reload`.
19
25
 
26
+ ## Installation
27
+
28
+ The current release targets Pi `0.84.x` and requires Node.js 22 or newer.2
29
+
30
+ To install it, use the following command:
31
+
32
+ ```bash
33
+ pi install npm:@anthodev/pi-tools-style
34
+ ```
35
+
36
+ To install this checkout as a local Pi package:
37
+
38
+ ```bash
39
+ cd /absolute/path/to/pi-tools-style
40
+ npm install --legacy-peer-deps
41
+ pi install "$(pwd)"
42
+ ```
43
+
44
+ Pi stores the package in user settings and references the directory directly, so local changes become available after `/reload` or a restart.
45
+
20
46
  ## What it looks like
21
47
 
22
48
  ASCII mode stays readable in any terminal:
@@ -45,38 +71,6 @@ Once execution finishes, the spinner disappears and the title remains stable:
45
71
 
46
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.
47
73
 
48
- ## Installation
49
-
50
- The current release targets Pi `0.84.x` and requires Node.js 22 or newer.
51
-
52
- To install this checkout as a local Pi package:
53
-
54
- ```bash
55
- cd /absolute/path/to/pi-tools-style
56
- npm install --legacy-peer-deps
57
- pi install "$(pwd)"
58
- ```
59
-
60
- Pi stores the package in user settings and references the directory directly, so local changes become available after `/reload` or a restart.
61
-
62
- To install it only for the current project, run this from that project:
63
-
64
- ```bash
65
- pi install -l /absolute/path/to/pi-tools-style
66
- ```
67
-
68
- To try the extension without installing it:
69
-
70
- ```bash
71
- pi -e /absolute/path/to/pi-tools-style/index.ts
72
- ```
73
-
74
- Once the package is published to npm, it can also be installed with:
75
-
76
- ```bash
77
- pi install npm:@anthodev/pi-tools-style
78
- ```
79
-
80
74
  ## Usage
81
75
 
82
76
  Boxes start enabled. Manage them from Pi with:
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@anthodev/pi-tools-style",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Presentation-only boxes for Pi tool calls and shell commands",
5
- "keywords": ["pi-package", "pi-extension", "tui", "tools"],
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "tui",
9
+ "tools"
10
+ ],
6
11
  "type": "module",
7
12
  "license": "MIT",
8
13
  "repository": {
@@ -34,6 +34,23 @@ interface StoredConfig extends Omit<RenderDecoratorConfig, "enabled"> {
34
34
  enabled: boolean;
35
35
  }
36
36
 
37
+ interface ResolvedFrameOptions {
38
+ options: FrameOptions;
39
+ styleSignature: string | undefined;
40
+ title: string | undefined;
41
+ }
42
+
43
+ interface RenderCacheEntry {
44
+ config: StoredConfig;
45
+ output: string[];
46
+ rendered: readonly string[];
47
+ styleSignature: string | undefined;
48
+ title: string | undefined;
49
+ width: number;
50
+ }
51
+
52
+ const BORDER_STYLE_PROBE = "╭─╮│╰─╯";
53
+
37
54
  export function installRenderDecorator<T extends Renderable>(
38
55
  component: RenderableConstructor<T>,
39
56
  config: RenderDecoratorConfig<T>,
@@ -96,6 +113,8 @@ function createDecoratedRenderer(
96
113
  key: symbol,
97
114
  renderOriginal: RenderFunction,
98
115
  ): RenderFunction {
116
+ const cache = new WeakMap<Renderable, RenderCacheEntry>();
117
+
99
118
  return function decoratedRender(width: number): string[] {
100
119
  const current = readConfig(key);
101
120
  if (!current?.enabled) return renderOriginal.call(this, width);
@@ -109,22 +128,38 @@ function createDecoratedRenderer(
109
128
  }
110
129
 
111
130
  try {
112
- return decorateLines(this, rendered, width, current);
131
+ const resolved = resolveFrameOptions(this, current);
132
+ const cached = cache.get(this);
133
+ if (
134
+ cached?.config === current &&
135
+ cached.width === width &&
136
+ cached.title === resolved.title &&
137
+ cached.styleSignature === resolved.styleSignature &&
138
+ linesEqual(cached.rendered, rendered)
139
+ ) {
140
+ return cached.output;
141
+ }
142
+
143
+ const output = decorateLines(rendered, width, current, resolved.options);
144
+ cache.set(this, {
145
+ config: current,
146
+ output,
147
+ rendered: [...rendered],
148
+ styleSignature: resolved.styleSignature,
149
+ title: resolved.title,
150
+ width,
151
+ });
152
+ return output;
113
153
  } catch {
114
154
  return renderOriginal.call(this, width);
115
155
  }
116
156
  };
117
157
  }
118
158
 
119
- function decorateLines(
159
+ function resolveFrameOptions(
120
160
  instance: Renderable,
121
- rendered: readonly string[],
122
- width: number,
123
161
  config: StoredConfig,
124
- ): string[] {
125
- const lines = config.trimOuterRules
126
- ? trimOuterRules(rendered)
127
- : [...rendered];
162
+ ): ResolvedFrameOptions {
128
163
  const title = config.title?.(instance);
129
164
  const options: FrameOptions = {};
130
165
 
@@ -137,9 +172,34 @@ function decorateLines(
137
172
  }
138
173
  if (title) options.title = title;
139
174
 
175
+ return {
176
+ options,
177
+ // Theme providers can change border output without changing component content.
178
+ styleSignature: styleBorder?.(BORDER_STYLE_PROBE),
179
+ title,
180
+ };
181
+ }
182
+
183
+ function decorateLines(
184
+ rendered: readonly string[],
185
+ width: number,
186
+ config: StoredConfig,
187
+ options: FrameOptions,
188
+ ): string[] {
189
+ const lines = config.trimOuterRules
190
+ ? trimOuterRules(rendered)
191
+ : [...rendered];
192
+
140
193
  return frameLines(lines, width, options);
141
194
  }
142
195
 
196
+ function linesEqual(left: readonly string[], right: readonly string[]): boolean {
197
+ return (
198
+ left.length === right.length &&
199
+ left.every((line, index) => line === right[index])
200
+ );
201
+ }
202
+
143
203
  function storeConfig<T extends Renderable>(
144
204
  config: RenderDecoratorConfig<T>,
145
205
  ): void {