@agnishc/edb-global-footer 0.10.4 → 0.10.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.5] - 2026-05-15
4
+
5
+ ### Changed
6
+ - Working indicator no longer includes its own spinner frame in the message — removed double-spinner (the Loader renders the spinner via `setWorkingIndicator`)
7
+ - Completion message now uses `setWidget` instead of `setWorkingMessage` so it remains visible after `agent_end` tears down the Loader
8
+ - `cancelCompletionTimer` now clears the completion widget immediately when a new agent run starts
9
+ - TPS calculator now excludes tool execution time — clock pauses on `tool_execution_start` and resumes on next `message_update`, so TPS reflects actual streaming speed only
10
+ - TPS indicator moved to the right side of footer line 2, before the provider/model block
11
+ - TPS color is now three-state: red (< 30 t/s), yellow (< 50 t/s), green (≥ 50 t/s); lightning icon remains white
12
+ - Usage overlay now uses `DynamicBorder` (full-width, adapts to terminal) replacing the fixed-width 55-char box (imported from `edb-usage-stats`)
13
+ - `waitForIdle()` removed from `/usage` command handler — overlay now opens immediately even while the agent is running
14
+
3
15
  ## [0.10.4] - 2026-05-15
4
16
 
5
17
  ## [0.10.3] - 2026-05-15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agnishc/edb-global-footer",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "description": "Pi extension: two-line status footer showing path, git branch, token usage, and model",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/footer.ts CHANGED
@@ -17,7 +17,7 @@ import type { GitStatus } from "./types";
17
17
  // Common uses:
18
18
  // - "extmgr" - package manager status ("15 pkgs • auto update off")
19
19
  // - Keys for extensions whose status you want to hide
20
- const STATUS_KEY_BLACKLIST = new Set<string>(["extmgr"]);
20
+ const STATUS_KEY_BLACKLIST = new Set<string>(["extmgr", "sm"]);
21
21
 
22
22
  // ── Live state types ───────────────────────────────────────────────────────────
23
23
 
@@ -101,17 +101,16 @@ export function createFooterRenderer(
101
101
  }
102
102
 
103
103
  // Build three groups separated by pipes
104
- // Group 1: ↑input ↓output ⚡tps
104
+ // Group 1: ↑input ↓output
105
105
  // Group 2: Rcache Wcache $cost
106
106
  // Group 3: context%
107
107
  const { tps } = tpsState;
108
108
  const showTps = tps > 0;
109
109
 
110
- // Group 1: Read/Write + TPS
110
+ // Group 1: Read/Write tokens
111
111
  const group1Parts: string[] = [];
112
112
  if (totalInput) group1Parts.push(`↑${formatTokens(totalInput)}`);
113
113
  if (totalOutput) group1Parts.push(`↓${formatTokens(totalOutput)}`);
114
- if (showTps) group1Parts.push(theme.fg("accent", `⚡${formatTps(tps)}`));
115
114
  const group1 = group1Parts.length > 0 ? group1Parts.join(" ") : null;
116
115
 
117
116
  // Group 2: Cache + Price
@@ -154,6 +153,10 @@ export function createFooterRenderer(
154
153
  // Model + thinking level with icons
155
154
  const model = ctx.model;
156
155
  const rightPartsLine2: string[] = [];
156
+ if (showTps) {
157
+ const tpsColor = tps < 30 ? "error" : tps < 50 ? "warning" : "success";
158
+ rightPartsLine2.push(`⚡${theme.fg(tpsColor, formatTps(tps))}`);
159
+ }
157
160
  if (model) {
158
161
  if (model.provider && footerData.getAvailableProviderCount() > 1) {
159
162
  rightPartsLine2.push(theme.fg("muted", `(${model.provider})`));
@@ -176,18 +179,19 @@ export function createFooterRenderer(
176
179
  // Filter out blacklisted status keys
177
180
  const visibleStatuses = new Map<string, string>(
178
181
  Array.from(extensionStatuses.entries() as [string, string][]).filter(
179
- ([key]) => !STATUS_KEY_BLACKLIST.has(key),
182
+ ([key, value]) => !STATUS_KEY_BLACKLIST.has(key) && value.trim() !== "",
180
183
  ),
181
184
  );
182
185
  if (visibleStatuses.size > 0) {
183
186
  const statusLine = Array.from(visibleStatuses.values())
184
187
  .map((text) => sanitizeStatusText(text))
188
+ .filter((text) => text !== "")
185
189
  .sort((a, b) => a.localeCompare(b))
186
190
  .join(" ");
187
- lines.push(truncateToWidth(statusLine, width, theme.fg("dim", "...")));
191
+ if (statusLine) lines.push(truncateToWidth(statusLine, width, theme.fg("dim", "...")));
188
192
  }
189
193
 
190
- return lines.map((line) => truncateToWidth(line, width));
194
+ return lines.filter((line) => line !== "").map((line) => truncateToWidth(line, width));
191
195
  },
192
196
  };
193
197
  };
package/src/index.ts CHANGED
@@ -91,4 +91,12 @@ export default function globalFooterExtension(pi: ExtensionAPI): void {
91
91
  tpsCalculator.onMessageEnd(event.message);
92
92
  requestRender();
93
93
  });
