@underactive/pi-topping 0.2.0 → 0.3.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
@@ -1,55 +1,61 @@
1
1
  # pi-topping
2
2
 
3
- We garnish our pies. It seemed rude not to extend Pi the same courtesy. This is a [Pi](https://github.com/earendil-works/pi-coding-agent) extension that changes the default "Working..." loader with a shimmering activity word, a scrolling token-activity meter, an elapsed timer, and a live output-token count.
3
+ We garnish our pies. It seemed rude not to extend Pi the same courtesy. This is a [Pi](https://github.com/earendil-works/pi-coding-agent) extension that adds some quality of life improvements to Pi's UX, while looking good in the process:
4
4
 
5
- ![Demo of pi-topping's shimmering activity word, scrolling activity meter, elapsed timer, and token count](https://raw.githubusercontent.com/underactive/pi-topping/main/demo.gif)
5
+ **User Prompt** — high-vis bordered prompt box with configurable border color, pi icon toggle, and timestamp.
6
+
7
+ ![Example of pi-topping's decorated user prompt](https://raw.githubusercontent.com/underactive/pi-topping/main/demo_user_prompt.png)
6
8
 
7
- ## What it does
9
+ **Working Loader Text** — animated spinner (with color choice), randomized activity word, shimmer (with direction), token activity meter (with color, direction, and dim toggle), elapsed timer, and output token display.
8
10
 
9
- - Picks a random activity word (`Cerebrating…`, `Noodling…`, `Zigzagging…`, etc.) when you submit a prompt, and a new one each time a tool call starts.
10
- - Sweeps a theme-aware shimmer across the word while it's showing.
11
- - Shows an elapsed timer (`Xm YYs`) that counts up from when you submitted the prompt.
12
- - Shows a live output-token estimate (`↓ N tokens`), reconciled against the real usage total once each assistant message finishes.
13
- - Resets everything — word, timer, token count — on each new prompt, and restores Pi's normal loader between turns.
14
- - Leaves a durable completion marker in the transcript after each finished turn, e.g. `π Baked for 6m 41s` — a fresh random word and the total time for that turn, rendered with `π` in the theme's primary text color and the rest dimmed. It's TUI-only display content and never enters LLM context.
11
+ ![Demo of pi-topping's shimmering activity word, scrolling activity meter, elapsed timer, and token count](https://raw.githubusercontent.com/underactive/pi-topping/main/demo.gif)
15
12
 
16
- ## Activity meter
13
+ **Completion Marker** — end-of-turn marker with icon, randomized verb, and token consumption display. Hooks into Pi's `agent_settled` event.
17
14
 
18
- Next to Pi's spinner there's an eight-column scrolling meter that reflects how fast tokens are streaming in. It updates every 100ms and uses your theme's accent color, so it should blend in with whatever theme you're running.
15
+ ![Example of pi-topping's completion marker](https://raw.githubusercontent.com/underactive/pi-topping/main/demo_completion_marker.png)
19
16
 
20
17
  ## Settings
21
18
 
22
- Run `/topping-settings` (TUI only) to toggle things on or off:
19
+ Run `/topping-settings` (TUI only) to customize your toppings:
23
20
 
24
21
  ```
25
- ╔═[ Pi Topping: Settings ]══════════════════════════╗
26
- ╟─ Preview ─────────────────────────────────────────╢
27
- ║ ║
28
- ║ ⠋ Crafting ⣤⣤⣤⣤ (0m 03s · ↓ 84 tokens) ║
29
- ║ ║
30
- ╟─ Decorations ─────────────────────────────────────╢
31
- ║ ▸ [■] Animated spinner ON ║
32
- ║ [■] "Working..." text shimmer ON ║
33
- ║ [■] Token activity monitor ON ║
34
- ╟─ Options ─────────────────────────────────────────╢
35
- ║ [■] Substitute Pi's "Working..." message ON ║
36
- ║ [■] Elapsed time since prompt ON ║
37
- ║ [■] Show output tokens ON ║
38
- ║ [■] Show completion marker ON ║
39
- ║ [ ] Scrolling: Right → Left OFF ║
40
- ╟───────────────────────────────────────────────────╢
41
- ║ ↑↓ move ␣ toggle ⏎ apply esc cancel ║
42
- ╚═══════════════════════════════════════════════════╝
22
+ ╔═┥ Pi Topping: Settings ┝═══════════════════════════════════╗
23
+ ╟─ Preview ──────────────────────────────────────────────────╢
24
+ ║ ║
25
+ ║ Crafting (0m 03s · ↓ 84 tokens) ║
26
+ ║ ║
27
+ ╟─ User Prompt ──────────────────────────────────────────────╢
28
+ ║ [■] High-vis prompt ON ║
29
+ ║ [■] Border color ‹ accent › ║
30
+ ║ [■] Pi icon ON ║
31
+ ║ [■] Timestamp ON ║
32
+ ║ ║
33
+ ╟─ Working Loader Text ──────────────────────────────────────╢
34
+ ║ [■] Animated spinner ON ║
35
+ ║ ▸ [■] Animated spinner color ‹ accent › ║
36
+ ║ [■] Randomize "Working" text ON ║
37
+ ║ [■] Text shimmer ON ║
38
+ ║ [■] Text shimmer direction ‹ Left to Right › ║
39
+ ║ [■] Token activity monitor ON ║
40
+ ║ [■] Token activity monitor color ‹ accent › ║
41
+ ║ [■] Token activity monitor direction ‹ Left to Right › ║
42
+ ║ [■] Token activity monitor dimmed OFF ║
43
+ ║ [■] Elapsed time since prompt ON ║
44
+ ║ [■] Show output tokens ON ║
45
+ ║ ║
46
+ ╟─ Completion Marker ────────────────────────────────────────╢
47
+ ║ [■] Show completion marker ON ║
48
+ ║ [■] Pi icon ON ║
49
+ ║ [■] Randomize "Worked" text ON ║
50
+ ║ [■] Tokens spent ON ║
51
+ ║ ║
52
+ ╟─ Options ──────────────────────────────────────────────────╢
53
+ ║ [■] Use NerdFont icons ON ║
54
+ ╟────────────────────────────────────────────────────────────╢
55
+ ║ ↑↓ move ←→ select ␣ toggle ⏎ apply esc cancel ║
56
+ ╚═════════════════════════════════════════════════════┥ 1/9 ┝╝
43
57
  ```
44
58
 
45
- **Decorations** — animated spinner, word shimmer, activity meter.
46
-
47
- **Options** — whether to substitute the random word at all, the elapsed timer, the token count, the completion marker, and the meter scrolling direction.
48
-
49
- The menu has a live preview so you can see changes as you make them. Everything is on by default, so nothing changes unless you open the menu. Use `↑↓` to move, `␣` to toggle, `⏎` to save, `esc` to cancel. Settings are saved to `~/.pi/agent/pi-topping/settings.json`.
50
-
51
- The toggle-menu component itself lives in `menu.ts` and is reusable by other extensions if you want a similar settings UI.
52
-
53
59
  ## Install
54
60
 
55
61
  ```bash
package/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** pi-topping extension entry point. */
2
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import { SessionManager } from "./session.ts";
3
+ import { SessionManager } from "./src/session.ts";
4
4
 
5
5
  export default function (pi: ExtensionAPI): void {
6
6
  new SessionManager(pi).install();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@underactive/pi-topping",
3
- "version": "0.2.0",
4
- "description": "A Pi extension that replaces the default \"Working...\" loader with a shimmering, randomized activity word, live elapsed timer, and output-token activity meter",
3
+ "version": "0.3.0",
4
+ "description": "A Pi extension that decorates user prompts and replaces the default \"Working...\" loader with a shimmering, randomized activity word, live elapsed timer, and output-token activity meter",
5
5
  "keywords": ["pi-package", "pi-extension"],
6
6
  "license": "MIT",
7
7
  "author": "Eric Sison",
@@ -20,7 +20,7 @@
20
20
  "publishConfig": {
21
21
  "access": "public"
22
22
  },
23
- "files": ["*.ts", "README.md", "LICENSE"],
23
+ "files": ["index.ts", "src/**/*.ts", "README.md", "LICENSE"],
24
24
  "scripts": {
25
25
  "test": "node --test tests/*.test.ts",
26
26
  "typecheck": "tsc --noEmit"
@@ -117,12 +117,19 @@ export class ActivityMeter {
117
117
  this.#levels.fill(0);
118
118
  }
119
119
 
120
- /** Colorize a meter cell using the default theme mapping (dim at IDLE, accent otherwise). */
120
+ /** Colorize a meter cell using the default theme mapping (dim at IDLE, `color` otherwise). */
121
121
  static colorizeCell(
122
122
  level: ActivityMeterLevel,
123
123
  char: string,
124
124
  theme: { fg: (style: ThemeColor, s: string) => string },
125
+ color: ThemeColor = "accent",
126
+ dimmed?: boolean,
125
127
  ): string {
126
- return level === ActivityMeterLevel.IDLE ? theme.fg("dim", char) : theme.fg("accent", char);
128
+ if (level === ActivityMeterLevel.IDLE) return theme.fg("dim", char);
129
+ const colored = theme.fg(color, char);
130
+ // ANSI dim (SGR 2) reduces color brightness; SGR 22 resets dim/bold.
131
+ // This is necessary because theme.fg("dim", ...) sets a gray color instead
132
+ // of applying the ANSI dim attribute, so nesting would overwrite the accent color.
133
+ return dimmed ? `\x1b[2m${colored}\x1b[22m` : colored;
127
134
  }
128
135
  }
@@ -1,5 +1,8 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
 
3
+ /** Pi's default working-indicator frames (same braille spinner as pi-tui's Loader). */
4
+ export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
5
+
3
6
  /** Format an output-token count for the working indicator. */
4
7
  export function formatTokens(count: number): string {
5
8
  if (count < 1000) return count.toString();
@@ -67,29 +70,41 @@ export function buildWorkingMessage(
67
70
  export function shimmerString(
68
71
  text: string,
69
72
  elapsedMs: number,
70
- theme: Pick<Theme, "fg" | "bold">,
73
+ theme: Pick<Theme, "getFgAnsi">,
74
+ direction: "ltr" | "rtl" = "ltr",
71
75
  ): string {
72
76
  const chars = [...text];
73
77
  if (chars.length === 0) return "";
74
78
  const SHIMMER_SWEEP_S = 2.0;
75
79
  const SHIMMER_BAND_HALF = 5.0;
76
80
  const SHIMMER_PADDING = 10;
81
+ const SHIMMER_BASE = ansiToRgb(theme.getFgAnsi("dim"));
82
+ const SHIMMER_HIGHLIGHT = ansiToRgb(theme.getFgAnsi("text"));
77
83
  const period = chars.length + SHIMMER_PADDING * 2;
78
84
  const elapsedS = elapsedMs / 1000;
79
- const pos = ((elapsedS % SHIMMER_SWEEP_S) / SHIMMER_SWEEP_S) * period;
85
+ const sweep = ((elapsedS % SHIMMER_SWEEP_S) / SHIMMER_SWEEP_S) * period;
86
+ const pos = direction === "rtl" ? period - sweep : sweep;
80
87
 
81
88
  return chars
82
89
  .map((ch, i) => {
83
- const dist = Math.abs(i + SHIMMER_PADDING - pos);
84
- const intensity =
85
- dist <= SHIMMER_BAND_HALF
86
- ? 0.5 * (1 + Math.cos((Math.PI * dist) / SHIMMER_BAND_HALF))
87
- : 0;
88
- if (intensity > 0.6) return theme.bold(theme.fg("text", ch));
89
- if (intensity > 0.15) return theme.fg("muted", ch);
90
- return theme.fg("dim", ch);
90
+ const dist = Math.abs((i + SHIMMER_PADDING) - pos);
91
+ const t = dist <= SHIMMER_BAND_HALF
92
+ ? 0.5 * (1 + Math.cos(Math.PI * dist / SHIMMER_BAND_HALF))
93
+ : 0;
94
+ const alpha = t * 0.9;
95
+ const r = Math.round(SHIMMER_HIGHLIGHT[0] * alpha + SHIMMER_BASE[0] * (1 - alpha));
96
+ const g = Math.round(SHIMMER_HIGHLIGHT[1] * alpha + SHIMMER_BASE[1] * (1 - alpha));
97
+ const b = Math.round(SHIMMER_HIGHLIGHT[2] * alpha + SHIMMER_BASE[2] * (1 - alpha));
98
+ const bold = t > 0.2 ? "\x1b[1m" : "";
99
+ return `${bold}\x1b[38;2;${r};${g};${b}m${ch}\x1b[22m`;
91
100
  })
92
- .join("");
101
+ .join("") + "\x1b[0m";
102
+ }
103
+
104
+ function ansiToRgb(ansi: string): [number, number, number] {
105
+ const match = ansi.match(/^\x1b\[38;2;(\d+);(\d+);(\d+)m$/);
106
+ if (!match) throw new Error(`Shimmer requires truecolor theme colors, received ${JSON.stringify(ansi)}`);
107
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
93
108
  }
94
109
 
95
110
  /**