@agnishc/edb-global-footer 0.10.3 → 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 +14 -0
- package/package.json +1 -1
- package/src/footer.ts +11 -7
- package/src/index.ts +8 -0
- package/src/tps.ts +40 -9
- package/src/workingIndicator.ts +23 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
15
|
+
## [0.10.4] - 2026-05-15
|
|
16
|
+
|
|
3
17
|
## [0.10.3] - 2026-05-15
|
|
4
18
|
|
|
5
19
|
## [0.9.0] - 2026-05-15
|
package/package.json
CHANGED
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
|
|
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
|
|
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
|
|
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.
|
|
40
|
+
this.lastUpdateAt = 0;
|
|
41
|
+
this.accumulatedMs = 0;
|
|
42
|
+
this.inTool = false;
|
|
39
43
|
this.tps = 0;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
|
-
*
|
|
47
|
+
* Call when a tool starts executing — pauses the streaming clock.
|
|
44
48
|
*/
|
|
45
|
-
|
|
46
|
-
this.streaming
|
|
47
|
-
|
|
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
|
|
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 = (
|
|
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
|
|
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;
|
package/src/workingIndicator.ts
CHANGED
|
@@ -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):
|
|
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[] = [
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
640
|
-
|
|
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
|
-
|
|
645
|
-
ctx?.ui.
|
|
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
|
}
|