@underactive/pi-topping 0.6.4 → 0.6.5

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
@@ -6,7 +6,7 @@ We garnish our pies. It seemed rude not to extend Pi the same courtesy. This is
6
6
 
7
7
  ![Example of pi-topping's decorated user prompt](https://raw.githubusercontent.com/underactive/pi-topping/main/media/demo_user_prompt.png)
8
8
 
9
- **“Working” Loader** — animated spinner (with color choice), randomized activity word, optional word packs, shimmer (with direction, speed, and invert option), token activity monitor (with color, direction, and dim toggle), elapsed timer, output token display, live output-token rate (with color choice and dim toggle), and the response model (with visibility, color, and dim controls) — all arrangeable left to right. During a blocking extension prompt, the loader switches to a stable dim waiting line with a pulsing indicator, then resumes afterward.
9
+ **“Working” Loader** — animated spinner (with color choice), randomized activity word, optional word packs, shimmer, token activity monitor, elapsed timer, output token display, live output-token rate, and the response model when it meaningfully differs from the selected model — all arrangeable left to right. During a blocking extension prompt, the loader switches to a stable dim waiting line with a pulsing indicator, then resumes afterward.
10
10
 
11
11
  ![Demo of pi-topping's shimmering activity word, scrolling activity meter, elapsed timer, token count, and token rate](https://raw.githubusercontent.com/underactive/pi-topping/main/media/demo.gif)
12
12
 
@@ -14,83 +14,91 @@ We garnish our pies. It seemed rude not to extend Pi the same courtesy. This is
14
14
 
15
15
  ![Example of pi-topping's completion marker](https://raw.githubusercontent.com/underactive/pi-topping/main/media/demo_completion_marker.png)
16
16
 
17
+ ## Install
18
+
19
+ ```bash
20
+ pi install npm:@underactive/pi-topping
21
+ ```
22
+
23
+ Restart Pi (or run `/reload`) to pick it up.
24
+
17
25
  ## Settings
18
26
 
19
27
  Run `/topping-settings` (TUI only) to customize your toppings. Settings persist to
20
28
  `~/.pi/agent/pi-topping/settings.json`; hand edits are tolerated on load.
21
29
 
22
30
  ```
23
- ╔═[ Pi Topping: Settings ]═══════════════════════════════════╗
24
- ╟─ Preview ──────────────────────────────────────────────────╢
25
- ║ ║
26
- ║ ⠋ Crafting… ⣠⣤⣶⣶⣤⣠⣀⢀ 28 tps · 3s · ↓ 84 tokens · test-model ║
27
- ║ ║
28
- ╟─ User Prompt ──────────────────────────────────────────────╢
29
- ║ [■] High-vis prompt ON ║
30
- ║ [■] Border style ‹ double › ║
31
- ║ [■] Border color ‹ borderAccent › ║
32
- ║ [■] Pi icon ON ║
33
- ║ [■] Timestamp ON ║
34
- ║ [■] Provider ON ║
35
- ║ [■] Model ON ║
36
- ║ ║
37
- ╟─ “Working” Loader ─────────────────────────────────────────╢
38
- ║ [■] Animated spinner ON ║
39
- ║ ❯ [■] Animated spinner color ‹ accent › ║
40
- ║ [■] Randomize “Working” text ON ║
41
- ║ [■] Text shimmer ON ║
42
- ║ [■] Invert shimmer OFF ║
43
- ║ [■] Text shimmer direction ‹ Left to Right › ║
44
- ║ [■] Text shimmer speed ‹ Normal › ║
45
- ║ [■] Token activity monitor ON ║
46
- ║ [■] Token activity monitor color ‹ accent › ║
47
- ║ [■] Token activity monitor direction ‹ Right to Left › ║
48
- ║ [■] Token activity monitor dimmed OFF ║
49
- ║ [■] Elapsed time since prompt ON ║
50
- ║ [■] Show output tokens ON ║
51
- ║ [■] Token rate ON ║
52
- ║ [■] Token rate color ‹ warning › ║
53
- ║ [■] Token rate dimmed OFF ║
54
- ║ [■] Response model ON ║
55
- ║ [■] Response model color ‹ accent › ║
56
- ║ [■] Response model dimmed OFF ║
57
- ║ ║
58
- ╟─ Elements Order ───────────────────────────────────────────╢
59
- ║ [ ] Animated spinner ║
60
- ║ [ ] “Working” text ║
61
- ║ [ ] Token activity monitor ║
62
- ║ [ ] Token rate ║
63
- ║ [ ] Elapsed time ║
64
- ║ [ ] Output tokens ║
65
- ║ [ ] Response model ║
66
- ║ ║
67
- ╟─ Completion Marker ────────────────────────────────────────╢
68
- ║ [■] Show completion marker ON ║
69
- ║ [■] Marker style ‹ elite › ║
70
- ║ [■] Border style ‹ none › ║
71
- ║ [■] Border color ‹ borderAccent › ║
72
- ║ [■] Pi icon ON ║
73
- ║ [■] Randomize “Worked” text ON ║
74
- ║ [■] Tokens spent ON ║
75
- ║ [■] Mid-turn inputs ON ║
76
- ║ ║
77
- ╟─ Word Packs ───────────────────────────────────────────────╢
78
- ║ [ ] Doctor Who OFF ║
79
- ║ [ ] Firefly OFF ║
80
- ║ [ ] Hitchhiker's Guide OFF ║
81
- ║ [ ] The Lord of the Rings OFF ║
82
- ║ [ ] The Matrix OFF ║
83
- ║ [ ] Portal OFF ║
84
- ║ [ ] SimCity OFF ║
85
- ║ [ ] Star Trek OFF ║
86
- ║ [ ] Star Wars OFF ║
87
- ║ ║
88
- ╟─ Options ──────────────────────────────────────────────────╢
89
- ║ [■] Use NerdFont icons ON ║
90
- ╟────────────────────────────────────────────────────────────╢
91
- ║ ↑↓ move PgUp/PgDn page ←→ select ║
92
- ║ ␣ toggle ⏎ apply esc cancel ║
93
- ╚════════════════════════════════════════════════════[ 9/50 ]╝
31
+ ╔═[ Pi Topping: Settings ]══════════════════════════════════════╗
32
+ ╟─ Preview ─────────────────────────────────────────────────────╢
33
+ ║ ║
34
+ ║ ⠋ Crafting… ⣠⣤⣶⣶⣤⣠⣀⢀ 28 tps · 3s · ↓ 84 tokens · test-model ║
35
+ ║ ║
36
+ ╟─ User Prompt ─────────────────────────────────────────────────╢
37
+ ║ [■] High-vis prompt ON ║
38
+ ║ [■] Border style ‹ double › ║
39
+ ║ [■] Border color ‹ borderAccent › ║
40
+ ║ [■] Pi icon ON ║
41
+ ║ [■] Timestamp ON ║
42
+ ║ [■] Provider ON ║
43
+ ║ [■] Model ON ║
44
+ ║ ║
45
+ ╟─ “Working” Loader ────────────────────────────────────────────╢
46
+ ║ [■] Animated spinner ON ║
47
+ ║ ❯ [■] Animated spinner color ‹ accent › ║
48
+ ║ [■] Randomize “Working” text ON ║
49
+ ║ [■] Text shimmer ON ║
50
+ ║ [■] Invert shimmer OFF ║
51
+ ║ [■] Text shimmer direction ‹ Left to Right › ║
52
+ ║ [■] Text shimmer speed ‹ Normal › ║
53
+ ║ [■] Token activity monitor ON ║
54
+ ║ [■] Token activity monitor color ‹ accent › ║
55
+ ║ [■] Token activity monitor direction ‹ Right to Left › ║
56
+ ║ [■] Token activity monitor dimmed OFF ║
57
+ ║ [■] Elapsed time since prompt ON ║
58
+ ║ [■] Show output tokens ON ║
59
+ ║ [■] Token rate ON ║
60
+ ║ [■] Token rate color ‹ warning › ║
61
+ ║ [■] Token rate dimmed OFF ║
62
+ ║ [■] Response model ON ║
63
+ ║ [■] Response model color ‹ accent › ║
64
+ ║ [■] Response model dimmed OFF ║
65
+ ║ ║
66
+ ╟─ Elements Order ──────────────────────────────────────────────╢
67
+ ║ [ ] Animated spinner ║
68
+ ║ [ ] “Working” text ║
69
+ ║ [ ] Token activity monitor ║
70
+ ║ [ ] Token rate ║
71
+ ║ [ ] Elapsed time ║
72
+ ║ [ ] Output tokens ║
73
+ ║ [ ] Response model ║
74
+ ║ ║
75
+ ╟─ Completion Marker ───────────────────────────────────────────╢
76
+ ║ [■] Show completion marker ON ║
77
+ ║ [■] Marker style ‹ elite › ║
78
+ ║ [■] Border style ‹ none › ║
79
+ ║ [■] Border color ‹ borderAccent › ║
80
+ ║ [■] Pi icon ON ║
81
+ ║ [■] Randomize “Worked” text ON ║
82
+ ║ [■] Tokens spent ON ║
83
+ ║ [■] Mid-turn inputs ON ║
84
+ ║ ║
85
+ ╟─ Word Packs ──────────────────────────────────────────────────╢
86
+ ║ [ ] Doctor Who OFF ║
87
+ ║ [ ] Firefly OFF ║
88
+ ║ [ ] Hitchhiker's Guide OFF ║
89
+ ║ [ ] The Lord of the Rings OFF ║
90
+ ║ [ ] The Matrix OFF ║
91
+ ║ [ ] Portal OFF ║
92
+ ║ [ ] SimCity OFF ║
93
+ ║ [ ] Star Trek OFF ║
94
+ ║ [ ] Star Wars OFF ║
95
+ ║ ║
96
+ ╟─ Options ─────────────────────────────────────────────────────╢
97
+ ║ [■] Use NerdFont icons ON ║
98
+ ╟───────────────────────────────────────────────────────────────╢
99
+ ║ ↑↓ move PgUp/PgDn page ←→ select ║
100
+ ║ ␣ toggle ⏎ apply esc cancel ║
101
+ ╚═══════════════════════════════════════════════════════[ 9/50 ]╝
94
102
  ```
95
103
 
96
104
  ### Word packs
@@ -123,15 +131,7 @@ full brightness for 1.5 seconds, then fades through five theme-aware shades to t
123
131
  the next 0.25 seconds before returning to the placeholder; a new count restores full brightness and
124
132
  restarts the cycle.
125
133
 
126
- The response model is captured from assistant responses, displayed as its sanitized value with no label, and defaults to the final loader detail after output tokens. It can be hidden, recolored, permanently dimmed, or reordered. After the agent settles, its final value remains visible for 3 seconds, fades through five theme-aware shades over 0.5 seconds, then clears; new work cancels the pending fade.
127
-
128
- ## Install
129
-
130
- ```bash
131
- pi install npm:@underactive/pi-topping
132
- ```
133
-
134
- Restart Pi (or run `/reload`) to pick it up.
134
+ The response model is captured from assistant responses, displayed as its sanitized value with no label only when it meaningfully differs from the selected model, and defaults to the final loader detail after output tokens. Equivalent local paths and decorated filenames are suppressed. It can be hidden, recolored, permanently dimmed, or reordered. After the agent settles, its final value remains visible for 3 seconds, fades through five theme-aware shades over 0.5 seconds, then clears; new work cancels the pending fade.
135
135
 
136
136
  ## Requirements
137
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@underactive/pi-topping",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "A Pi extension that adds quality of life improvements to Pi's UX: a high-vis user prompt, a shimmering working loader with token activity meter and elapsed timer, and an end-of-turn completion marker",
5
5
  "keywords": ["pi-package", "pi-extension"],
6
6
  "license": "MIT",
package/src/session.ts CHANGED
@@ -42,7 +42,7 @@ import { applyMenuResult, buildMenuSections, loadSettings, saveSettings, type Se
42
42
  import { notifyMissingToppingsOnce } from "./toppings.ts";
43
43
  import { PreviewRenderer } from "./preview.ts";
44
44
  import { buildCompletionMarkerContent, buildCompletionMarkerLine, PROMPT_BOX_TYPE, promptBoxRenderer, type PromptBoxDetails } from "./prompt-decorator.ts";
45
- import { stripControlChars } from "./util.ts";
45
+ import { modelsResemble, stripControlChars } from "./util.ts";
46
46
  import { loadBundledWordPacks, loadUserWordPacks, pickWorkingTextSelection, type WorkingTextSelection, type WordPack } from "./word-packs.ts";
47
47
 
48
48
  type MessageStartEvent = Extract<ExtensionEvent, { type: "message_start" }>;
@@ -86,6 +86,8 @@ interface SessionState {
86
86
  lastTokenRateSampledAt: number;
87
87
  lastTokenRateTotal: number;
88
88
  responseModel: string;
89
+ /** Last raw responseModel value seen, or the NOT_SENT sentinel before the first call. */
90
+ lastResponseModelRaw: unknown;
89
91
  responseModelHoldTimer: ReturnType<typeof setTimeout> | null;
90
92
  responseModelFadeTimer: ReturnType<typeof setInterval> | null;
91
93
  responseModelFadeGeneration: number;
@@ -123,6 +125,7 @@ function makeFreshState(): SessionState {
123
125
  lastTokenRateSampledAt: 0,
124
126
  lastTokenRateTotal: 0,
125
127
  responseModel: "",
128
+ lastResponseModelRaw: NOT_SENT,
126
129
  responseModelHoldTimer: null,
127
130
  responseModelFadeTimer: null,
128
131
  responseModelFadeGeneration: 0,
@@ -148,6 +151,7 @@ export class SessionManager {
148
151
  #settings = loadSettings();
149
152
  #userPacks: WordPack[] = [];
150
153
  #bundledPacks = loadBundledWordPacks();
154
+ #allPacks: WordPack[] = [...this.#bundledPacks];
151
155
  #currentCtx: ExtensionContext | null = null;
152
156
  readonly #pi: ExtensionAPI;
153
157
 
@@ -163,6 +167,7 @@ export class SessionManager {
163
167
  this.#state = makeFreshState();
164
168
  this.#settings = loadSettings();
165
169
  this.#userPacks = loadUserWordPacks();
170
+ this.#allPacks = [...this.#bundledPacks, ...this.#userPacks];
166
171
  this.#state.activityMeter.setDirection(this.#settings.decorations.meterDirection);
167
172
  if (this.usable(ctx)) {
168
173
  this.applyIndicator(ctx);
@@ -446,7 +451,17 @@ export class SessionManager {
446
451
  }
447
452
 
448
453
  private updateResponseModel(raw: unknown): void {
454
+ if (raw === this.#state.lastResponseModelRaw) return;
455
+ this.#state.lastResponseModelRaw = raw;
449
456
  const responseModel = typeof raw === "string" ? stripControlChars(raw).trim() : "";
457
+ const selectedModel = this.#currentCtx?.model?.id;
458
+ if (responseModel && modelsResemble(selectedModel, responseModel)) {
459
+ if (this.#state.responseModel) {
460
+ this.#state.responseModel = "";
461
+ this.tick();
462
+ }
463
+ return;
464
+ }
450
465
  if (responseModel && responseModel !== this.#state.responseModel) {
451
466
  this.#state.responseModel = responseModel;
452
467
  this.tick();
@@ -511,7 +526,7 @@ export class SessionManager {
511
526
  }
512
527
 
513
528
  private pickWorkingWord(): WorkingTextSelection {
514
- return pickWorkingTextSelection(this.#settings.wordPacks, [...this.#bundledPacks, ...this.#userPacks]);
529
+ return pickWorkingTextSelection(this.#settings.wordPacks, this.#allPacks);
515
530
  }
516
531
 
517
532
  private resetTurn(now: number): void {
@@ -523,6 +538,7 @@ export class SessionManager {
523
538
  state.confirmTokens = 0;
524
539
  state.liveTokens = 0;
525
540
  state.responseModel = "";
541
+ state.lastResponseModelRaw = NOT_SENT;
526
542
  state.activityMeter.reset();
527
543
  resetTokenRateState(state);
528
544
  state.midTurnInputs = 0;
@@ -639,6 +655,7 @@ export class SessionManager {
639
655
  }
640
656
 
641
657
  this.#userPacks = loadUserWordPacks();
658
+ this.#allPacks = [...this.#bundledPacks, ...this.#userPacks];
642
659
  const before = this.indicatorFingerprint();
643
660
  const preview = new PreviewRenderer(ctx, [...this.#bundledPacks, ...this.#userPacks]);
644
661
  const result = await showMenu<Record<string, boolean | string>>(ctx, {
package/src/util.ts CHANGED
@@ -7,3 +7,12 @@ export function isPlainObject(value: unknown): value is Record<string, unknown>
7
7
  export function stripControlChars(text: string): string {
8
8
  return text.replace(/[\p{Cc}\p{Cf}]/gu, "");
9
9
  }
10
+
11
+ /** True when a reported response model appears to be a decorated form of the selected model. */
12
+ export function modelsResemble(selectedModel?: string, responseModel?: string): boolean {
13
+ if (!selectedModel || !responseModel) return false;
14
+ const selectedName = selectedModel.split(/[\\/]/).at(-1)?.toLowerCase() ?? "";
15
+ const selectedTokens = selectedName.match(/[a-z0-9]{2,}/g) ?? [];
16
+ const normalizedResponse = responseModel.toLowerCase();
17
+ return selectedTokens.length > 0 && selectedTokens.every((token) => normalizedResponse.includes(token));
18
+ }
package/src/word-packs.ts CHANGED
@@ -84,7 +84,6 @@ export function loadBundledWordPacks(): WordPack[] {
84
84
  packs.push(pack);
85
85
  }
86
86
  }
87
- if (!packs.some((pack) => pack.id === "simcity")) throw new Error("Missing bundled SimCity word pack");
88
87
  const bundledIds = new Set(packs.map((pack) => pack.id));
89
88
  for (const id of RESERVED_BUNDLED_PACK_IDS) {
90
89
  if (!bundledIds.has(id)) throw new Error(`Missing bundled word pack: ${id}`);
@@ -109,8 +108,10 @@ export function isWordPackEnabled(id: string, enabled: Record<string, boolean>):
109
108
  }
110
109
 
111
110
  export function selectWorkingTextSelection(enabled: Record<string, boolean>, packs: readonly WordPack[], fraction: number): WorkingTextSelection {
112
- const pool = [...WORDS, ...packs.filter((pack) => isWordPackEnabled(pack.id, enabled)).flatMap((pack) => pack.words)];
113
- const entry = pool[Math.min(pool.length - 1, Math.floor(fraction * pool.length))]!;
111
+ const enabledWords = packs.filter((pack) => isWordPackEnabled(pack.id, enabled)).flatMap((pack) => pack.words);
112
+ const total = WORDS.length + enabledWords.length;
113
+ const idx = Math.min(total - 1, Math.floor(fraction * total));
114
+ const entry = idx < WORDS.length ? WORDS[idx] : enabledWords[idx - WORDS.length];
114
115
  return { text: `${entry.present_tense}…`, pastTense: entry.past_tense };
115
116
  }
116
117