@underactive/pi-topping 0.6.1 → 0.6.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 +13 -6
- package/package.json +3 -3
- package/src/menu.ts +57 -31
- package/src/session.ts +61 -2
- package/src/setup-command.ts +1 -1
- package/src/word-packs.ts +1 -1
- package/wordpacks/doctor-who.json +283 -0
- package/wordpacks/firefly.json +135 -0
- package/wordpacks/hitchhikers-guide.json +199 -0
- package/wordpacks/lord-of-the-rings.json +275 -0
- package/wordpacks/matrix.json +243 -0
- package/wordpacks/portal.json +171 -0
- package/wordpacks/star-trek.json +26 -26
- package/wordpacks/star-wars.json +49 -25
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
|

|
|
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, and live output-token rate (with color choice and dim toggle) — all arrangeable left to right.
|
|
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, and live output-token rate (with color choice and dim toggle) — 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
|

|
|
12
12
|
|
|
@@ -71,6 +71,12 @@ Run `/topping-settings` (TUI only) to customize your toppings. Settings persist
|
|
|
71
71
|
║ [■] Mid-turn inputs ON ║
|
|
72
72
|
║ ║
|
|
73
73
|
╟─ Word Packs ───────────────────────────────────────────────╢
|
|
74
|
+
║ [ ] Doctor Who OFF ║
|
|
75
|
+
║ [ ] Firefly OFF ║
|
|
76
|
+
║ [ ] Hitchhiker's Guide OFF ║
|
|
77
|
+
║ [ ] The Lord of the Rings OFF ║
|
|
78
|
+
║ [ ] The Matrix OFF ║
|
|
79
|
+
║ [ ] Portal OFF ║
|
|
74
80
|
║ [ ] SimCity OFF ║
|
|
75
81
|
║ [ ] Star Trek OFF ║
|
|
76
82
|
║ [ ] Star Wars OFF ║
|
|
@@ -78,15 +84,16 @@ Run `/topping-settings` (TUI only) to customize your toppings. Settings persist
|
|
|
78
84
|
╟─ Options ──────────────────────────────────────────────────╢
|
|
79
85
|
║ [■] Use NerdFont icons ON ║
|
|
80
86
|
╟────────────────────────────────────────────────────────────╢
|
|
81
|
-
║ ↑↓ move ←→ select
|
|
82
|
-
|
|
87
|
+
║ ↑↓ move PgUp/PgDn page ←→ select ║
|
|
88
|
+
║ ␣ toggle ⏎ apply esc cancel ║
|
|
89
|
+
╚════════════════════════════════════════════════════[ 9/47 ]╝
|
|
83
90
|
```
|
|
84
91
|
|
|
85
92
|
### Word packs
|
|
86
93
|
|
|
87
|
-
Base activity words are always available. All word packs, including the shipped **SimCity**, **Star Trek**, and **Star Wars** packs and custom packs, are disabled by default. Enable packs in `/topping-settings` to add their entries to the same uniformly selected pool. When Randomize “Worked” text is on, the completion marker uses the selected entry’s matching past tense; otherwise it uses `Worked`.
|
|
94
|
+
Base activity words are always available. All word packs, including the shipped **Doctor Who**, **Firefly**, **Hitchhiker's Guide**, **The Lord of the Rings**, **The Matrix**, **Portal**, **SimCity**, **Star Trek**, and **Star Wars** packs and custom packs, are disabled by default. Enable packs in `/topping-settings` to add their entries to the same uniformly selected pool. When Randomize “Worked” text is on, the completion marker uses the selected entry’s matching past tense; otherwise it uses `Worked`.
|
|
88
95
|
|
|
89
|
-
Bundled packs live in [`wordpacks/`](wordpacks/). Copy `wordpacks/simcity.json`, `wordpacks/star-trek.json`, or `wordpacks/star-wars.json` to `~/.pi/agent/pi-topping/word-packs.json`, then change the pack `id` (the bundled `simcity`, `star-trek`, and `star-wars` IDs are reserved), name, and words:
|
|
96
|
+
Bundled packs live in [`wordpacks/`](wordpacks/). Copy `wordpacks/doctor-who.json`, `wordpacks/firefly.json`, `wordpacks/hitchhikers-guide.json`, `wordpacks/lord-of-the-rings.json`, `wordpacks/matrix.json`, `wordpacks/portal.json`, `wordpacks/simcity.json`, `wordpacks/star-trek.json`, or `wordpacks/star-wars.json` to `~/.pi/agent/pi-topping/word-packs.json`, then change the pack `id` (the bundled `doctor-who`, `firefly`, `hitchhikers-guide`, `lord-of-the-rings`, `matrix`, `portal`, `simcity`, `star-trek`, and `star-wars` IDs are reserved), name, and words:
|
|
90
97
|
|
|
91
98
|
```json
|
|
92
99
|
{
|
|
@@ -122,7 +129,7 @@ Restart Pi (or run `/reload`) to pick it up.
|
|
|
122
129
|
|
|
123
130
|
## Requirements
|
|
124
131
|
|
|
125
|
-
- [Pi](https://github.com/earendil-works/pi-coding-agent)
|
|
132
|
+
- [Pi](https://github.com/earendil-works/pi-coding-agent). Pi ≥0.84.4 is required for the waiting-for-input loader state; older Pi versions keep the animated loader.
|
|
126
133
|
- Node.js ≥22.19.0
|
|
127
134
|
|
|
128
135
|
## Development
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@underactive/pi-topping",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
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",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"@earendil-works/pi-tui": "*"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
34
|
-
"@earendil-works/pi-tui": "0.
|
|
33
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
34
|
+
"@earendil-works/pi-tui": "0.84.4",
|
|
35
35
|
"@types/node": "^24.0.0",
|
|
36
36
|
"typescript": "^5.9.3"
|
|
37
37
|
},
|
package/src/menu.ts
CHANGED
|
@@ -5,27 +5,14 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Renders a titled box containing one or more sections of boolean toggle,
|
|
7
7
|
* multi-value cycle, or drag-to-reorder items, plus an optional live-updating
|
|
8
|
-
* preview section driven by a caller-supplied render callback
|
|
9
|
-
*
|
|
10
|
-
* ╔═[ Pi Topping: Settings ]════════════════════════╗
|
|
11
|
-
* ╟─ Preview ────────────────────────────────────╢
|
|
12
|
-
* ║ ║
|
|
13
|
-
* ║ ⠋ Cerebrating… ⣤⣤⣤⣤ 28 tok/s · 3s · ↓ 84 tokens ║
|
|
14
|
-
* ║ ║
|
|
15
|
-
* ╟─ Decorations ───────────────────────────────────╢
|
|
16
|
-
* ║ ❯ [■] Animated spinner ON ║
|
|
17
|
-
* ║ [ ] "Working..." text shimmer OFF ║
|
|
18
|
-
* ║ ║
|
|
19
|
-
* ╟──────────────────────────────────────────────────╢
|
|
20
|
-
* ║ ↑↓ move ␣ toggle ⏎ apply esc cancel ║
|
|
21
|
-
* ╚══════════════════════════════════════════[ 1/2 ]═╝
|
|
8
|
+
* preview section driven by a caller-supplied render callback.
|
|
22
9
|
*
|
|
23
10
|
* Intended to be reused by any extension that needs a simple modal toggle
|
|
24
11
|
* menu; it has no dependency on this extension's own settings shape.
|
|
25
12
|
*/
|
|
26
13
|
|
|
27
14
|
import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
28
|
-
import { type Component, Key, matchesKey, truncateToWidth, type TUI, visibleWidth } from "@earendil-works/pi-tui";
|
|
15
|
+
import { type Component, Key, matchesKey, truncateToWidth, type SizeValue, type TUI, visibleWidth } from "@earendil-works/pi-tui";
|
|
29
16
|
|
|
30
17
|
export type MenuValue = boolean | string;
|
|
31
18
|
|
|
@@ -79,6 +66,8 @@ export interface MenuConfig {
|
|
|
79
66
|
preview?: (values: Record<string, MenuValue>, elapsedMs: number, activeItemId: string | undefined, width: number) => string[] | PreviewResult;
|
|
80
67
|
/** Optional heading for the preview block. Defaults to `Preview`. */
|
|
81
68
|
previewTitle?: string;
|
|
69
|
+
/** Maximum overlay height in rows or as a percentage of the terminal height. */
|
|
70
|
+
maxHeight?: SizeValue;
|
|
82
71
|
}
|
|
83
72
|
|
|
84
73
|
export interface MenuResult<T> {
|
|
@@ -86,7 +75,7 @@ export interface MenuResult<T> {
|
|
|
86
75
|
values: T;
|
|
87
76
|
}
|
|
88
77
|
|
|
89
|
-
const DEFAULT_HINTS = ["\u2191\u2193 move", "\u2423 toggle", "\u23ce apply", "esc cancel"];
|
|
78
|
+
const DEFAULT_HINTS = ["\u2191\u2193 move", "PgUp/PgDn page", "\u2423 toggle", "\u23ce apply", "esc cancel"];
|
|
90
79
|
const OVERLAY_WIDTH = "86%";
|
|
91
80
|
export const DEFAULT_PREVIEW_WIDTH = 76;
|
|
92
81
|
const ROW_PREFIX_WIDTH = 8; // " " + marker + " " + "[" + box + "]" + " "
|
|
@@ -141,6 +130,7 @@ export class MenuComponent implements Component {
|
|
|
141
130
|
private readonly previewFn: MenuConfig["preview"];
|
|
142
131
|
private readonly previewOrigin: number | undefined;
|
|
143
132
|
private readonly tui: TUI | undefined;
|
|
133
|
+
private readonly maxHeight: SizeValue | undefined;
|
|
144
134
|
private previewTimer: ReturnType<typeof setTimeout> | undefined;
|
|
145
135
|
private previewNextRefreshMs: number | undefined;
|
|
146
136
|
private disposed = false;
|
|
@@ -149,6 +139,7 @@ export class MenuComponent implements Component {
|
|
|
149
139
|
private cachedWidth: number | undefined;
|
|
150
140
|
private cachedRows: number | undefined;
|
|
151
141
|
private cachedLines: string[] | undefined;
|
|
142
|
+
private pageItemCount: number | undefined;
|
|
152
143
|
|
|
153
144
|
constructor(
|
|
154
145
|
config: MenuConfig,
|
|
@@ -163,6 +154,7 @@ export class MenuComponent implements Component {
|
|
|
163
154
|
this.hints = config.hints ?? DEFAULT_HINTS;
|
|
164
155
|
this.previewTitle = config.previewTitle ?? "Preview";
|
|
165
156
|
this.tui = tui;
|
|
157
|
+
this.maxHeight = config.maxHeight;
|
|
166
158
|
this.values = buildInitialValues(config);
|
|
167
159
|
this.flat = [];
|
|
168
160
|
for (const [sectionIndex, section] of config.sections.entries()) {
|
|
@@ -203,7 +195,7 @@ export class MenuComponent implements Component {
|
|
|
203
195
|
|
|
204
196
|
// Map input to a normalized key name.
|
|
205
197
|
let mappedKey: string | undefined;
|
|
206
|
-
for (const k of [Key.up, Key.down, Key.left, Key.right, Key.space, Key.enter, Key.escape]) {
|
|
198
|
+
for (const k of [Key.up, Key.down, Key.pageUp, Key.pageDown, Key.left, Key.right, Key.space, Key.enter, Key.escape]) {
|
|
207
199
|
if (matchesKey(data, k)) {
|
|
208
200
|
mappedKey = k;
|
|
209
201
|
break;
|
|
@@ -214,14 +206,16 @@ export class MenuComponent implements Component {
|
|
|
214
206
|
const keyActions: Record<string, () => void> = {
|
|
215
207
|
[Key.up]: () => {
|
|
216
208
|
if (this.moveGrabbedItem(-1)) return;
|
|
217
|
-
this.cursor = (this.cursor - 1
|
|
209
|
+
this.cursor = Math.max(0, this.cursor - 1);
|
|
218
210
|
this.invalidate();
|
|
219
211
|
},
|
|
220
212
|
[Key.down]: () => {
|
|
221
213
|
if (this.moveGrabbedItem(1)) return;
|
|
222
|
-
this.cursor = (this.cursor + 1)
|
|
214
|
+
this.cursor = Math.min(this.flat.length - 1, this.cursor + 1);
|
|
223
215
|
this.invalidate();
|
|
224
216
|
},
|
|
217
|
+
[Key.pageUp]: () => this.moveByPage(-1),
|
|
218
|
+
[Key.pageDown]: () => this.moveByPage(1),
|
|
225
219
|
[Key.left]: () => this.cycleCurrentValue(-1),
|
|
226
220
|
[Key.right]: () => this.cycleCurrentValue(1),
|
|
227
221
|
[Key.space]: () => {
|
|
@@ -258,9 +252,9 @@ export class MenuComponent implements Component {
|
|
|
258
252
|
}
|
|
259
253
|
|
|
260
254
|
/**
|
|
261
|
-
* Move the grabbed reorder row
|
|
262
|
-
*
|
|
263
|
-
*
|
|
255
|
+
* Move the grabbed reorder row within its group, clamped at the group edges.
|
|
256
|
+
* Returns true when the key belongs to the mover and must not also move the
|
|
257
|
+
* cursor — which is what keeps a grab from escaping its group.
|
|
264
258
|
*/
|
|
265
259
|
private moveGrabbedItem(delta: number): boolean {
|
|
266
260
|
const grabbed = this.flat[this.cursor]!;
|
|
@@ -272,10 +266,10 @@ export class MenuComponent implements Component {
|
|
|
272
266
|
let end = this.cursor;
|
|
273
267
|
while (end < this.flat.length - 1 && this.flat[end + 1]!.reorderGroup === group) end++;
|
|
274
268
|
|
|
275
|
-
const target = this.cursor + delta;
|
|
276
|
-
if (target
|
|
277
|
-
this.flat
|
|
278
|
-
this.flat
|
|
269
|
+
const target = Math.max(start, Math.min(end, this.cursor + delta));
|
|
270
|
+
if (target !== this.cursor) {
|
|
271
|
+
this.flat.splice(this.cursor, 1);
|
|
272
|
+
this.flat.splice(target, 0, grabbed);
|
|
279
273
|
this.cursor = target;
|
|
280
274
|
this.values[group] = this.flat.slice(start, end + 1).map(flat => flat.id).join(",");
|
|
281
275
|
this.invalidate();
|
|
@@ -283,6 +277,20 @@ export class MenuComponent implements Component {
|
|
|
283
277
|
return true;
|
|
284
278
|
}
|
|
285
279
|
|
|
280
|
+
private moveByPage(direction: -1 | 1): void {
|
|
281
|
+
const pageItemCount = this.getPageItemCount();
|
|
282
|
+
if (this.moveGrabbedItem(direction * pageItemCount)) return;
|
|
283
|
+
this.cursor = Math.max(0, Math.min(this.flat.length - 1, this.cursor + direction * pageItemCount));
|
|
284
|
+
this.invalidate();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private getPageItemCount(): number {
|
|
288
|
+
if (this.pageItemCount !== undefined) return this.pageItemCount;
|
|
289
|
+
const rows = this.availableRows();
|
|
290
|
+
if (rows === undefined) return Math.max(1, Math.min(this.flat.length, 10));
|
|
291
|
+
return Math.max(1, rows - 6);
|
|
292
|
+
}
|
|
293
|
+
|
|
286
294
|
private cycleCurrentValue(delta: number): void {
|
|
287
295
|
const item = this.flat[this.cursor]!;
|
|
288
296
|
if (!item.cycleValues?.length || (item.cycleEnabledBy && !this.values[item.cycleEnabledBy])) return;
|
|
@@ -377,10 +385,17 @@ export class MenuComponent implements Component {
|
|
|
377
385
|
private buildResponsiveToggleSections(innerWidth: number, maxRows: number, allLines: string[]): string[] {
|
|
378
386
|
if (allLines.length <= maxRows) {
|
|
379
387
|
this.scrollStart = 0;
|
|
388
|
+
this.pageItemCount = Math.max(1, this.flat.length);
|
|
380
389
|
return allLines;
|
|
381
390
|
}
|
|
382
|
-
if (maxRows <= 0)
|
|
383
|
-
|
|
391
|
+
if (maxRows <= 0) {
|
|
392
|
+
this.pageItemCount = 1;
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
if (maxRows === 1) {
|
|
396
|
+
this.pageItemCount = 1;
|
|
397
|
+
return this.buildToggleWindow(innerWidth, this.cursor, 1).lines;
|
|
398
|
+
}
|
|
384
399
|
|
|
385
400
|
const contentRows = maxRows - 1; // Reserve one fixed row for scroll status.
|
|
386
401
|
if (this.cursor < this.scrollStart) this.scrollStart = this.cursor;
|
|
@@ -405,6 +420,7 @@ export class MenuComponent implements Component {
|
|
|
405
420
|
}
|
|
406
421
|
}
|
|
407
422
|
const window = this.buildToggleWindow(innerWidth, this.scrollStart, contentRows);
|
|
423
|
+
this.pageItemCount = Math.max(1, window.end - this.scrollStart + 1);
|
|
408
424
|
|
|
409
425
|
const rows = [...window.lines];
|
|
410
426
|
while (rows.length < contentRows) rows.push(this.renderBlankRow(innerWidth));
|
|
@@ -417,8 +433,16 @@ export class MenuComponent implements Component {
|
|
|
417
433
|
}
|
|
418
434
|
|
|
419
435
|
private availableRows(): number | undefined {
|
|
420
|
-
const rows =
|
|
421
|
-
|
|
436
|
+
const rows = this.tui?.terminal?.rows;
|
|
437
|
+
if (typeof rows !== "number" || !Number.isFinite(rows) || rows <= 0) return undefined;
|
|
438
|
+
|
|
439
|
+
const terminalRows = Math.floor(rows);
|
|
440
|
+
if (this.maxHeight === undefined) return terminalRows;
|
|
441
|
+
const requestedRows = typeof this.maxHeight === "number"
|
|
442
|
+
? this.maxHeight
|
|
443
|
+
: (Number.parseFloat(this.maxHeight) / 100) * terminalRows;
|
|
444
|
+
if (!Number.isFinite(requestedRows)) return terminalRows;
|
|
445
|
+
return Math.max(1, Math.min(terminalRows, Math.floor(requestedRows)));
|
|
422
446
|
}
|
|
423
447
|
|
|
424
448
|
private buildLines(maxWidth: number, maxRows?: number): string[] {
|
|
@@ -431,6 +455,7 @@ export class MenuComponent implements Component {
|
|
|
431
455
|
const naturalBody = this.buildToggleSections(innerWidth);
|
|
432
456
|
const naturalHeight = header.length + naturalBody.length + footer.length;
|
|
433
457
|
if (maxRows === undefined || naturalHeight <= maxRows) {
|
|
458
|
+
this.pageItemCount = Math.max(1, this.flat.length);
|
|
434
459
|
return [...header, ...naturalBody, ...footer].map(line => truncateToWidth(line, boxWidth, ""));
|
|
435
460
|
}
|
|
436
461
|
|
|
@@ -571,9 +596,10 @@ export async function showMenu<T extends Record<string, MenuValue>>(
|
|
|
571
596
|
return { applied: false, values: initialValues };
|
|
572
597
|
}
|
|
573
598
|
|
|
599
|
+
const maxHeight = config.maxHeight ?? "100%";
|
|
574
600
|
return ctx.ui.custom<MenuResult<T>>(
|
|
575
601
|
(tui, theme, _keybindings, done) =>
|
|
576
602
|
new MenuComponent(config, theme, done as (result: MenuResult<Record<string, MenuValue>>) => void, tui),
|
|
577
|
-
{ overlay: true, overlayOptions: { width: OVERLAY_WIDTH, maxHeight
|
|
603
|
+
{ overlay: true, overlayOptions: { width: OVERLAY_WIDTH, maxHeight } },
|
|
578
604
|
);
|
|
579
605
|
}
|
package/src/session.ts
CHANGED
|
@@ -47,8 +47,20 @@ type MessageStartEvent = Extract<ExtensionEvent, { type: "message_start" }>;
|
|
|
47
47
|
type MessageUpdateEvent = Extract<ExtensionEvent, { type: "message_update" }>;
|
|
48
48
|
type MessageEndEvent = Extract<ExtensionEvent, { type: "message_end" }>;
|
|
49
49
|
type ToolExecutionStartEvent = Extract<ExtensionEvent, { type: "tool_execution_start" }>;
|
|
50
|
+
type UIPromptStartEvent = Extract<ExtensionEvent, { type: "ui_prompt_start" }>;
|
|
51
|
+
type UIPromptEndEvent = Extract<ExtensionEvent, { type: "ui_prompt_end" }>;
|
|
52
|
+
type UIPromptKind = UIPromptStartEvent["kind"];
|
|
50
53
|
|
|
51
54
|
const DONE_ENTRY_TYPE = "pi-topping-done";
|
|
55
|
+
const WAITING_LABELS: Record<UIPromptKind, string> = {
|
|
56
|
+
select: "Waiting for selection",
|
|
57
|
+
confirm: "Waiting for confirmation",
|
|
58
|
+
input: "Waiting for input",
|
|
59
|
+
editor: "Waiting in editor",
|
|
60
|
+
custom: "Waiting for input",
|
|
61
|
+
};
|
|
62
|
+
const WAITING_PULSE_FRAMES = ["·", "•", "●", "•"];
|
|
63
|
+
const WAITING_PULSE_INTERVAL_MS = 120;
|
|
52
64
|
// Reconciliation resets the EMA at message boundaries, so retain and then fade the last rate to avoid flicker.
|
|
53
65
|
const TOKEN_RATE_HOLD_MS = 1_500;
|
|
54
66
|
const TOKEN_RATE_FADE_MS = 250;
|
|
@@ -74,6 +86,7 @@ interface SessionState {
|
|
|
74
86
|
tokenRateFadeStartsAt: number;
|
|
75
87
|
timer: ReturnType<typeof setInterval> | null;
|
|
76
88
|
busy: boolean;
|
|
89
|
+
waiting: { kind: UIPromptKind; title?: string } | null;
|
|
77
90
|
midTurnInputs: number;
|
|
78
91
|
/** Last string passed to setWorkingMessage(), or the NOT_SENT sentinel before the first call. */
|
|
79
92
|
lastMessage: string | undefined | typeof NOT_SENT;
|
|
@@ -106,11 +119,17 @@ function makeFreshState(): SessionState {
|
|
|
106
119
|
tokenRateFadeStartsAt: 0,
|
|
107
120
|
timer: null,
|
|
108
121
|
busy: false,
|
|
122
|
+
waiting: null,
|
|
109
123
|
midTurnInputs: 0,
|
|
110
124
|
lastMessage: NOT_SENT,
|
|
111
125
|
};
|
|
112
126
|
}
|
|
113
127
|
|
|
128
|
+
function waitingLabel(waiting: NonNullable<SessionState["waiting"]>): string {
|
|
129
|
+
const title = waiting.title ? stripControlChars(waiting.title).trim() : "";
|
|
130
|
+
return title ? `Waiting: ${title}` : WAITING_LABELS[waiting.kind];
|
|
131
|
+
}
|
|
132
|
+
|
|
114
133
|
/** Owns mutable extension state and Pi lifecycle registrations. */
|
|
115
134
|
export class SessionManager {
|
|
116
135
|
#counter = new StreamingWordCounter();
|
|
@@ -225,6 +244,23 @@ export class SessionManager {
|
|
|
225
244
|
}
|
|
226
245
|
};
|
|
227
246
|
|
|
247
|
+
#onUIPromptStart = (event: UIPromptStartEvent, ctx: ExtensionContext): void => {
|
|
248
|
+
this.#currentCtx = ctx;
|
|
249
|
+
if (!this.usable(ctx) || !this.#state.busy) return;
|
|
250
|
+
this.#state.waiting = { kind: event.kind, title: event.title };
|
|
251
|
+
this.applyWaitingIndicator(ctx);
|
|
252
|
+
this.tick();
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
#onUIPromptEnd = (_event: UIPromptEndEvent, ctx: ExtensionContext): void => {
|
|
256
|
+
this.#currentCtx = ctx;
|
|
257
|
+
if (!this.usable(ctx) || !this.#state.waiting) return;
|
|
258
|
+
this.#state.waiting = null;
|
|
259
|
+
if (!this.#state.busy) return;
|
|
260
|
+
this.applyIndicator(ctx);
|
|
261
|
+
this.tick();
|
|
262
|
+
};
|
|
263
|
+
|
|
228
264
|
#onAgentSettled = async (_e: AgentSettledEvent, ctx: ExtensionContext): Promise<void> => {
|
|
229
265
|
this.#currentCtx = ctx;
|
|
230
266
|
if (!this.usable(ctx)) return;
|
|
@@ -232,6 +268,7 @@ export class SessionManager {
|
|
|
232
268
|
const hadPrompt = !!this.#state.startTime;
|
|
233
269
|
const elapsedMs = hadPrompt ? Date.now() - this.#state.startTime : 0;
|
|
234
270
|
this.#state.busy = false;
|
|
271
|
+
this.#state.waiting = null;
|
|
235
272
|
this.#state.startTime = 0;
|
|
236
273
|
this.#counter.reset();
|
|
237
274
|
this.stopTimer();
|
|
@@ -252,6 +289,7 @@ export class SessionManager {
|
|
|
252
289
|
};
|
|
253
290
|
|
|
254
291
|
#onSessionShutdown = async (_e: SessionShutdownEvent, _ctx: ExtensionContext): Promise<void> => {
|
|
292
|
+
this.#state.waiting = null;
|
|
255
293
|
this.stopTimer();
|
|
256
294
|
};
|
|
257
295
|
|
|
@@ -298,6 +336,8 @@ export class SessionManager {
|
|
|
298
336
|
this.#pi.on("message_end", this.#onMessageEnd);
|
|
299
337
|
this.#pi.on("tool_execution_start", this.#onToolExecutionStart);
|
|
300
338
|
this.#pi.on("agent_settled", this.#onAgentSettled);
|
|
339
|
+
this.#pi.on("ui_prompt_start", this.#onUIPromptStart);
|
|
340
|
+
this.#pi.on("ui_prompt_end", this.#onUIPromptEnd);
|
|
301
341
|
this.#pi.on("session_shutdown", this.#onSessionShutdown);
|
|
302
342
|
this.#pi.registerEntryRenderer<DoneEntryData>(DONE_ENTRY_TYPE, (entry, _o, theme) => this.#renderDoneEntry(entry, theme));
|
|
303
343
|
this.#pi.registerMessageRenderer<PromptBoxDetails>(PROMPT_BOX_TYPE, promptBoxRenderer);
|
|
@@ -345,6 +385,13 @@ export class SessionManager {
|
|
|
345
385
|
return `${decorations.animatedSpinner}:${decorations.spinnerColor}:${decorations.spinnerColorEnabled}:${this.#settings.loaderOrder[0]}`;
|
|
346
386
|
}
|
|
347
387
|
|
|
388
|
+
private applyWaitingIndicator(ctx: ExtensionContext): void {
|
|
389
|
+
ctx.ui.setWorkingIndicator({
|
|
390
|
+
frames: WAITING_PULSE_FRAMES.map((frame) => ctx.ui.theme.fg("dim", frame)),
|
|
391
|
+
intervalMs: WAITING_PULSE_INTERVAL_MS,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
348
395
|
private applyIndicator(ctx: ExtensionContext): void {
|
|
349
396
|
if (!this.#settings.decorations.animatedSpinner || this.spinnerInMessage()) {
|
|
350
397
|
ctx.ui.setWorkingIndicator({ frames: [] });
|
|
@@ -401,6 +448,14 @@ export class SessionManager {
|
|
|
401
448
|
const ctx = this.#currentCtx;
|
|
402
449
|
const state = this.#state;
|
|
403
450
|
if (!state.busy || !ctx) return;
|
|
451
|
+
if (state.waiting) {
|
|
452
|
+
const msg = ctx.ui.theme.fg("dim", waitingLabel(state.waiting));
|
|
453
|
+
if (msg !== state.lastMessage) {
|
|
454
|
+
state.lastMessage = msg;
|
|
455
|
+
ctx.ui.setWorkingMessage(msg);
|
|
456
|
+
}
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
404
459
|
|
|
405
460
|
const now = Date.now();
|
|
406
461
|
const total = state.confirmTokens + state.liveTokens;
|
|
@@ -497,8 +552,9 @@ export class SessionManager {
|
|
|
497
552
|
const preview = new PreviewRenderer(ctx, [...this.#bundledPacks, ...this.#userPacks]);
|
|
498
553
|
const result = await showMenu<Record<string, boolean | string>>(ctx, {
|
|
499
554
|
title: "Pi Topping: Settings",
|
|
555
|
+
maxHeight: "75%",
|
|
500
556
|
sections: buildMenuSections(this.#settings, this.#bundledPacks, this.#userPacks),
|
|
501
|
-
hints: ["↑↓ move", "←→ select", "␣ toggle", "⏎ apply", "esc cancel"],
|
|
557
|
+
hints: ["↑↓ move", "PgUp/PgDn page", "←→ select", "␣ toggle", "⏎ apply", "esc cancel"],
|
|
502
558
|
preview: preview.render.bind(preview),
|
|
503
559
|
});
|
|
504
560
|
if (!result.applied) return;
|
|
@@ -512,7 +568,10 @@ export class SessionManager {
|
|
|
512
568
|
}
|
|
513
569
|
|
|
514
570
|
this.#settings = updatedSettings;
|
|
515
|
-
if (before !== this.indicatorFingerprint())
|
|
571
|
+
if (before !== this.indicatorFingerprint()) {
|
|
572
|
+
if (this.#state.waiting) this.applyWaitingIndicator(ctx);
|
|
573
|
+
else this.applyIndicator(ctx);
|
|
574
|
+
}
|
|
516
575
|
this.#state.activityMeter.setDirection(this.#settings.decorations.meterDirection);
|
|
517
576
|
if (this.#state.busy) {
|
|
518
577
|
this.stopTimer();
|
package/src/setup-command.ts
CHANGED
|
@@ -114,7 +114,7 @@ export function registerSetupCommand(pi: ExtensionAPI, install: ToppingInstaller
|
|
|
114
114
|
})),
|
|
115
115
|
},
|
|
116
116
|
],
|
|
117
|
-
hints: ["↑↓ move", "␣ toggle", "⏎ install", "esc cancel"],
|
|
117
|
+
hints: ["↑↓ move", "PgUp/PgDn page", "␣ toggle", "⏎ install", "esc cancel"],
|
|
118
118
|
previewTitle: "Description",
|
|
119
119
|
preview: (_values, _elapsedMs, activeItemId) => ({ lines: renderDescription(missing, activeItemId) }),
|
|
120
120
|
});
|
package/src/word-packs.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { isPlainObject, stripControlChars } from "./util.ts";
|
|
|
6
6
|
import { WORDS, type WordEntry } from "./words.ts";
|
|
7
7
|
|
|
8
8
|
const PACK_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
9
|
-
const RESERVED_BUNDLED_PACK_IDS = new Set(["simcity", "star-trek", "star-wars"]);
|
|
9
|
+
const RESERVED_BUNDLED_PACK_IDS = new Set(["doctor-who", "firefly", "hitchhikers-guide", "lord-of-the-rings", "matrix", "portal", "simcity", "star-trek", "star-wars"]);
|
|
10
10
|
|
|
11
11
|
export function isWordPackId(value: string): boolean { return PACK_ID_PATTERN.test(value); }
|
|
12
12
|
const BUNDLED_PACK_DIRECTORY = fileURLToPath(new URL("../wordpacks/", import.meta.url));
|