94
+
95
+ pi.on("tool_execution_start", () => {
96
+ tpsCalculator.onToolStart();
97
+ });
98
+
99
+ pi.on("tool_execution_end", () => {
100
+ tpsCalculator.onToolEnd();
101
+ });
94
102
  }
package/src/tps.ts CHANGED
@@ -25,7 +25,9 @@ export interface TpsState {
25
25
 
26
26
  export class TpsCalculator {
27
27
  private streaming = false;
28
- private streamStart = 0;
28
+ private lastUpdateAt = 0; // timestamp of last message_update
29
+ private accumulatedMs = 0; // total streaming ms, excluding tool execution time
30
+ private inTool = false; // whether a tool is currently executing
29
31
  private tps = 0;
30
32
  private lastOutputTokens = 0;
31
33
  private lastInputTokens = 0;
@@ -35,33 +37,56 @@ export class TpsCalculator {
35
37
  */
36
38
  resetForTurn(): void {
37
39
  this.streaming = false;
38
- this.streamStart = 0;
40
+ this.lastUpdateAt = 0;
41
+ this.accumulatedMs = 0;
42
+ this.inTool = false;
39
43
  this.tps = 0;
40
44
  }
41
45
 
42
46
  /**
43
- * Called when streaming starts (first message_update).
47
+ * Call when a tool starts executing — pauses the streaming clock.
44
48
  */
45
- streamStarted(): void {
46
- this.streaming = true;
47
- this.streamStart = performance.now();
49
+ onToolStart(): void {
50
+ if (this.streaming && this.lastUpdateAt > 0) {
51
+ // Accumulate streaming time up to this point
52
+ this.accumulatedMs += performance.now() - this.lastUpdateAt;
53
+ this.lastUpdateAt = 0;
54
+ }
55
+ this.inTool = true;
56
+ }
57
+
58
+ /**
59
+ * Call when a tool finishes executing — resumes the streaming clock on next update.
60
+ */
61
+ onToolEnd(): void {
62
+ this.inTool = false;
63
+ // lastUpdateAt will be reset on the next message_update
48
64
  }
49
65
 
50
66
  /**
51
67
  * Called on each message_update during streaming.
52
68
  */
53
69
  onMessageUpdate(message: AssistantMessage): void {
70
+ const now = performance.now();
71
+
54
72
  if (!this.streaming) {
55
- this.streamStarted();
73
+ // First update of this streaming burst
74
+ this.streaming = true;
75
+ this.lastUpdateAt = now;
76
+ } else if (this.inTool || this.lastUpdateAt === 0) {
77
+ // Resuming after a tool call — restart the clock segment
78
+ this.lastUpdateAt = now;
79
+ this.inTool = false;
56
80
  }
57
81
 
82
+ // Count chars for live estimate
58
83
  let chars = 0;
59
84
  for (const block of message.content) {
60
85
  if (block.type === "text") chars += block.text.length;
61
86
  else if (block.type === "thinking") chars += block.thinking.length;
62
87
  }
63
88
 
64
- const elapsed = (performance.now() - this.streamStart) / 1000;
89
+ const elapsed = (this.accumulatedMs + (now - this.lastUpdateAt)) / 1000;
65
90
  if (elapsed > 0.1) {
66
91
  const estimatedTokens = chars / 4;
67
92
  this.tps = estimatedTokens / elapsed;
@@ -72,7 +97,13 @@ export class TpsCalculator {
72
97
  * Called on message_end to finalize TPS with actual usage.
73
98
  */
74
99
  onMessageEnd(message: AssistantMessage): void {
75
- const elapsed = this.streamStart > 0 ? (performance.now() - this.streamStart) / 1000 : 0;
100
+ const now = performance.now();
101
+ // Flush any remaining streaming time
102
+ if (this.lastUpdateAt > 0) {
103
+ this.accumulatedMs += now - this.lastUpdateAt;
104
+ this.lastUpdateAt = 0;
105
+ }
106
+ const elapsed = this.accumulatedMs / 1000;
76
107
  const outputTokens = message.usage?.output ?? 0;
77
108
  this.lastInputTokens = message.usage?.input ?? 0;
78
109
  this.lastOutputTokens = outputTokens;
@@ -506,9 +506,14 @@ function applyShimmer(text: string, frame: number, baseHex: string, shimmerHex:
506
506
  return result;
507
507
  }
508
508
 
509
+ export interface WorkingIndicatorRef {
510
+ currentLine: string | undefined;
511
+ }
512
+
509
513
  // ── Working Indicator ─────────────────────────────────────────────────────────
510
514
 
511
- export function installWorkingIndicator(pi: ExtensionAPI): void {
515
+ export function installWorkingIndicator(pi: ExtensionAPI): WorkingIndicatorRef {
516
+ const ref: WorkingIndicatorRef = { currentLine: undefined };
512
517
  let ctx: ExtensionContext | null = null;
513
518
  let isActive = false;
514
519
 
@@ -519,7 +524,6 @@ export function installWorkingIndicator(pi: ExtensionAPI): void {
519
524
  let completionTimer: ReturnType<typeof setTimeout> | null = null;
520
525
 
521
526
  // State
522
- let spinnerFrame = 0;
523
527
  let shimmerFrame = 0;
524
528
  let currentVerb = "Working";
525
529
  let startedAt = 0;
@@ -552,9 +556,6 @@ export function installWorkingIndicator(pi: ExtensionAPI): void {
552
556
  const theme = ctx.ui.theme;
553
557
  const sep = theme.fg("dim", " · ");
554
558
 
555
- // Spinner frame (accent colored)
556
- const frame = theme.fg("accent", SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length]!);
557
-
558
559
  // Verb with shimmer
559
560
  const verbText = `${currentVerb}...`;
560
561
  let verbStyled: string;
@@ -567,20 +568,21 @@ export function installWorkingIndicator(pi: ExtensionAPI): void {
567
568
  // Timer
568
569
  const timer = theme.fg("dim", formatElapsed(elapsedMs));
569
570
 
570
- // Assemble parts
571
- const parts: string[] = [frame, verbStyled];
572
- if (toolSuffix) parts.push(toolSuffix);
571
+ // Assemble parts — do NOT include frame here; the Loader prepends its own spinner via setWorkingIndicator
572
+ const parts: string[] = [verbStyled];
573
+ if (toolSuffix) parts.push(theme.fg("dim", toolSuffix));
573
574
  parts.push(timer);
574
575
 
575
- ctx.ui.setWorkingMessage(parts.join(sep));
576
+ const rendered = parts.join(sep);
577
+ ref.currentLine = rendered;
578
+ ctx.ui.setWorkingMessage(rendered);
576
579
  }
577
580
 
578
581
  // ── Timer management ──────────────────────────────────────────────────────
579
582
 
580
583
  function startTimers(): void {
581
- // Spinner + shimmer: 150ms
584
+ // Shimmer: 150ms
582
585
  spinnerTimer = setInterval(() => {
583
- spinnerFrame++;
584
586
  shimmerFrame++;
585
587
  render();
586
588
  }, SPINNER_INTERVAL_MS);
@@ -621,6 +623,7 @@ export function installWorkingIndicator(pi: ExtensionAPI): void {
621
623
  if (completionTimer) {
622
624
  clearTimeout(completionTimer);
623
625
  completionTimer = null;
626
+ ctx?.ui.setWidget("wi-completion", undefined);
624
627
  }
625
628
  }
626
629
 
@@ -635,14 +638,16 @@ export function installWorkingIndicator(pi: ExtensionAPI): void {
635
638
  const verb = theme.fg("accent", randomItem(COMPLETION_VERBS));
636
639
  const time = theme.fg("dim", formatElapsed(totalMs));
637
640
  const sep = theme.fg("dim", " · ");
641
+ const completionLine = `${check}${sep}${verb}${sep}${time}`;
638
642
 
639
- ctx.ui.setWorkingMessage(`${check}${sep}${verb}${sep}${time}`);
640
- ctx.ui.setStatus("working-indicator", `${check} ${verb}`);
643
+ // The Loader is torn down by pi on agent_end, so setWorkingMessage is a no-op.
644
+ // Use setWidget to briefly show the completion state above the editor.
645
+ ctx.ui.setWidget("wi-completion", [completionLine]);
641
646
 
642
647
  completionTimer = setTimeout(() => {
643
648
  completionTimer = null;
644
- ctx?.ui.setWorkingMessage(undefined);
645
- ctx?.ui.setStatus("working-indicator", "");
649
+ ref.currentLine = undefined;
650
+ ctx?.ui.setWidget("wi-completion", undefined);
646
651
  }, 3000);
647
652
  }
648
653
 
@@ -670,7 +675,6 @@ export function installWorkingIndicator(pi: ExtensionAPI): void {
670
675
  isActive = true;
671
676
  startedAt = Date.now();
672
677
  elapsedMs = 0;
673
- spinnerFrame = 0;
674
678
  shimmerFrame = 0;
675
679
  toolSuffix = undefined;
676
680
  activeTools.clear();
@@ -742,6 +746,9 @@ export function installWorkingIndicator(pi: ExtensionAPI): void {
742
746
  stopTimers();
743
747
  cancelCompletionTimer();
744
748
  isActive = false;
749
+ ref.currentLine = undefined;
745
750
  ctx = null;
746
751
  });
752
+
753
+ return ref;
747
754
  }