@heyhuynhgiabuu/pi-pretty 0.6.25 → 0.6.26
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 +16 -0
- package/README.md +19 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +137 -34
- package/dist/index.js.map +1 -1
- package/dist/tools/bash.js +1 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/find.js +4 -4
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/grep.js +1 -1
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/ls.js +2 -2
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/read.js +2 -2
- package/dist/tools/read.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/working-indicator.d.ts +63 -6
- package/dist/working-indicator.d.ts.map +1 -1
- package/dist/working-indicator.js +169 -14
- package/dist/working-indicator.js.map +1 -1
- package/package.json +7 -6
- package/src/index.ts +145 -38
- package/src/tools/bash.ts +1 -1
- package/src/tools/find.ts +4 -4
- package/src/tools/grep.ts +1 -1
- package/src/tools/ls.ts +2 -2
- package/src/tools/read.ts +2 -2
- package/src/types.ts +4 -0
- package/src/working-indicator.ts +214 -21
package/src/working-indicator.ts
CHANGED
|
@@ -366,6 +366,22 @@ export function buildWorkingFrames(
|
|
|
366
366
|
return { frames, intervalMs };
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
/** Estimate streamed output tokens: provider `usage.output` when exposed, else visible chars ÷ 4. */
|
|
370
|
+
export function workingTokens(message: unknown): number {
|
|
371
|
+
const usage = (message as { usage?: { output?: unknown } } | undefined)?.usage;
|
|
372
|
+
const output = Number(usage?.output);
|
|
373
|
+
if (Number.isFinite(output) && output > 0) return Math.round(output);
|
|
374
|
+
const content = (message as { content?: Array<{ type?: string; text?: unknown; thinking?: unknown }> } | undefined)
|
|
375
|
+
?.content;
|
|
376
|
+
if (!Array.isArray(content)) return 0;
|
|
377
|
+
let chars = 0;
|
|
378
|
+
for (const block of content) {
|
|
379
|
+
if (block?.type === "text" && typeof block.text === "string") chars += block.text.length;
|
|
380
|
+
else if (block?.type === "thinking" && typeof block.thinking === "string") chars += block.thinking.length;
|
|
381
|
+
}
|
|
382
|
+
return Math.round(chars / 4);
|
|
383
|
+
}
|
|
384
|
+
|
|
369
385
|
// ─── Widget ──────────────────────────────────────────────────────────────────
|
|
370
386
|
|
|
371
387
|
/** Slice of the pi-tui TUI instance the widget needs. */
|
|
@@ -386,6 +402,7 @@ export class WorkingWidget {
|
|
|
386
402
|
#tui: WidgetTuiLike | undefined;
|
|
387
403
|
#started = false;
|
|
388
404
|
#disposed = false;
|
|
405
|
+
#stats: string | undefined;
|
|
389
406
|
|
|
390
407
|
setFrames(frames: string[], intervalMs: number): void {
|
|
391
408
|
this.#frames = frames;
|
|
@@ -397,6 +414,19 @@ export class WorkingWidget {
|
|
|
397
414
|
this.#tui = tui;
|
|
398
415
|
}
|
|
399
416
|
|
|
417
|
+
/** Ask the host for a render — lets non-widget consumers (the thinking label
|
|
418
|
+
* ticker) reuse this widget's TUI handle instead of relying on natural
|
|
419
|
+
* streaming renders. */
|
|
420
|
+
requestRender(): void {
|
|
421
|
+
this.#tui?.requestRender();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Dynamic right-side status segment (e.g. live token count), styled dim at
|
|
425
|
+
* render time so theme changes don't bake stale ANSI into it. */
|
|
426
|
+
setStats(text: string | undefined): void {
|
|
427
|
+
this.#stats = text || undefined;
|
|
428
|
+
}
|
|
429
|
+
|
|
400
430
|
start(): void {
|
|
401
431
|
if (this.#disposed || this.#started) return;
|
|
402
432
|
this.#started = true;
|
|
@@ -421,7 +451,8 @@ export class WorkingWidget {
|
|
|
421
451
|
render(width: number): string[] {
|
|
422
452
|
if (!this.#started || this.#frames.length === 0) return [];
|
|
423
453
|
const frame = this.#frames[this.#index % this.#frames.length] ?? "";
|
|
424
|
-
|
|
454
|
+
const suffix = this.#stats ? `${FG_DIM}${this.#stats}${RESET_FG}` : "";
|
|
455
|
+
return [truncateToWidth(frame + suffix, Math.max(1, width))];
|
|
425
456
|
}
|
|
426
457
|
|
|
427
458
|
invalidate(): void {
|
|
@@ -462,17 +493,158 @@ export interface ThinkingUiLike {
|
|
|
462
493
|
setHiddenThinkingLabel(label?: string): void;
|
|
463
494
|
}
|
|
464
495
|
|
|
496
|
+
// ─── Per-row hidden-thinking labels ──────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
/** Controller for the host-component label interception. */
|
|
499
|
+
export interface PerRowThinkingLabels {
|
|
500
|
+
/** Mark the message currently streaming a thinking block (its row animates). */
|
|
501
|
+
setActive(timestamp: number): void;
|
|
502
|
+
/** Freeze a message's completed thinking duration in ms (its row shows
|
|
503
|
+
* `Thought for Xs` derived from this). */
|
|
504
|
+
complete(timestamp: number, durationMs: number): void;
|
|
505
|
+
/** Drop the active mark (run ended); completed rows keep their durations. */
|
|
506
|
+
clearActive(): void;
|
|
507
|
+
/** Restore the original prototype setter. */
|
|
508
|
+
uninstall(): void;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const PER_ROW_PATCH = Symbol.for("pi-pretty.per-row-thinking-labels");
|
|
512
|
+
|
|
513
|
+
interface RowInternals {
|
|
514
|
+
lastMessage?: { timestamp?: unknown };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Intercept the host's per-row label fan-out method
|
|
519
|
+
* (`AssistantMessageComponent.prototype.setHiddenThinkingLabel`): each row
|
|
520
|
+
* substitutes the incoming global label with its own — the streaming row
|
|
521
|
+
* animates, completed rows stay frozen at their own `Thought for Xs`, unknown
|
|
522
|
+
* rows fall back to pi's default. Any per-call failure passes the incoming
|
|
523
|
+
* label through unchanged, so a host internals change degrades to pi-pretty's
|
|
524
|
+
* global-label behavior instead of breaking. Durations live per session.
|
|
525
|
+
*/
|
|
526
|
+
export function installPerRowThinkingLabels(componentClass: unknown): PerRowThinkingLabels | undefined {
|
|
527
|
+
const proto = (componentClass as { prototype?: { setHiddenThinkingLabel?: unknown } } | undefined)?.prototype;
|
|
528
|
+
const original = proto?.setHiddenThinkingLabel;
|
|
529
|
+
if (typeof componentClass !== "function" || !proto || typeof original !== "function") return undefined;
|
|
530
|
+
const existing = (original as { [PER_ROW_PATCH]?: PerRowThinkingLabels })[PER_ROW_PATCH];
|
|
531
|
+
// Reuse the installed patch but start from clean state: a session_start
|
|
532
|
+
// without an intervening shutdown (defensive) must not resurrect stale rows.
|
|
533
|
+
if (existing) {
|
|
534
|
+
existing.clearActive();
|
|
535
|
+
return existing;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const state: { activeTs?: number; completed: Map<number, number> } = { completed: new Map() };
|
|
539
|
+
const patched = function (this: unknown, label: string | undefined): void {
|
|
540
|
+
const own = ownLabel(this, label);
|
|
541
|
+
(original as (this: unknown, label: string | undefined) => void).call(this, own);
|
|
542
|
+
};
|
|
543
|
+
const ownLabel = (component: unknown, incoming: string | undefined): string | undefined => {
|
|
544
|
+
try {
|
|
545
|
+
const ts = (component as RowInternals | undefined)?.lastMessage?.timestamp;
|
|
546
|
+
if (typeof ts !== "number") return incoming;
|
|
547
|
+
if (ts === state.activeTs) return incoming;
|
|
548
|
+
const ms = state.completed.get(ts);
|
|
549
|
+
return ms === undefined ? THINKING_LABEL : `Thought for ${formatThinkingDuration(ms)}`;
|
|
550
|
+
} catch {
|
|
551
|
+
return incoming;
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
const controller: PerRowThinkingLabels = {
|
|
555
|
+
setActive(timestamp: number): void {
|
|
556
|
+
state.activeTs = timestamp;
|
|
557
|
+
},
|
|
558
|
+
complete(timestamp: number, durationMs: number): void {
|
|
559
|
+
state.completed.set(timestamp, durationMs);
|
|
560
|
+
},
|
|
561
|
+
clearActive(): void {
|
|
562
|
+
state.activeTs = undefined;
|
|
563
|
+
},
|
|
564
|
+
uninstall(): void {
|
|
565
|
+
if (proto.setHiddenThinkingLabel === patched) {
|
|
566
|
+
proto.setHiddenThinkingLabel = original as { setHiddenThinkingLabel?: unknown }["setHiddenThinkingLabel"];
|
|
567
|
+
}
|
|
568
|
+
state.completed.clear();
|
|
569
|
+
state.activeTs = undefined;
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
(patched as { [PER_ROW_PATCH]?: PerRowThinkingLabels })[PER_ROW_PATCH] = controller;
|
|
573
|
+
proto.setHiddenThinkingLabel = patched as NonNullable<{ setHiddenThinkingLabel?: unknown }["setHiddenThinkingLabel"]>;
|
|
574
|
+
return controller;
|
|
575
|
+
}
|
|
576
|
+
|
|
465
577
|
export interface ThinkingLabelAnimator {
|
|
466
|
-
/** Apply the next shimmer frame
|
|
467
|
-
tick(): void;
|
|
578
|
+
/** Apply the next shimmer frame, rebuilding frames when the label changes. */
|
|
579
|
+
tick(label?: string): void;
|
|
580
|
+
/** Apply a non-animated label (the host supplies its normal thinking style). */
|
|
581
|
+
show(label: string): void;
|
|
468
582
|
/** Restore pi's default static label. */
|
|
469
583
|
restore(): void;
|
|
470
|
-
/**
|
|
584
|
+
/** Current pre-rendered label frames (exposed for diagnostics). */
|
|
471
585
|
readonly frames: readonly string[];
|
|
472
586
|
}
|
|
473
587
|
|
|
588
|
+
export interface ThinkingTimer {
|
|
589
|
+
/** Render the active label with the current elapsed duration. */
|
|
590
|
+
tick(): void;
|
|
591
|
+
/** Freeze the elapsed duration; returns the total in milliseconds, or
|
|
592
|
+
* undefined when the timer was already terminal. */
|
|
593
|
+
complete(): number | undefined;
|
|
594
|
+
/** Restore pi's default label and make the timer terminal. */
|
|
595
|
+
restore(): void;
|
|
596
|
+
}
|
|
597
|
+
|
|
474
598
|
const THINKING_LABEL = "Thinking...";
|
|
475
599
|
|
|
600
|
+
/** Format elapsed milliseconds as whole seconds with compact padded units. */
|
|
601
|
+
export function formatThinkingDuration(elapsedMs: number): string {
|
|
602
|
+
const totalSeconds = Math.floor(elapsedMs / 1000);
|
|
603
|
+
const seconds = totalSeconds % 60;
|
|
604
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
605
|
+
const minutes = totalMinutes % 60;
|
|
606
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
607
|
+
if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m ${String(seconds).padStart(2, "0")}s`;
|
|
608
|
+
if (totalMinutes > 0) return `${totalMinutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
609
|
+
return `${seconds}s`;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Couple elapsed-time wording to the label animator through an injected clock.
|
|
614
|
+
* The timer starts on the first thinking delta, freezes once, and is terminal
|
|
615
|
+
* after completion or restore. `startElapsedMs` resumes a later run in the
|
|
616
|
+
* same message from its accumulated thinking time (per-message semantics:
|
|
617
|
+
* the host renders one label line per thinking run, all sharing the message
|
|
618
|
+
* label, so a fresh-zero restart would visibly rewind earlier lines).
|
|
619
|
+
*/
|
|
620
|
+
export function createThinkingTimer(
|
|
621
|
+
animator: ThinkingLabelAnimator,
|
|
622
|
+
now: () => number = Date.now,
|
|
623
|
+
startElapsedMs = 0,
|
|
624
|
+
): ThinkingTimer {
|
|
625
|
+
const startedAt = now() - startElapsedMs;
|
|
626
|
+
let terminal = false;
|
|
627
|
+
const elapsed = (): number => now() - startedAt;
|
|
628
|
+
const duration = (): string => formatThinkingDuration(elapsed());
|
|
629
|
+
return {
|
|
630
|
+
tick(): void {
|
|
631
|
+
if (terminal) return;
|
|
632
|
+
animator.tick(`${THINKING_LABEL} ${duration()}`);
|
|
633
|
+
},
|
|
634
|
+
complete(): number | undefined {
|
|
635
|
+
if (terminal) return undefined;
|
|
636
|
+
terminal = true;
|
|
637
|
+
const ms = elapsed();
|
|
638
|
+
animator.show(`Thought for ${formatThinkingDuration(ms)}`);
|
|
639
|
+
return ms;
|
|
640
|
+
},
|
|
641
|
+
restore(): void {
|
|
642
|
+
terminal = true;
|
|
643
|
+
animator.restore();
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
476
648
|
/**
|
|
477
649
|
* True while a streaming assistant message is currently emitting a thinking
|
|
478
650
|
* block (its last content block). Used to gate label ticks to the thinking
|
|
@@ -489,9 +661,9 @@ export function thinkingBlockActive(message: unknown): boolean {
|
|
|
489
661
|
* Animate pi's hidden-thinking label ("Thinking...") with the same shimmer as
|
|
490
662
|
* the working row. Pi renders the label as static italic `thinkingText` text;
|
|
491
663
|
* the only extension lever is `setHiddenThinkingLabel(label)`, which rebuilds
|
|
492
|
-
* chat children
|
|
493
|
-
*
|
|
494
|
-
*
|
|
664
|
+
* chat children, so index.ts drives it at 30fps only while the streaming
|
|
665
|
+
* message's last block is thinking. The elapsed label is rebuilt only when its
|
|
666
|
+
* whole-second text changes; the shimmer frame still advances every tick.
|
|
495
667
|
*
|
|
496
668
|
* Tiers: low = theme `thinkingText` (pi's own label look), mid/high = session
|
|
497
669
|
* accent when enabled; every frame is italic like pi's label; no spinner.
|
|
@@ -504,6 +676,7 @@ export function createThinkingLabelAnimator(
|
|
|
504
676
|
): ThinkingLabelAnimator {
|
|
505
677
|
const noop: ThinkingLabelAnimator = {
|
|
506
678
|
tick() {},
|
|
679
|
+
show() {},
|
|
507
680
|
restore() {},
|
|
508
681
|
frames: [],
|
|
509
682
|
};
|
|
@@ -519,25 +692,37 @@ export function createThinkingLabelAnimator(
|
|
|
519
692
|
const accent = hexToAnsiFg(sessionAccentHex(sessionName));
|
|
520
693
|
if (accent) ansi = { ...ansi, mid: accent, high: accent };
|
|
521
694
|
}
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
695
|
+
const buildLabelFrames = (label: string): string[] =>
|
|
696
|
+
buildWorkingFrames([label], {
|
|
697
|
+
mode: workSettings.mode,
|
|
698
|
+
ansi,
|
|
699
|
+
bold: workSettings.bold,
|
|
700
|
+
spinner: false,
|
|
701
|
+
italic: true,
|
|
702
|
+
}).frames;
|
|
703
|
+
|
|
704
|
+
let currentLabel = THINKING_LABEL;
|
|
705
|
+
let frames = buildLabelFrames(currentLabel);
|
|
530
706
|
|
|
531
707
|
let index = 0;
|
|
532
|
-
let
|
|
708
|
+
let appliedLabel: string | undefined;
|
|
533
709
|
return {
|
|
534
|
-
frames
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
710
|
+
get frames(): readonly string[] {
|
|
711
|
+
return frames;
|
|
712
|
+
},
|
|
713
|
+
tick(label = THINKING_LABEL): void {
|
|
714
|
+
if (label !== currentLabel) {
|
|
715
|
+
frames = buildLabelFrames(label);
|
|
716
|
+
currentLabel = label;
|
|
717
|
+
}
|
|
718
|
+
// A static frame changes only when its elapsed-time label changes.
|
|
719
|
+
if (appliedLabel === currentLabel && frames.length === 1) return;
|
|
538
720
|
ui.setHiddenThinkingLabel(frames[index % frames.length]);
|
|
539
721
|
index++;
|
|
540
|
-
|
|
722
|
+
appliedLabel = currentLabel;
|
|
723
|
+
},
|
|
724
|
+
show(label: string): void {
|
|
725
|
+
ui.setHiddenThinkingLabel(label);
|
|
541
726
|
},
|
|
542
727
|
restore(): void {
|
|
543
728
|
// undefined → pi falls back to its default static label.
|
|
@@ -576,6 +761,10 @@ export interface WorkingIndicatorController {
|
|
|
576
761
|
stop(): void;
|
|
577
762
|
/** Remove the widget and stop the animation. */
|
|
578
763
|
dispose(): void;
|
|
764
|
+
/** Update the dim right-side status segment (live token count). */
|
|
765
|
+
setStats(text: string | undefined): void;
|
|
766
|
+
/** Request a host render via the widget's TUI handle. */
|
|
767
|
+
requestRender(): void;
|
|
579
768
|
/** Pre-rendered frames (exposed for diagnostics). */
|
|
580
769
|
readonly frames: readonly string[];
|
|
581
770
|
}
|
|
@@ -639,6 +828,8 @@ export async function installWorkingIndicator(
|
|
|
639
828
|
start() {},
|
|
640
829
|
stop() {},
|
|
641
830
|
dispose() {},
|
|
831
|
+
setStats() {},
|
|
832
|
+
requestRender() {},
|
|
642
833
|
frames: [],
|
|
643
834
|
};
|
|
644
835
|
if (!settings.enabled) return noopController;
|
|
@@ -686,6 +877,8 @@ export async function installWorkingIndicator(
|
|
|
686
877
|
widget.dispose();
|
|
687
878
|
ui.setWidget(WIDGET_KEY, undefined);
|
|
688
879
|
},
|
|
880
|
+
setStats: (text: string | undefined) => widget.setStats(text),
|
|
881
|
+
requestRender: () => widget.requestRender(),
|
|
689
882
|
frames,
|
|
690
883
|
};
|
|
691
884
|
}
|