@heyhuynhgiabuu/pi-pretty 0.6.25 → 0.6.27
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/CHANGELOG.md +34 -0
- package/README.md +47 -17
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +30 -0
- package/dist/config.js.map +1 -1
- package/dist/fff.d.ts +16 -1
- package/dist/fff.d.ts.map +1 -1
- package/dist/fff.js +139 -33
- package/dist/fff.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +192 -43
- package/dist/index.js.map +1 -1
- package/dist/prompt-editor.d.ts +11 -0
- package/dist/prompt-editor.d.ts.map +1 -0
- package/dist/prompt-editor.js +78 -0
- package/dist/prompt-editor.js.map +1 -0
- package/dist/render.d.ts +9 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +25 -9
- package/dist/render.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +73 -30
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +29 -10
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +20 -10
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +10 -5
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/metrics.d.ts +6 -1
- package/dist/tools/metrics.d.ts.map +1 -1
- package/dist/tools/metrics.js +21 -8
- package/dist/tools/metrics.js.map +1 -1
- package/dist/tools/read.d.ts +2 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +93 -30
- package/dist/tools/read.js.map +1 -1
- package/dist/types.d.ts +13 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/working-indicator.d.ts +91 -26
- package/dist/working-indicator.d.ts.map +1 -1
- package/dist/working-indicator.js +259 -77
- package/dist/working-indicator.js.map +1 -1
- package/package.json +7 -6
- package/src/config.ts +51 -0
- package/src/fff.ts +158 -29
- package/src/index.ts +207 -47
- package/src/prompt-editor.ts +102 -0
- package/src/render.ts +30 -11
- package/src/tools/bash.ts +94 -32
- package/src/tools/find.ts +39 -17
- package/src/tools/grep.ts +26 -15
- package/src/tools/ls.ts +19 -10
- package/src/tools/metrics.ts +28 -8
- package/src/tools/read.ts +122 -30
- package/src/types.ts +14 -3
- package/src/working-indicator.ts +309 -88
package/src/working-indicator.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* omp-style shimmer working indicator
|
|
2
|
+
* omp-style shimmer working indicator.
|
|
3
3
|
*
|
|
4
4
|
* Ports the shimmer sweep from oh-my-pi's `modes/theme/shimmer.ts`: the sweep
|
|
5
5
|
* is discretized into one pre-rendered frame per band position, cycled at
|
|
6
6
|
* 1000/30 ms so the band travels 30 cells/second — omp's exact speed.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* (`setWorkingVisible(false)`) and install our own zero-padding component via
|
|
12
|
-
* `setWidget(..., "aboveEditor")`, animating it with a 33ms interval.
|
|
8
|
+
* On hosts that expose Pi's embedded working-status API, frames are delegated
|
|
9
|
+
* to the host so the row is rendered in the editor's top border. Older hosts
|
|
10
|
+
* without that API use the zero-padding widget fallback above the editor.
|
|
13
11
|
*
|
|
14
|
-
* Frame layout: `<dim spinner> <shimmer text
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* Frame layout: `<dim spinner> <shimmer text>`, one full sweep per phrase —
|
|
13
|
+
* `texts` rotates through phrases chapter by chapter with a continuous spinner
|
|
14
|
+
* phase. The extension adds no interrupt hint or token-count suffix.
|
|
17
15
|
*/
|
|
18
16
|
|
|
19
17
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
@@ -61,6 +59,7 @@ export interface WorkingIndicatorSettings {
|
|
|
61
59
|
mode: WorkingIndicatorMode;
|
|
62
60
|
palette: WorkingIndicatorPalette;
|
|
63
61
|
bold: boolean;
|
|
62
|
+
/** @deprecated Retained for config compatibility; no interrupt hint is rendered. */
|
|
64
63
|
hint: boolean;
|
|
65
64
|
/** Tint mid/high tiers (and dim the spinner) with a per-session accent color. */
|
|
66
65
|
sessionAccent: boolean;
|
|
@@ -261,7 +260,7 @@ export interface WorkingFrameOptions {
|
|
|
261
260
|
spinnerColor?: string;
|
|
262
261
|
/** Wrap every frame in italic (pi's hidden-thinking label is italic). */
|
|
263
262
|
italic?: boolean;
|
|
264
|
-
/**
|
|
263
|
+
/** @deprecated Retained for call-site compatibility; suffixes are not rendered. */
|
|
265
264
|
hint?: string;
|
|
266
265
|
intervalMs?: number;
|
|
267
266
|
}
|
|
@@ -277,7 +276,6 @@ export function buildWorkingFrames(
|
|
|
277
276
|
): { frames: string[]; intervalMs: number } {
|
|
278
277
|
const intervalMs = options.intervalMs ?? WORKING_INTERVAL_MS;
|
|
279
278
|
const bold = options.bold ?? true;
|
|
280
|
-
const suffix = options.hint ?? "";
|
|
281
279
|
const withSpinner = options.spinner ?? true;
|
|
282
280
|
const spinnerColor = options.spinnerColor ?? options.ansi.low;
|
|
283
281
|
const highOpen = bold ? `${BOLD_OPEN}${options.ansi.high}` : options.ansi.high;
|
|
@@ -331,7 +329,7 @@ export function buildWorkingFrames(
|
|
|
331
329
|
text,
|
|
332
330
|
ranges,
|
|
333
331
|
Array.from({ length: total }, () => "mid" as Tier),
|
|
334
|
-
)}
|
|
332
|
+
)}`,
|
|
335
333
|
);
|
|
336
334
|
return { frames, intervalMs };
|
|
337
335
|
}
|
|
@@ -344,7 +342,7 @@ export function buildWorkingFrames(
|
|
|
344
342
|
text,
|
|
345
343
|
ranges,
|
|
346
344
|
Array.from({ length: total }, () => "high" as Tier),
|
|
347
|
-
)}
|
|
345
|
+
)}`,
|
|
348
346
|
);
|
|
349
347
|
continue;
|
|
350
348
|
}
|
|
@@ -353,19 +351,35 @@ export function buildWorkingFrames(
|
|
|
353
351
|
const goingRight = sweep < range;
|
|
354
352
|
const head = goingRight ? sweep : cycleCells - sweep;
|
|
355
353
|
const tiers = Array.from({ length: total }, (_, i) => tierFor(kittIntensity(i, head, goingRight)));
|
|
356
|
-
frames.push(`${spinnerPrefix(spinnerIndex++)}${paintFrame(text, ranges, tiers)}
|
|
354
|
+
frames.push(`${spinnerPrefix(spinnerIndex++)}${paintFrame(text, ranges, tiers)}`);
|
|
357
355
|
}
|
|
358
356
|
} else {
|
|
359
357
|
const period = total + CLASSIC_PADDING * 2;
|
|
360
358
|
for (let pos = 0; pos < period; pos++) {
|
|
361
359
|
const tiers = Array.from({ length: total }, (_, i) => tierFor(classicIntensity(i, pos)));
|
|
362
|
-
frames.push(`${spinnerPrefix(spinnerIndex++)}${paintFrame(text, ranges, tiers)}
|
|
360
|
+
frames.push(`${spinnerPrefix(spinnerIndex++)}${paintFrame(text, ranges, tiers)}`);
|
|
363
361
|
}
|
|
364
362
|
}
|
|
365
363
|
}
|
|
366
364
|
return { frames, intervalMs };
|
|
367
365
|
}
|
|
368
366
|
|
|
367
|
+
/** @deprecated The working row no longer displays token stats. */
|
|
368
|
+
export function workingTokens(message: unknown): number {
|
|
369
|
+
const usage = (message as { usage?: { output?: unknown } } | undefined)?.usage;
|
|
370
|
+
const output = Number(usage?.output);
|
|
371
|
+
if (Number.isFinite(output) && output > 0) return Math.round(output);
|
|
372
|
+
const content = (message as { content?: Array<{ type?: string; text?: unknown; thinking?: unknown }> } | undefined)
|
|
373
|
+
?.content;
|
|
374
|
+
if (!Array.isArray(content)) return 0;
|
|
375
|
+
let chars = 0;
|
|
376
|
+
for (const block of content) {
|
|
377
|
+
if (block?.type === "text" && typeof block.text === "string") chars += block.text.length;
|
|
378
|
+
else if (block?.type === "thinking" && typeof block.thinking === "string") chars += block.thinking.length;
|
|
379
|
+
}
|
|
380
|
+
return Math.round(chars / 4);
|
|
381
|
+
}
|
|
382
|
+
|
|
369
383
|
// ─── Widget ──────────────────────────────────────────────────────────────────
|
|
370
384
|
|
|
371
385
|
/** Slice of the pi-tui TUI instance the widget needs. */
|
|
@@ -375,8 +389,8 @@ export interface WidgetTuiLike {
|
|
|
375
389
|
|
|
376
390
|
/**
|
|
377
391
|
* Zero-padding working-row component. The host renders whatever `render()`
|
|
378
|
-
* returns with no margins, so the row sits flush-left
|
|
379
|
-
*
|
|
392
|
+
* returns with no margins, so the legacy widget row sits flush-left without
|
|
393
|
+
* relying on the host loader's padding.
|
|
380
394
|
*/
|
|
381
395
|
export class WorkingWidget {
|
|
382
396
|
#frames: string[] = [];
|
|
@@ -397,6 +411,16 @@ export class WorkingWidget {
|
|
|
397
411
|
this.#tui = tui;
|
|
398
412
|
}
|
|
399
413
|
|
|
414
|
+
/** Ask the host for a render — lets non-widget consumers (the thinking label
|
|
415
|
+
* ticker) reuse this widget's TUI handle instead of relying on natural
|
|
416
|
+
* streaming renders. */
|
|
417
|
+
requestRender(): void {
|
|
418
|
+
this.#tui?.requestRender();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** @deprecated Token stats are owned by Pi and are no longer rendered here. */
|
|
422
|
+
setStats(_text: string | undefined): void {}
|
|
423
|
+
|
|
400
424
|
start(): void {
|
|
401
425
|
if (this.#disposed || this.#started) return;
|
|
402
426
|
this.#started = true;
|
|
@@ -462,16 +486,161 @@ export interface ThinkingUiLike {
|
|
|
462
486
|
setHiddenThinkingLabel(label?: string): void;
|
|
463
487
|
}
|
|
464
488
|
|
|
489
|
+
// ─── Per-row hidden-thinking labels ──────────────────────────────────────────
|
|
490
|
+
|
|
491
|
+
/** Controller for the host-component label interception. */
|
|
492
|
+
export interface PerRowThinkingLabels {
|
|
493
|
+
/** Mark the message currently streaming a thinking block (its row animates). */
|
|
494
|
+
setActive(timestamp: number): void;
|
|
495
|
+
/** Freeze a message's completed thinking duration in ms (its row shows
|
|
496
|
+
* `Thought for X` derived from this). */
|
|
497
|
+
complete(timestamp: number, durationMs: number): void;
|
|
498
|
+
/** Drop the active mark (run ended); completed rows keep their durations. */
|
|
499
|
+
clearActive(): void;
|
|
500
|
+
/** Restore the original prototype setter. */
|
|
501
|
+
uninstall(): void;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const PER_ROW_PATCH = Symbol.for("pi-pretty.per-row-thinking-labels");
|
|
505
|
+
|
|
506
|
+
interface RowInternals {
|
|
507
|
+
lastMessage?: { timestamp?: unknown };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Intercept the host's per-row label fan-out method
|
|
512
|
+
* (`AssistantMessageComponent.prototype.setHiddenThinkingLabel`): each row
|
|
513
|
+
* substitutes the incoming global label with its own — the streaming row
|
|
514
|
+
* animates, completed rows stay frozen at their own `Thought for X`, unknown
|
|
515
|
+
* rows fall back to pi's default. Any per-call failure passes the incoming
|
|
516
|
+
* label through unchanged, so a host internals change degrades to pi-pretty's
|
|
517
|
+
* global-label behavior instead of breaking. Durations live per session.
|
|
518
|
+
*/
|
|
519
|
+
export function installPerRowThinkingLabels(componentClass: unknown): PerRowThinkingLabels | undefined {
|
|
520
|
+
const proto = (componentClass as { prototype?: { setHiddenThinkingLabel?: unknown } } | undefined)?.prototype;
|
|
521
|
+
const original = proto?.setHiddenThinkingLabel;
|
|
522
|
+
if (typeof componentClass !== "function" || !proto || typeof original !== "function") return undefined;
|
|
523
|
+
const existing = (original as { [PER_ROW_PATCH]?: PerRowThinkingLabels })[PER_ROW_PATCH];
|
|
524
|
+
// Reuse the installed patch but start from clean state: a session_start
|
|
525
|
+
// without an intervening shutdown (defensive) must not resurrect stale rows.
|
|
526
|
+
if (existing) {
|
|
527
|
+
existing.clearActive();
|
|
528
|
+
return existing;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const state: { activeTs?: number; completed: Map<number, number> } = { completed: new Map() };
|
|
532
|
+
const patched = function (this: unknown, label: string | undefined): void {
|
|
533
|
+
const own = ownLabel(this, label);
|
|
534
|
+
(original as (this: unknown, label: string | undefined) => void).call(this, own);
|
|
535
|
+
};
|
|
536
|
+
const ownLabel = (component: unknown, incoming: string | undefined): string | undefined => {
|
|
537
|
+
try {
|
|
538
|
+
const ts = (component as RowInternals | undefined)?.lastMessage?.timestamp;
|
|
539
|
+
if (typeof ts !== "number") return incoming;
|
|
540
|
+
if (ts === state.activeTs) return incoming;
|
|
541
|
+
const ms = state.completed.get(ts);
|
|
542
|
+
return ms === undefined ? THINKING_LABEL : `${THOUGHT_LABEL} ${formatThinkingDuration(ms)}`;
|
|
543
|
+
} catch {
|
|
544
|
+
return incoming;
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
const controller: PerRowThinkingLabels = {
|
|
548
|
+
setActive(timestamp: number): void {
|
|
549
|
+
state.activeTs = timestamp;
|
|
550
|
+
},
|
|
551
|
+
complete(timestamp: number, durationMs: number): void {
|
|
552
|
+
state.completed.set(timestamp, durationMs);
|
|
553
|
+
},
|
|
554
|
+
clearActive(): void {
|
|
555
|
+
state.activeTs = undefined;
|
|
556
|
+
},
|
|
557
|
+
uninstall(): void {
|
|
558
|
+
if (proto.setHiddenThinkingLabel === patched) {
|
|
559
|
+
proto.setHiddenThinkingLabel = original as { setHiddenThinkingLabel?: unknown }["setHiddenThinkingLabel"];
|
|
560
|
+
}
|
|
561
|
+
state.completed.clear();
|
|
562
|
+
state.activeTs = undefined;
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
(patched as { [PER_ROW_PATCH]?: PerRowThinkingLabels })[PER_ROW_PATCH] = controller;
|
|
566
|
+
proto.setHiddenThinkingLabel = patched as NonNullable<{ setHiddenThinkingLabel?: unknown }["setHiddenThinkingLabel"]>;
|
|
567
|
+
return controller;
|
|
568
|
+
}
|
|
569
|
+
|
|
465
570
|
export interface ThinkingLabelAnimator {
|
|
466
|
-
/** Apply the next shimmer frame
|
|
467
|
-
tick(): void;
|
|
571
|
+
/** Apply the next shimmer frame, rebuilding frames when the label changes. */
|
|
572
|
+
tick(label?: string): void;
|
|
573
|
+
/** Apply a non-animated label (the host supplies its normal thinking style). */
|
|
574
|
+
show(label: string): void;
|
|
468
575
|
/** Restore pi's default static label. */
|
|
469
576
|
restore(): void;
|
|
470
|
-
/**
|
|
577
|
+
/** Current pre-rendered label frames (exposed for diagnostics). */
|
|
471
578
|
readonly frames: readonly string[];
|
|
472
579
|
}
|
|
473
580
|
|
|
474
|
-
|
|
581
|
+
export interface ThinkingTimer {
|
|
582
|
+
/** Render the active label with the current elapsed duration. */
|
|
583
|
+
tick(): void;
|
|
584
|
+
/** Freeze the elapsed duration; returns the total in milliseconds, or
|
|
585
|
+
* undefined when the timer was already terminal. */
|
|
586
|
+
complete(): number | undefined;
|
|
587
|
+
/** Restore pi's default label and make the timer terminal. */
|
|
588
|
+
restore(): void;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const THINKING_LABEL = " Thinking...";
|
|
592
|
+
const THOUGHT_LABEL = " Thought for";
|
|
593
|
+
const padThinkingLabel = (label: string): string => (label.startsWith(" ") ? label : ` ${label}`);
|
|
594
|
+
|
|
595
|
+
/** Format sub-second elapsed time as milliseconds, then use compact whole seconds. */
|
|
596
|
+
export function formatThinkingDuration(elapsedMs: number): string {
|
|
597
|
+
const wholeMs = Math.max(0, Math.floor(elapsedMs));
|
|
598
|
+
if (wholeMs < 1000) return `${wholeMs}ms`;
|
|
599
|
+
const totalSeconds = Math.floor(wholeMs / 1000);
|
|
600
|
+
const seconds = totalSeconds % 60;
|
|
601
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
602
|
+
const minutes = totalMinutes % 60;
|
|
603
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
604
|
+
if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m ${String(seconds).padStart(2, "0")}s`;
|
|
605
|
+
if (totalMinutes > 0) return `${totalMinutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
606
|
+
return `${seconds}s`;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Couple elapsed-time wording to the label animator through an injected clock.
|
|
611
|
+
* The timer starts on the first thinking delta, freezes once, and is terminal
|
|
612
|
+
* after completion or restore. `startElapsedMs` resumes a later run in the
|
|
613
|
+
* same message from its accumulated thinking time (per-message semantics:
|
|
614
|
+
* the host renders one label line per thinking run, all sharing the message
|
|
615
|
+
* label, so a fresh-zero restart would visibly rewind earlier lines).
|
|
616
|
+
*/
|
|
617
|
+
export function createThinkingTimer(
|
|
618
|
+
animator: ThinkingLabelAnimator,
|
|
619
|
+
now: () => number = Date.now,
|
|
620
|
+
startElapsedMs = 0,
|
|
621
|
+
): ThinkingTimer {
|
|
622
|
+
const startedAt = now() - startElapsedMs;
|
|
623
|
+
let terminal = false;
|
|
624
|
+
const elapsed = (): number => now() - startedAt;
|
|
625
|
+
const duration = (): string => formatThinkingDuration(elapsed());
|
|
626
|
+
return {
|
|
627
|
+
tick(): void {
|
|
628
|
+
if (terminal) return;
|
|
629
|
+
animator.tick(`${THINKING_LABEL} ${duration()}`);
|
|
630
|
+
},
|
|
631
|
+
complete(): number | undefined {
|
|
632
|
+
if (terminal) return undefined;
|
|
633
|
+
terminal = true;
|
|
634
|
+
const ms = elapsed();
|
|
635
|
+
animator.show(`${THOUGHT_LABEL} ${formatThinkingDuration(ms)}`);
|
|
636
|
+
return ms;
|
|
637
|
+
},
|
|
638
|
+
restore(): void {
|
|
639
|
+
terminal = true;
|
|
640
|
+
animator.restore();
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
}
|
|
475
644
|
|
|
476
645
|
/**
|
|
477
646
|
* True while a streaming assistant message is currently emitting a thinking
|
|
@@ -489,9 +658,9 @@ export function thinkingBlockActive(message: unknown): boolean {
|
|
|
489
658
|
* Animate pi's hidden-thinking label ("Thinking...") with the same shimmer as
|
|
490
659
|
* the working row. Pi renders the label as static italic `thinkingText` text;
|
|
491
660
|
* the only extension lever is `setHiddenThinkingLabel(label)`, which rebuilds
|
|
492
|
-
* chat children
|
|
493
|
-
*
|
|
494
|
-
*
|
|
661
|
+
* chat children, so index.ts drives it at 30fps only while the streaming
|
|
662
|
+
* message's last block is thinking. The elapsed label is rebuilt when its
|
|
663
|
+
* displayed duration changes; the shimmer frame still advances every tick.
|
|
495
664
|
*
|
|
496
665
|
* Tiers: low = theme `thinkingText` (pi's own label look), mid/high = session
|
|
497
666
|
* accent when enabled; every frame is italic like pi's label; no spinner.
|
|
@@ -504,6 +673,7 @@ export function createThinkingLabelAnimator(
|
|
|
504
673
|
): ThinkingLabelAnimator {
|
|
505
674
|
const noop: ThinkingLabelAnimator = {
|
|
506
675
|
tick() {},
|
|
676
|
+
show() {},
|
|
507
677
|
restore() {},
|
|
508
678
|
frames: [],
|
|
509
679
|
};
|
|
@@ -519,25 +689,42 @@ export function createThinkingLabelAnimator(
|
|
|
519
689
|
const accent = hexToAnsiFg(sessionAccentHex(sessionName));
|
|
520
690
|
if (accent) ansi = { ...ansi, mid: accent, high: accent };
|
|
521
691
|
}
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
692
|
+
const buildLabelFrames = (label: string): string[] => {
|
|
693
|
+
const padded = padThinkingLabel(label);
|
|
694
|
+
const text = padded.trimStart();
|
|
695
|
+
const leftPadding = padded.slice(0, padded.length - text.length);
|
|
696
|
+
return buildWorkingFrames([text], {
|
|
697
|
+
mode: workSettings.mode,
|
|
698
|
+
ansi,
|
|
699
|
+
bold: workSettings.bold,
|
|
700
|
+
spinner: false,
|
|
701
|
+
italic: true,
|
|
702
|
+
}).frames.map((frame) => `${leftPadding}${frame}`);
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
let currentLabel = THINKING_LABEL;
|
|
706
|
+
let frames = buildLabelFrames(currentLabel);
|
|
530
707
|
|
|
531
708
|
let index = 0;
|
|
532
|
-
let
|
|
709
|
+
let appliedLabel: string | undefined;
|
|
533
710
|
return {
|
|
534
|
-
frames
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
711
|
+
get frames(): readonly string[] {
|
|
712
|
+
return frames;
|
|
713
|
+
},
|
|
714
|
+
tick(label = THINKING_LABEL): void {
|
|
715
|
+
const paddedLabel = padThinkingLabel(label);
|
|
716
|
+
if (paddedLabel !== currentLabel) {
|
|
717
|
+
frames = buildLabelFrames(paddedLabel);
|
|
718
|
+
currentLabel = paddedLabel;
|
|
719
|
+
}
|
|
720
|
+
// A static frame changes only when its elapsed-time label changes.
|
|
721
|
+
if (appliedLabel === currentLabel && frames.length === 1) return;
|
|
538
722
|
ui.setHiddenThinkingLabel(frames[index % frames.length]);
|
|
539
723
|
index++;
|
|
540
|
-
|
|
724
|
+
appliedLabel = currentLabel;
|
|
725
|
+
},
|
|
726
|
+
show(label: string): void {
|
|
727
|
+
ui.setHiddenThinkingLabel(padThinkingLabel(label));
|
|
541
728
|
},
|
|
542
729
|
restore(): void {
|
|
543
730
|
// undefined → pi falls back to its default static label.
|
|
@@ -548,15 +735,24 @@ export function createThinkingLabelAnimator(
|
|
|
548
735
|
|
|
549
736
|
// ─── Installation ────────────────────────────────────────────────────────────
|
|
550
737
|
|
|
551
|
-
/** Structural slice of pi's Theme needed for the
|
|
738
|
+
/** Structural slice of pi's Theme needed for the palette and thinking label. */
|
|
552
739
|
export interface WorkingThemeLike {
|
|
553
|
-
/**
|
|
740
|
+
/** Retained for compatibility with older theme adapters. */
|
|
554
741
|
fg?: (name: "dim", text: string) => string;
|
|
555
742
|
getFgAnsi?: (name: "dim" | "muted" | "accent" | "thinkingText") => string;
|
|
556
743
|
}
|
|
557
744
|
|
|
745
|
+
export interface WorkingStatusOptionsLike {
|
|
746
|
+
frames?: string[];
|
|
747
|
+
intervalMs?: number;
|
|
748
|
+
}
|
|
749
|
+
|
|
558
750
|
export interface WorkingUiLike {
|
|
559
751
|
theme?: WorkingThemeLike;
|
|
752
|
+
/** Hosts with Pi's embedded working-status API render this in the editor border. */
|
|
753
|
+
setWorkingIndicator?: (options?: WorkingStatusOptionsLike) => void;
|
|
754
|
+
/** Clear the host message appended after a custom native frame. */
|
|
755
|
+
setWorkingMessage?: (message?: string) => void;
|
|
560
756
|
setWorkingVisible(visible: boolean): void;
|
|
561
757
|
setWidget(
|
|
562
758
|
key: string,
|
|
@@ -565,6 +761,7 @@ export interface WorkingUiLike {
|
|
|
565
761
|
): void;
|
|
566
762
|
}
|
|
567
763
|
|
|
764
|
+
/** @deprecated Retained for compatibility with the former hint-resolution seam. */
|
|
568
765
|
export interface KeybindingsLike {
|
|
569
766
|
getKeys(name: string): string[];
|
|
570
767
|
}
|
|
@@ -574,60 +771,26 @@ export interface WorkingIndicatorController {
|
|
|
574
771
|
start(): void;
|
|
575
772
|
/** Streaming ended — hide the row and pause the animation. */
|
|
576
773
|
stop(): void;
|
|
577
|
-
/** Remove the
|
|
774
|
+
/** Remove the indicator and stop the animation. */
|
|
578
775
|
dispose(): void;
|
|
776
|
+
/** @deprecated Token stats are owned by Pi and are no longer rendered here. */
|
|
777
|
+
setStats(text: string | undefined): void;
|
|
778
|
+
/** Request a host render for consumers that drive another animated label. */
|
|
779
|
+
requestRender(): void;
|
|
579
780
|
/** Pre-rendered frames (exposed for diagnostics). */
|
|
580
781
|
readonly frames: readonly string[];
|
|
581
782
|
}
|
|
582
783
|
|
|
583
784
|
const WIDGET_KEY = "pi-pretty-working";
|
|
584
785
|
|
|
585
|
-
async function loadHostKeybindings(): Promise<KeybindingsLike | undefined> {
|
|
586
|
-
try {
|
|
587
|
-
const tui = (await import("@earendil-works/pi-tui")) as typeof import("@earendil-works/pi-tui");
|
|
588
|
-
return tui.getKeybindings?.();
|
|
589
|
-
} catch {
|
|
590
|
-
return undefined;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
function formatKeyText(keys: string[]): string {
|
|
595
|
-
const darwin = process.platform === "darwin";
|
|
596
|
-
return keys
|
|
597
|
-
.join("/")
|
|
598
|
-
.split("/")
|
|
599
|
-
.map((key) =>
|
|
600
|
-
key
|
|
601
|
-
.split("+")
|
|
602
|
-
.map((part) => (darwin && part.toLowerCase() === "alt" ? "option" : part))
|
|
603
|
-
.join("+"),
|
|
604
|
-
)
|
|
605
|
-
.join("/");
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
async function resolveHint(
|
|
609
|
-
ui: WorkingUiLike,
|
|
610
|
-
deps?: { getKeybindings?: () => KeybindingsLike | undefined | Promise<KeybindingsLike | undefined> },
|
|
611
|
-
): Promise<string | undefined> {
|
|
612
|
-
try {
|
|
613
|
-
const loader = deps?.getKeybindings ?? loadHostKeybindings;
|
|
614
|
-
const keybindings = await loader();
|
|
615
|
-
const keys = keybindings?.getKeys?.("app.interrupt");
|
|
616
|
-
if (!keys?.length) return undefined;
|
|
617
|
-
const text = ` (${formatKeyText(keys)} to interrupt)`;
|
|
618
|
-
return ui.theme?.fg ? ui.theme.fg("dim", text) : text;
|
|
619
|
-
} catch {
|
|
620
|
-
return undefined;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
786
|
/**
|
|
625
|
-
*
|
|
787
|
+
* Install the custom working shimmer and return its lifecycle controller.
|
|
626
788
|
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
789
|
+
* Current Pi hosts receive the pre-rendered frames through the embedded
|
|
790
|
+
* working-status API, which places them in the editor's top border. Older
|
|
791
|
+
* hosts use a zero-padding widget above the editor. The host lifecycle drives
|
|
792
|
+
* the controller (`start` on agent_start, `stop` on agent_end); disabled or
|
|
793
|
+
* empty phrase settings leave host defaults untouched.
|
|
631
794
|
*/
|
|
632
795
|
export async function installWorkingIndicator(
|
|
633
796
|
ui: WorkingUiLike,
|
|
@@ -639,11 +802,14 @@ export async function installWorkingIndicator(
|
|
|
639
802
|
start() {},
|
|
640
803
|
stop() {},
|
|
641
804
|
dispose() {},
|
|
805
|
+
setStats() {},
|
|
806
|
+
requestRender() {},
|
|
642
807
|
frames: [],
|
|
643
808
|
};
|
|
644
809
|
if (!settings.enabled) return noopController;
|
|
645
810
|
const texts = settings.texts.map((t) => t.trim()).filter((t) => t.length > 0);
|
|
646
811
|
if (texts.length === 0) return noopController;
|
|
812
|
+
void deps; // Retained for compatibility; interrupt hints are no longer resolved here.
|
|
647
813
|
|
|
648
814
|
const ansiResolved = resolvePaletteAnsi(settings.palette, ui.theme);
|
|
649
815
|
let ansi = ansiResolved;
|
|
@@ -656,20 +822,73 @@ export async function installWorkingIndicator(
|
|
|
656
822
|
spinnerColor = hexToAnsiFg(dimAccentHex(accentHex));
|
|
657
823
|
}
|
|
658
824
|
}
|
|
659
|
-
const hint = settings.hint ? await resolveHint(ui, deps) : undefined;
|
|
660
825
|
const { frames, intervalMs } = buildWorkingFrames(texts, {
|
|
661
826
|
mode: settings.mode,
|
|
662
827
|
ansi,
|
|
663
828
|
bold: settings.bold,
|
|
664
829
|
spinnerColor,
|
|
665
|
-
hint,
|
|
666
830
|
});
|
|
667
831
|
if (frames.length === 0) return noopController;
|
|
668
832
|
|
|
833
|
+
const setWorkingIndicator = ui.setWorkingIndicator;
|
|
834
|
+
const setWorkingMessage = ui.setWorkingMessage;
|
|
835
|
+
if (typeof setWorkingIndicator === "function" && typeof setWorkingMessage === "function") {
|
|
836
|
+
let started = false;
|
|
837
|
+
let disposed = false;
|
|
838
|
+
const updateNativeMessage = (): void => {
|
|
839
|
+
// Keep Pi's built-in message hidden because the custom frame already
|
|
840
|
+
// supplies the working label; no hint or token suffix is added here.
|
|
841
|
+
setWorkingMessage.call(ui, "");
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
try {
|
|
845
|
+
setWorkingIndicator.call(ui, { frames, intervalMs });
|
|
846
|
+
// Keep the native status row hidden until the extension lifecycle starts it.
|
|
847
|
+
// An empty message is intentional: all visible content lives in our frames.
|
|
848
|
+
updateNativeMessage();
|
|
849
|
+
ui.setWorkingVisible(false);
|
|
850
|
+
} catch (error: unknown) {
|
|
851
|
+
try {
|
|
852
|
+
setWorkingIndicator.call(ui);
|
|
853
|
+
setWorkingMessage.call(ui);
|
|
854
|
+
} catch {
|
|
855
|
+
// Best-effort rollback; the caller still restores host visibility.
|
|
856
|
+
}
|
|
857
|
+
throw error;
|
|
858
|
+
}
|
|
859
|
+
return {
|
|
860
|
+
start: () => {
|
|
861
|
+
if (disposed || started) return;
|
|
862
|
+
started = true;
|
|
863
|
+
updateNativeMessage();
|
|
864
|
+
ui.setWorkingVisible(true);
|
|
865
|
+
},
|
|
866
|
+
stop: () => {
|
|
867
|
+
if (!started) return;
|
|
868
|
+
started = false;
|
|
869
|
+
ui.setWorkingVisible(false);
|
|
870
|
+
},
|
|
871
|
+
dispose: () => {
|
|
872
|
+
if (disposed) return;
|
|
873
|
+
if (started) {
|
|
874
|
+
started = false;
|
|
875
|
+
ui.setWorkingVisible(false);
|
|
876
|
+
}
|
|
877
|
+
disposed = true;
|
|
878
|
+
setWorkingIndicator.call(ui);
|
|
879
|
+
setWorkingMessage.call(ui);
|
|
880
|
+
},
|
|
881
|
+
setStats: (_text: string | undefined) => {},
|
|
882
|
+
requestRender: () => updateNativeMessage(),
|
|
883
|
+
frames,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
669
887
|
const widget = new WorkingWidget();
|
|
670
888
|
widget.setFrames(frames, intervalMs);
|
|
671
889
|
// Widget first, visibility second: if setWidget throws (older host without
|
|
672
|
-
// the API) the host loader was never hidden and pi's
|
|
890
|
+
// the native working-status API) the host loader was never hidden and pi's
|
|
891
|
+
// default remains.
|
|
673
892
|
ui.setWidget(
|
|
674
893
|
WIDGET_KEY,
|
|
675
894
|
(tui) => {
|
|
@@ -686,6 +905,8 @@ export async function installWorkingIndicator(
|
|
|
686
905
|
widget.dispose();
|
|
687
906
|
ui.setWidget(WIDGET_KEY, undefined);
|
|
688
907
|
},
|
|
908
|
+
setStats: (_text: string | undefined) => {},
|
|
909
|
+
requestRender: () => widget.requestRender(),
|
|
689
910
|
frames,
|
|
690
911
|
};
|
|
691
912
|
}
|