pi-editor-footer 0.5.0 → 0.6.1
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/dist/color-policy.js +13 -5
- package/dist/index.js +49 -8
- package/dist/live-border.js +69 -13
- package/dist/telemetry.js +81 -6
- package/package.json +1 -2
- package/src/color-policy.ts +13 -5
- package/src/index.ts +98 -52
- package/src/live-border.ts +91 -14
- package/src/telemetry.ts +84 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.1] - 2026-08-25
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Live `↑` now per-agent delta (`279k-261k=18k` for 10 turns) not session total — `live-border` top `↑`/`↓` when idle uses `totals - baseline` at `agent_start` (`LiveBorder.setAgentBaseline`), timeline `↑`/`↓`/`$` also prefers baseline delta; when running uses per-agent sum via `telemetry:peekAgentLive()` which now always resets on `agent_start` (removed stale `if (agentStartMs===null)` guard) and handles `agent_end` alias for `agent_settled`
|
|
12
|
+
|
|
13
|
+
## [0.6.0] - 2026-08-24
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Context usage colors now `12.5%`/`25%`/`50%` quotas — `dim` 0-12.5 → `accent` 12.5-25 → `warning` 25-50 → `error` 50-100 via `color-policy:contextUsageColor` (was `25%`/`50%`/`75%`), respects theme semantic tokens
|
|
18
|
+
- Live `↑`/`↓` tokens now per agent run (option B) — cumulative across turns in this agent via `telemetry:peekAgentLive()` + `live-border` top `↑`/`↓` (was per-turn via `getLastTurnTelemetry`), `getLastTelemetry` stays agent sum
|
|
19
|
+
- Stall relocated from bottom telemetry to top right of tool use with `dim |` pipe — `run-activity` now `tools | !2×3.3s` top, bottom `telemetry:formatTurnTelemetry` now `TPS · TTFT` only (suppressed `stalls:false`)
|
|
20
|
+
|
|
7
21
|
## [0.5.0] - 2026-08-22
|
|
8
22
|
|
|
9
23
|
### Changed
|
package/dist/color-policy.js
CHANGED
|
@@ -6,13 +6,21 @@ export function stressColor(value, warn = 70, danger = 90) {
|
|
|
6
6
|
return "accent";
|
|
7
7
|
}
|
|
8
8
|
export function contextUsageColor(pct) {
|
|
9
|
-
// 25
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
// 12.5 / 25 / 50 quotas — four urgency tiers, increasingly aggressive as context fills.
|
|
10
|
+
// 0 – 12.5% dim — plenty of headroom, visually quiet.
|
|
11
|
+
// 12.5 – 25% accent — first nudge, noticeable but calm.
|
|
12
|
+
// 25 – 50% warning — half consumed, needs attention.
|
|
13
|
+
// 50 – 100% error — critical, about to run out.
|
|
14
|
+
// Uses theme semantic tokens (dim/accent/warning/error) so the progression
|
|
15
|
+
// respects the active theme and remains legible on light/dark/custom palettes.
|
|
16
|
+
// A fixed hex palette (e.g. grey→sky→amber→red) would be more vivid but
|
|
17
|
+
// would ignore the user's theme and can clash with light backgrounds —
|
|
18
|
+
// semantic tokens keep the "aggressive" ordering while staying theme-coherent.
|
|
13
19
|
if (pct >= 50)
|
|
14
|
-
return "
|
|
20
|
+
return "error";
|
|
15
21
|
if (pct >= 25)
|
|
22
|
+
return "warning";
|
|
23
|
+
if (pct >= 12.5)
|
|
16
24
|
return "accent";
|
|
17
25
|
return "dim";
|
|
18
26
|
}
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,7 @@ const REFRESH_MS = 1000;
|
|
|
52
52
|
let liveTickTimer = null;
|
|
53
53
|
let footerState = createInitialState();
|
|
54
54
|
let agentStartMs = null;
|
|
55
|
+
let agentBaselineTotals = null;
|
|
55
56
|
let currentModelInfo = {
|
|
56
57
|
provider: "",
|
|
57
58
|
modelId: "unknown",
|
|
@@ -170,7 +171,8 @@ function injectTimelineDimLine(_ctx, rawLine) {
|
|
|
170
171
|
// SAFETY: pi custom entry is TUI-only, not sent to LLM
|
|
171
172
|
extensionPi?.appendEntry?.("timeline", { text: rawLine });
|
|
172
173
|
}
|
|
173
|
-
catch {
|
|
174
|
+
catch {
|
|
175
|
+
// SAFETY: best-effort, ignore recoverable error
|
|
174
176
|
// SAFETY: best-effort, ignore recoverable error
|
|
175
177
|
}
|
|
176
178
|
// keep legacy array in sync
|
|
@@ -320,7 +322,8 @@ export default function (pi) {
|
|
|
320
322
|
return new Text(lines.join("\n"));
|
|
321
323
|
});
|
|
322
324
|
}
|
|
323
|
-
catch {
|
|
325
|
+
catch {
|
|
326
|
+
// SAFETY: best-effort, ignore recoverable error
|
|
324
327
|
// SAFETY: best-effort, ignore recoverable error
|
|
325
328
|
}
|
|
326
329
|
let headerCleanupInner = null;
|
|
@@ -451,6 +454,8 @@ export default function (pi) {
|
|
|
451
454
|
}
|
|
452
455
|
currentModelInfo = modelInfoOf(ctx);
|
|
453
456
|
lastSessionCtx = ctx;
|
|
457
|
+
agentBaselineTotals = null;
|
|
458
|
+
liveBorder.setAgentBaseline(null);
|
|
454
459
|
// Deferred so we win the single editor slot (see installEditor).
|
|
455
460
|
deferredInstallTimer = setTimeout(() => installEditor(ctx.ui), 0);
|
|
456
461
|
// header disabled — first line workspace/hints removed per user request; cwd preserved in footer below input
|
|
@@ -544,6 +549,8 @@ export default function (pi) {
|
|
|
544
549
|
// timeline entries are custom entries interleaved — no aboveEditor widget to clear
|
|
545
550
|
wallTimeHistory = [];
|
|
546
551
|
agentStartMs = null;
|
|
552
|
+
agentBaselineTotals = null;
|
|
553
|
+
liveBorder.setAgentBaseline(null);
|
|
547
554
|
footerState = {
|
|
548
555
|
...footerState,
|
|
549
556
|
workingSince: undefined,
|
|
@@ -591,9 +598,21 @@ export default function (pi) {
|
|
|
591
598
|
const refreshAllLive = () => {
|
|
592
599
|
liveBorder.render();
|
|
593
600
|
};
|
|
594
|
-
pi.on("agent_start", (e) => {
|
|
601
|
+
pi.on("agent_start", (e, ctx) => {
|
|
595
602
|
telemetryTracker.handle(e);
|
|
596
603
|
runActivityTracker.startRun();
|
|
604
|
+
// Capture baseline totals for per-agent delta (live input 18k not 279k = totals - baseline)
|
|
605
|
+
try {
|
|
606
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
607
|
+
const baselineCtx = (ctx ?? lastSessionCtx);
|
|
608
|
+
if (baselineCtx?.sessionManager?.getEntries) {
|
|
609
|
+
agentBaselineTotals = getUsageTotals(baselineCtx);
|
|
610
|
+
liveBorder.setAgentBaseline(agentBaselineTotals);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
// SAFETY: best-effort, ignore recoverable error
|
|
615
|
+
}
|
|
597
616
|
// timeline stays permanently between each run — do not hide on start
|
|
598
617
|
agentStartMs = Date.now();
|
|
599
618
|
footerState = {
|
|
@@ -604,6 +623,11 @@ export default function (pi) {
|
|
|
604
623
|
startLiveTick();
|
|
605
624
|
refreshAllLive();
|
|
606
625
|
});
|
|
626
|
+
pi.on("agent_end", (e) => {
|
|
627
|
+
// Alias for agent_settled — ensure tracker resets even if only agent_end is emitted
|
|
628
|
+
telemetryTracker.handle(e);
|
|
629
|
+
runActivityTracker.settle();
|
|
630
|
+
});
|
|
607
631
|
pi.on("turn_start", (e, ctx) => {
|
|
608
632
|
// Input is known at turn_start via context usage — seed live input so peekLive shows it during streaming (output already streams via liveDeltaChars)
|
|
609
633
|
const usageTokens = ctx // SAFETY: pi context seam — getContextUsage is ExtensionContext API
|
|
@@ -691,10 +715,26 @@ export default function (pi) {
|
|
|
691
715
|
const cacheRate = totals.latestCacheHitRate ?? 0;
|
|
692
716
|
const cacheStr = `${glyphs.cacheHit} ${cacheRate.toFixed(1)}%`;
|
|
693
717
|
// Respect timeline.* toggles for specified metrics (wallTime/tokens/cost), but datetime/cache/turn/tools are always shown per user spec
|
|
694
|
-
// Timeline tokens
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
718
|
+
// Timeline tokens per-agent: prefer baseline delta (totals - baseline) which yields 18k (279k-261k) not 279k total.
|
|
719
|
+
// Fallback to tel (tracker sum) then session totals.
|
|
720
|
+
let telInput;
|
|
721
|
+
let telOutput;
|
|
722
|
+
let telCost;
|
|
723
|
+
if (agentBaselineTotals) {
|
|
724
|
+
telInput = Math.max(0, totals.input - agentBaselineTotals.input);
|
|
725
|
+
telOutput = Math.max(0, totals.output - agentBaselineTotals.output);
|
|
726
|
+
telCost = Math.max(0, totals.cost - agentBaselineTotals.cost);
|
|
727
|
+
}
|
|
728
|
+
else if (tel) {
|
|
729
|
+
telInput = tel.inputTokens;
|
|
730
|
+
telOutput = tel.outputTokens;
|
|
731
|
+
telCost = tel.costUsd;
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
telInput = totals.input;
|
|
735
|
+
telOutput = totals.output;
|
|
736
|
+
telCost = totals.cost;
|
|
737
|
+
}
|
|
698
738
|
const line1Parts = [dt];
|
|
699
739
|
if (currentConfig.timeline.wallTime)
|
|
700
740
|
line1Parts.push(wallDur);
|
|
@@ -724,7 +764,8 @@ export default function (pi) {
|
|
|
724
764
|
wallText);
|
|
725
765
|
}
|
|
726
766
|
}
|
|
727
|
-
catch {
|
|
767
|
+
catch {
|
|
768
|
+
// SAFETY: best-effort, ignore recoverable error
|
|
728
769
|
// SAFETY: best-effort, ignore recoverable error
|
|
729
770
|
}
|
|
730
771
|
// final settled telemetry overwrites live peek with authoritative totals
|
package/dist/live-border.js
CHANGED
|
@@ -13,16 +13,21 @@
|
|
|
13
13
|
import { createChromeSnapshot, formatTopContextFromSnapshot, } from "./chrome-state.js";
|
|
14
14
|
import { resolveGlyphs, resolveIconMode } from "./icons.js";
|
|
15
15
|
import { formatRunActivityTopRight } from "./run-activity.js";
|
|
16
|
-
import { formatTelemetryTokens, formatTurnTelemetry } from "./telemetry.js";
|
|
16
|
+
import { formatTelemetryTokens, formatTurnDuration, formatTurnTelemetry, } from "./telemetry.js";
|
|
17
17
|
export const REFRESH_MS = 1000;
|
|
18
18
|
export class LiveBorder {
|
|
19
19
|
deps;
|
|
20
20
|
timer = null;
|
|
21
21
|
lastRenderMs = 0;
|
|
22
22
|
pendingRender = null;
|
|
23
|
+
agentBaseline = null;
|
|
23
24
|
constructor(deps) {
|
|
24
25
|
this.deps = deps;
|
|
25
26
|
}
|
|
27
|
+
/** Set baseline totals at agent_start for per-agent delta (input/output/cost). */
|
|
28
|
+
setAgentBaseline(baseline) {
|
|
29
|
+
this.agentBaseline = baseline ? { ...baseline } : null;
|
|
30
|
+
}
|
|
26
31
|
/** Coalesced render: top (run-activity) + bottom (telemetry) + context bar → editor. */
|
|
27
32
|
render() {
|
|
28
33
|
const now = Date.now();
|
|
@@ -92,13 +97,33 @@ export class LiveBorder {
|
|
|
92
97
|
refreshTopBorder() {
|
|
93
98
|
const editor = this.deps.getEditor();
|
|
94
99
|
const ctx = this.deps.getCtx();
|
|
100
|
+
const cfg = this.deps.getConfig();
|
|
95
101
|
if (!editor || !ctx)
|
|
96
102
|
return;
|
|
97
103
|
try {
|
|
98
|
-
// SAFETY: theme is live pi TUI theme read at render time
|
|
99
|
-
const theme = ctx.ui.theme; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
104
|
+
// SAFETY: theme is live pi TUI theme read at render time — intentional unsafe cast, validated at runtime
|
|
105
|
+
const theme = ctx.ui.theme; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
100
106
|
const snap = this.deps.runActivityTracker.getSnapshot();
|
|
101
|
-
|
|
107
|
+
let text = formatRunActivityTopRight(snap, theme);
|
|
108
|
+
// Relocate stall to the right of tool use with pipe separator — agent-run live (option B), not bottom telemetry
|
|
109
|
+
if (cfg.telemetry.enabled && cfg.telemetry.stalls) {
|
|
110
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — telemetry tracker for stall (agent run, option B)
|
|
111
|
+
const tracker = this.deps.telemetryTracker;
|
|
112
|
+
const tel = tracker.peekAgentLive() ?? tracker.getLastTelemetry();
|
|
113
|
+
if (tel && tel.stallMs > 0) {
|
|
114
|
+
const glyphs = resolveGlyphs(cfg.icons.mode);
|
|
115
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — theme fg
|
|
116
|
+
const stallText = theme.fg("warning", `${glyphs.stall}${tel.stallCount}×${formatTurnDuration(tel.stallMs).trim()}`);
|
|
117
|
+
if (text) {
|
|
118
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — theme fg for pipe
|
|
119
|
+
const pipe = theme.fg("dim", " | ");
|
|
120
|
+
text = `${text}${pipe}${stallText}`;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
text = stallText;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
102
127
|
editor.setTopRightText(text);
|
|
103
128
|
}
|
|
104
129
|
catch {
|
|
@@ -122,15 +147,17 @@ export class LiveBorder {
|
|
|
122
147
|
return;
|
|
123
148
|
}
|
|
124
149
|
try {
|
|
125
|
-
// SAFETY: peekLive ?? getLastTelemetry preserves cost after toggle (AGENTS.md gotcha)
|
|
150
|
+
// SAFETY: peekLive ?? getLastTelemetry preserves cost after toggle (AGENTS.md gotcha) — intentional unsafe cast, validated at runtime
|
|
126
151
|
const live = this.deps.telemetryTracker.peekLive() ??
|
|
127
152
|
this.deps.telemetryTracker.getLastTelemetry();
|
|
128
153
|
if (!live)
|
|
129
154
|
return;
|
|
130
|
-
// SAFETY: pi
|
|
131
|
-
const theme = ctx?.ui?.theme;
|
|
155
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — theme from extension context
|
|
156
|
+
const theme = ctx?.ui?.theme;
|
|
132
157
|
const glyphs = resolveGlyphs(cfg.icons.mode);
|
|
133
|
-
|
|
158
|
+
// Stall relocated to top right of tool use with pipe — suppress in bottom telemetry
|
|
159
|
+
const bottomCfg = { ...cfg.telemetry, stalls: false };
|
|
160
|
+
const right = formatTurnTelemetry(live, theme, bottomCfg, glyphs);
|
|
134
161
|
if (live.totalMs > 0) {
|
|
135
162
|
editor.setTelemetryText(right);
|
|
136
163
|
editor.setBottomLeftText("");
|
|
@@ -169,11 +196,40 @@ export class LiveBorder {
|
|
|
169
196
|
// Tokens line above model info — left aligned, no border; hidden at startup per user request
|
|
170
197
|
let tokensText = "";
|
|
171
198
|
if (cfg.telemetry.enabled && cfg.telemetry.tokens) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
199
|
+
const isRunning = this.deps.runActivityTracker.isRunning();
|
|
200
|
+
// When idle (settled) and baseline available, show per-agent delta (input/output/cost) = cur totals - baseline at agent_start.
|
|
201
|
+
// This yields 18k for the example (279k session - 261k baseline = 18k agent) instead of 279k total.
|
|
202
|
+
// When running, use live agent tracker (sum of turns in this agent + live turn) which is already per-agent after guard fix.
|
|
203
|
+
if (!isRunning && this.agentBaseline) {
|
|
204
|
+
const cur = snapshot.totals;
|
|
205
|
+
const base = this.agentBaseline;
|
|
206
|
+
const deltaInput = Math.max(0, cur.input - base.input);
|
|
207
|
+
const deltaOutput = Math.max(0, cur.output - base.output);
|
|
208
|
+
// Build minimal telemetry for formatting (only tokens matter for formatTelemetryTokens)
|
|
209
|
+
const deltaTel = {
|
|
210
|
+
tps: null,
|
|
211
|
+
ttftMs: 0,
|
|
212
|
+
totalMs: 0,
|
|
213
|
+
inputTokens: deltaInput,
|
|
214
|
+
outputTokens: deltaOutput,
|
|
215
|
+
stallMs: 0,
|
|
216
|
+
stallCount: 0,
|
|
217
|
+
rateUsdPerMTokens: null,
|
|
218
|
+
generationMs: 0,
|
|
219
|
+
totalTokens: deltaInput + deltaOutput,
|
|
220
|
+
costUsd: Math.max(0, cur.cost - base.cost),
|
|
221
|
+
measurementMs: null,
|
|
222
|
+
};
|
|
223
|
+
tokensText = formatTelemetryTokens(deltaTel, theme, cfg.telemetry, glyphs);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// Live or fallback — per-agent sum from tracker (option B), correctly reset per agent_start
|
|
227
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — telemetry tracker for top tokens line (agent run)
|
|
228
|
+
const tracker = this.deps.telemetryTracker;
|
|
229
|
+
const live = tracker.peekAgentLive() ?? tracker.getLastTelemetry();
|
|
230
|
+
if (live) {
|
|
231
|
+
tokensText = formatTelemetryTokens(live, theme, cfg.telemetry, glyphs);
|
|
232
|
+
}
|
|
177
233
|
}
|
|
178
234
|
}
|
|
179
235
|
editor.setTopContextText(contextText);
|
package/dist/telemetry.js
CHANGED
|
@@ -50,6 +50,7 @@ export class TurnTelemetryTracker {
|
|
|
50
50
|
agentStartMs = null;
|
|
51
51
|
agentTurns = [];
|
|
52
52
|
lastTelemetry = null;
|
|
53
|
+
lastTurnTelemetry = null;
|
|
53
54
|
decayBaseTps = null;
|
|
54
55
|
decayStartMs = null;
|
|
55
56
|
constructor(now = () => performance.now()) {
|
|
@@ -58,6 +59,78 @@ export class TurnTelemetryTracker {
|
|
|
58
59
|
getLastTelemetry() {
|
|
59
60
|
return this.lastTelemetry;
|
|
60
61
|
}
|
|
62
|
+
getLastTurnTelemetry() {
|
|
63
|
+
return this.lastTurnTelemetry;
|
|
64
|
+
}
|
|
65
|
+
/** Agent-run live: cumulative across turns in current agent, including current live turn. Option B. */
|
|
66
|
+
peekAgentLive() {
|
|
67
|
+
// No agent active -> show last agent sum (option B) or current live turn if any
|
|
68
|
+
if (this.agentStartMs === null) {
|
|
69
|
+
const live = this.peekLive();
|
|
70
|
+
if (live)
|
|
71
|
+
return live;
|
|
72
|
+
return this.lastTelemetry ?? this.lastTurnTelemetry;
|
|
73
|
+
}
|
|
74
|
+
const live = this.peekLive();
|
|
75
|
+
const hasCompleted = this.agentTurns.length > 0;
|
|
76
|
+
if (!hasCompleted && !live)
|
|
77
|
+
return null;
|
|
78
|
+
let inputTokens = 0;
|
|
79
|
+
let outputTokens = 0;
|
|
80
|
+
let totalTokens = 0;
|
|
81
|
+
let costUsd = 0;
|
|
82
|
+
let stallMs = 0;
|
|
83
|
+
let stallCount = 0;
|
|
84
|
+
let generationMs = 0;
|
|
85
|
+
let ttftMs = 0;
|
|
86
|
+
// sum completed turns
|
|
87
|
+
for (const t of this.agentTurns) {
|
|
88
|
+
inputTokens += t.inputTokens;
|
|
89
|
+
outputTokens += t.outputTokens;
|
|
90
|
+
totalTokens += t.totalTokens;
|
|
91
|
+
costUsd += t.costUsd;
|
|
92
|
+
stallMs += t.stallMs;
|
|
93
|
+
stallCount += t.stallCount;
|
|
94
|
+
generationMs += t.generationMs;
|
|
95
|
+
}
|
|
96
|
+
if (this.agentTurns.length > 0)
|
|
97
|
+
ttftMs = this.agentTurns[0].ttftMs;
|
|
98
|
+
if (live) {
|
|
99
|
+
inputTokens += live.inputTokens;
|
|
100
|
+
outputTokens += live.outputTokens;
|
|
101
|
+
totalTokens += live.totalTokens;
|
|
102
|
+
costUsd += live.costUsd;
|
|
103
|
+
stallMs += live.stallMs;
|
|
104
|
+
stallCount += live.stallCount;
|
|
105
|
+
generationMs += live.generationMs;
|
|
106
|
+
if (ttftMs === 0)
|
|
107
|
+
ttftMs = live.ttftMs;
|
|
108
|
+
}
|
|
109
|
+
const now = this.now();
|
|
110
|
+
const totalMs = Math.max(0, now - this.agentStartMs);
|
|
111
|
+
const measurementMs = outputTokens > 0 && generationMs > 0 ? generationMs : null;
|
|
112
|
+
const tps = measurementMs === null
|
|
113
|
+
? null
|
|
114
|
+
: round(outputTokens / (measurementMs / 1000), 1);
|
|
115
|
+
const validCost = Number.isFinite(costUsd) && costUsd > 0;
|
|
116
|
+
const validTokens = Number.isFinite(totalTokens) && totalTokens > 0;
|
|
117
|
+
return {
|
|
118
|
+
tps,
|
|
119
|
+
ttftMs,
|
|
120
|
+
totalMs,
|
|
121
|
+
inputTokens,
|
|
122
|
+
outputTokens,
|
|
123
|
+
stallMs,
|
|
124
|
+
stallCount,
|
|
125
|
+
rateUsdPerMTokens: validCost && validTokens
|
|
126
|
+
? round(costUsd / (totalTokens / 1_000_000), 2)
|
|
127
|
+
: null,
|
|
128
|
+
generationMs,
|
|
129
|
+
totalTokens,
|
|
130
|
+
costUsd: validCost ? costUsd : 0,
|
|
131
|
+
measurementMs,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
61
134
|
/** Live snapshot while a turn is running — for real-time border refresh. Returns null when idle. */
|
|
62
135
|
peekLive() {
|
|
63
136
|
const turn = this.turn;
|
|
@@ -152,11 +225,10 @@ export class TurnTelemetryTracker {
|
|
|
152
225
|
handle(event) {
|
|
153
226
|
switch (event.type) {
|
|
154
227
|
case "agent_start":
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this.agentTurns = [];
|
|
158
|
-
}
|
|
228
|
+
this.agentStartMs = this.now();
|
|
229
|
+
this.agentTurns = [];
|
|
159
230
|
return;
|
|
231
|
+
case "agent_end":
|
|
160
232
|
case "agent_settled":
|
|
161
233
|
return this.endAgent();
|
|
162
234
|
case "turn_start":
|
|
@@ -269,8 +341,10 @@ export class TurnTelemetryTracker {
|
|
|
269
341
|
const telemetry = this.endTurn();
|
|
270
342
|
if (telemetry && this.agentStartMs !== null)
|
|
271
343
|
this.agentTurns.push(telemetry);
|
|
272
|
-
if (telemetry)
|
|
344
|
+
if (telemetry) {
|
|
345
|
+
this.lastTurnTelemetry = telemetry;
|
|
273
346
|
this.lastTelemetry = telemetry;
|
|
347
|
+
}
|
|
274
348
|
return telemetry;
|
|
275
349
|
}
|
|
276
350
|
endTurn() {
|
|
@@ -351,10 +425,11 @@ export class TurnTelemetryTracker {
|
|
|
351
425
|
measurementMs,
|
|
352
426
|
};
|
|
353
427
|
this.lastTelemetry = result;
|
|
428
|
+
// keep lastTurnTelemetry as last turn's per-turn telemetry (live input stays per-turn, not agent total)
|
|
354
429
|
return result;
|
|
355
430
|
}
|
|
356
431
|
}
|
|
357
|
-
function formatTurnDuration(ms) {
|
|
432
|
+
export function formatTurnDuration(ms) {
|
|
358
433
|
if (ms < 60_000) {
|
|
359
434
|
// TTFT fixed width: 2-digit integer + one decimal -> padded 4 + "s" =5, then overall duration field padded to 7 for telemetry totalMs
|
|
360
435
|
// For TTFT we want 5, for duration we want 7 — caller will pad accordingly, so here return 5 for <60s case
|
package/package.json
CHANGED
package/src/color-policy.ts
CHANGED
|
@@ -8,11 +8,19 @@ export function stressColor(value: number, warn = 70, danger = 90): ThemeColor {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function contextUsageColor(pct: number): ThemeColor {
|
|
11
|
-
// 25
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
// 12.5 / 25 / 50 quotas — four urgency tiers, increasingly aggressive as context fills.
|
|
12
|
+
// 0 – 12.5% dim — plenty of headroom, visually quiet.
|
|
13
|
+
// 12.5 – 25% accent — first nudge, noticeable but calm.
|
|
14
|
+
// 25 – 50% warning — half consumed, needs attention.
|
|
15
|
+
// 50 – 100% error — critical, about to run out.
|
|
16
|
+
// Uses theme semantic tokens (dim/accent/warning/error) so the progression
|
|
17
|
+
// respects the active theme and remains legible on light/dark/custom palettes.
|
|
18
|
+
// A fixed hex palette (e.g. grey→sky→amber→red) would be more vivid but
|
|
19
|
+
// would ignore the user's theme and can clash with light backgrounds —
|
|
20
|
+
// semantic tokens keep the "aggressive" ordering while staying theme-coherent.
|
|
21
|
+
if (pct >= 50) return "error";
|
|
22
|
+
if (pct >= 25) return "warning";
|
|
23
|
+
if (pct >= 12.5) return "accent";
|
|
16
24
|
return "dim";
|
|
17
25
|
}
|
|
18
26
|
|
package/src/index.ts
CHANGED
|
@@ -122,8 +122,8 @@ let lastSessionCtx: ExtensionContextLike | null = null;
|
|
|
122
122
|
let extensionPi: unknown = null;
|
|
123
123
|
let wallTimeHistory: string[] = []; // kept for compat, not used for widget
|
|
124
124
|
const timeline = new TranscriptTimeline({
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
getLastSessionCtx: () => lastSessionCtx,
|
|
126
|
+
getTuiRef: () => tuiRef,
|
|
127
127
|
});
|
|
128
128
|
void timeline; // keep import used while now using pi.appendEntry interleaved path
|
|
129
129
|
let currentConfig: ThemeConfig = loadConfig();
|
|
@@ -140,6 +140,7 @@ const REFRESH_MS = 1000;
|
|
|
140
140
|
let liveTickTimer: ReturnType<typeof setInterval> | null = null;
|
|
141
141
|
let footerState: FooterState = createInitialState();
|
|
142
142
|
let agentStartMs: number | null = null;
|
|
143
|
+
let agentBaselineTotals: ReturnType<typeof getUsageTotals> | null = null;
|
|
143
144
|
let currentModelInfo: ModelInfo = {
|
|
144
145
|
provider: "",
|
|
145
146
|
modelId: "unknown",
|
|
@@ -267,28 +268,31 @@ function formatDateTimeWithTimezone(d: Date = new Date()): string {
|
|
|
267
268
|
// en-CA gives YYYY-MM-DD, HH:MM:SS
|
|
268
269
|
return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}:${get("second")} ${tz}`.trim();
|
|
269
270
|
} catch {
|
|
270
|
-
|
|
271
|
+
// SAFETY: best-effort, ignore recoverable error
|
|
271
272
|
return d.toLocaleString();
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
275
|
|
|
275
276
|
function injectTimelineDimLine(
|
|
276
|
-
|
|
277
|
-
|
|
277
|
+
_ctx: ExtensionUIContextLike,
|
|
278
|
+
rawLine: string,
|
|
278
279
|
): void {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
280
|
+
// Use pi.appendEntry so it is interleaved in chat container (not aboveEditor stacked widget)
|
|
281
|
+
try {
|
|
282
|
+
// SAFETY: pi custom entry is TUI-only, not sent to LLM
|
|
283
|
+
(
|
|
284
|
+
extensionPi as unknown as { appendEntry?: (t: string, d: unknown) => void }
|
|
285
|
+
)?.appendEntry?.("timeline", { text: rawLine });
|
|
286
|
+
} catch {
|
|
287
|
+
// SAFETY: best-effort, ignore recoverable error
|
|
288
|
+
// SAFETY: best-effort, ignore recoverable error
|
|
289
|
+
}
|
|
290
|
+
// keep legacy array in sync
|
|
291
|
+
wallTimeHistory = [...wallTimeHistory, rawLine];
|
|
288
292
|
}
|
|
289
293
|
function clearTimelineHistory(_ctx?: ExtensionUIContextLike): void {
|
|
290
|
-
|
|
291
|
-
|
|
294
|
+
// No widget to clear — entries are interleaved and persist with session
|
|
295
|
+
wallTimeHistory = [];
|
|
292
296
|
}
|
|
293
297
|
|
|
294
298
|
/** shift+up/down handler: scroll the detail window one line, clamped. */
|
|
@@ -435,30 +439,36 @@ export default function (pi: ExtensionAPILike): void {
|
|
|
435
439
|
extensionPi = pi;
|
|
436
440
|
let watchTimer: ReturnType<typeof setInterval> | null = null;
|
|
437
441
|
let deferredInstallTimer: ReturnType<typeof setTimeout> | null = null;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
442
|
+
// Timeline now uses pi.appendEntry("timeline") interleaved in chat container — not aboveEditor widget
|
|
443
|
+
// Register renderer for timeline custom entries (dim, left-aligned, interleaved)
|
|
444
|
+
try {
|
|
445
|
+
// SAFETY: pi entry renderer is public API — timeline entries are TUI-only, not sent to LLM
|
|
446
|
+
(
|
|
447
|
+
pi as unknown as { registerEntryRenderer?: (t: string, r: unknown) => void }
|
|
448
|
+
).registerEntryRenderer?.(
|
|
449
|
+
"timeline",
|
|
450
|
+
(entry: unknown, _opts: unknown, theme: unknown) => {
|
|
451
|
+
const data = (entry as { data?: { text?: string } }).data;
|
|
452
|
+
const text = data?.text ?? "";
|
|
453
|
+
const lines = text.split("\n").map((l: string) => {
|
|
454
|
+
try {
|
|
455
|
+
return (theme as { fg: (c: string, s: string) => string }).fg(
|
|
456
|
+
"dim",
|
|
457
|
+
" " + l,
|
|
458
|
+
);
|
|
459
|
+
} catch {
|
|
460
|
+
// SAFETY: best-effort, ignore recoverable error
|
|
461
|
+
return " " + l;
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
// SAFETY: intentional unsafe cast — validated at runtime
|
|
465
|
+
return new Text(lines.join("\n")) as unknown as Component;
|
|
466
|
+
},
|
|
467
|
+
);
|
|
468
|
+
} catch {
|
|
469
|
+
// SAFETY: best-effort, ignore recoverable error
|
|
470
|
+
// SAFETY: best-effort, ignore recoverable error
|
|
471
|
+
}
|
|
462
472
|
let headerCleanupInner: (() => void) | null = null;
|
|
463
473
|
|
|
464
474
|
// Toggle the border glow + model label (off restores pi's stock border).
|
|
@@ -602,7 +612,8 @@ export default function (pi: ExtensionAPILike): void {
|
|
|
602
612
|
|
|
603
613
|
currentModelInfo = modelInfoOf(ctx);
|
|
604
614
|
lastSessionCtx = ctx;
|
|
605
|
-
|
|
615
|
+
agentBaselineTotals = null;
|
|
616
|
+
liveBorder.setAgentBaseline(null);
|
|
606
617
|
// Deferred so we win the single editor slot (see installEditor).
|
|
607
618
|
deferredInstallTimer = setTimeout(() => installEditor(ctx.ui), 0);
|
|
608
619
|
|
|
@@ -703,10 +714,12 @@ export default function (pi: ExtensionAPILike): void {
|
|
|
703
714
|
// before re-evaluating the module on /reload). Any timer that captured this
|
|
704
715
|
// session's ctx must be dead before then — otherwise its next tick hits the
|
|
705
716
|
// stale `ctx.ui` getter and assertActive() throws, crashing the process.
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
717
|
+
pi.on("session_shutdown", () => {
|
|
718
|
+
// timeline entries are custom entries interleaved — no aboveEditor widget to clear
|
|
719
|
+
wallTimeHistory = [];
|
|
709
720
|
agentStartMs = null;
|
|
721
|
+
agentBaselineTotals = null;
|
|
722
|
+
liveBorder.setAgentBaseline(null);
|
|
710
723
|
footerState = {
|
|
711
724
|
...footerState,
|
|
712
725
|
workingSince: undefined,
|
|
@@ -755,9 +768,22 @@ export default function (pi: ExtensionAPILike): void {
|
|
|
755
768
|
liveBorder.render();
|
|
756
769
|
};
|
|
757
770
|
|
|
758
|
-
pi.on("agent_start", (e) => {
|
|
771
|
+
pi.on("agent_start", (e, ctx) => {
|
|
759
772
|
telemetryTracker.handle(e as never);
|
|
760
773
|
runActivityTracker.startRun();
|
|
774
|
+
// Capture baseline totals for per-agent delta (live input 18k not 279k = totals - baseline)
|
|
775
|
+
try {
|
|
776
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
777
|
+
const baselineCtx = (ctx ?? lastSessionCtx) as unknown as Parameters<
|
|
778
|
+
typeof getUsageTotals
|
|
779
|
+
>[0];
|
|
780
|
+
if (baselineCtx?.sessionManager?.getEntries) {
|
|
781
|
+
agentBaselineTotals = getUsageTotals(baselineCtx);
|
|
782
|
+
liveBorder.setAgentBaseline(agentBaselineTotals);
|
|
783
|
+
}
|
|
784
|
+
} catch {
|
|
785
|
+
// SAFETY: best-effort, ignore recoverable error
|
|
786
|
+
}
|
|
761
787
|
// timeline stays permanently between each run — do not hide on start
|
|
762
788
|
agentStartMs = Date.now();
|
|
763
789
|
footerState = {
|
|
@@ -768,6 +794,11 @@ export default function (pi: ExtensionAPILike): void {
|
|
|
768
794
|
startLiveTick();
|
|
769
795
|
refreshAllLive();
|
|
770
796
|
});
|
|
797
|
+
pi.on("agent_end", (e) => {
|
|
798
|
+
// Alias for agent_settled — ensure tracker resets even if only agent_end is emitted
|
|
799
|
+
telemetryTracker.handle(e as never);
|
|
800
|
+
runActivityTracker.settle();
|
|
801
|
+
});
|
|
771
802
|
pi.on("turn_start", (e, ctx) => {
|
|
772
803
|
// Input is known at turn_start via context usage — seed live input so peekLive shows it during streaming (output already streams via liveDeltaChars)
|
|
773
804
|
const usageTokens = (
|
|
@@ -864,10 +895,24 @@ export default function (pi: ExtensionAPILike): void {
|
|
|
864
895
|
const cacheRate = totals.latestCacheHitRate ?? 0;
|
|
865
896
|
const cacheStr = `${glyphs.cacheHit} ${cacheRate.toFixed(1)}%`;
|
|
866
897
|
// Respect timeline.* toggles for specified metrics (wallTime/tokens/cost), but datetime/cache/turn/tools are always shown per user spec
|
|
867
|
-
// Timeline tokens
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
898
|
+
// Timeline tokens per-agent: prefer baseline delta (totals - baseline) which yields 18k (279k-261k) not 279k total.
|
|
899
|
+
// Fallback to tel (tracker sum) then session totals.
|
|
900
|
+
let telInput: number;
|
|
901
|
+
let telOutput: number;
|
|
902
|
+
let telCost: number;
|
|
903
|
+
if (agentBaselineTotals) {
|
|
904
|
+
telInput = Math.max(0, totals.input - agentBaselineTotals.input);
|
|
905
|
+
telOutput = Math.max(0, totals.output - agentBaselineTotals.output);
|
|
906
|
+
telCost = Math.max(0, totals.cost - agentBaselineTotals.cost);
|
|
907
|
+
} else if (tel) {
|
|
908
|
+
telInput = tel.inputTokens;
|
|
909
|
+
telOutput = tel.outputTokens;
|
|
910
|
+
telCost = tel.costUsd;
|
|
911
|
+
} else {
|
|
912
|
+
telInput = totals.input;
|
|
913
|
+
telOutput = totals.output;
|
|
914
|
+
telCost = totals.cost;
|
|
915
|
+
}
|
|
871
916
|
const line1Parts: string[] = [dt];
|
|
872
917
|
if (currentConfig.timeline.wallTime) line1Parts.push(wallDur);
|
|
873
918
|
else line1Parts.push(wallDur); // wall time always per spec (11s)
|
|
@@ -894,9 +939,10 @@ export default function (pi: ExtensionAPILike): void {
|
|
|
894
939
|
wallText,
|
|
895
940
|
);
|
|
896
941
|
}
|
|
897
|
-
} catch {
|
|
898
|
-
|
|
899
|
-
|
|
942
|
+
} catch {
|
|
943
|
+
// SAFETY: best-effort, ignore recoverable error
|
|
944
|
+
// SAFETY: best-effort, ignore recoverable error
|
|
945
|
+
}
|
|
900
946
|
// final settled telemetry overwrites live peek with authoritative totals
|
|
901
947
|
if (tel && installedEditor && currentConfig.telemetry.enabled) {
|
|
902
948
|
try {
|
package/src/live-border.ts
CHANGED
|
@@ -18,7 +18,12 @@ import {
|
|
|
18
18
|
import { resolveGlyphs, resolveIconMode } from "./icons.js";
|
|
19
19
|
import { formatRunActivityTopRight } from "./run-activity.js";
|
|
20
20
|
import type { RunActivityTracker } from "./run-activity.js";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
formatTelemetryTokens,
|
|
23
|
+
formatTurnDuration,
|
|
24
|
+
formatTurnTelemetry,
|
|
25
|
+
} from "./telemetry.js";
|
|
26
|
+
import type { UsageTotals } from "./state.js";
|
|
22
27
|
import type { TurnTelemetry } from "./telemetry.js";
|
|
23
28
|
import type { TurnTelemetryTracker } from "./telemetry.js";
|
|
24
29
|
import type { ThemeConfig } from "./config.js";
|
|
@@ -39,9 +44,15 @@ export class LiveBorder {
|
|
|
39
44
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
40
45
|
private lastRenderMs = 0;
|
|
41
46
|
private pendingRender: ReturnType<typeof setTimeout> | null = null;
|
|
47
|
+
private agentBaseline: UsageTotals | null = null;
|
|
42
48
|
|
|
43
49
|
constructor(private readonly deps: LiveBorderDeps) {}
|
|
44
50
|
|
|
51
|
+
/** Set baseline totals at agent_start for per-agent delta (input/output/cost). */
|
|
52
|
+
setAgentBaseline(baseline: UsageTotals | null): void {
|
|
53
|
+
this.agentBaseline = baseline ? { ...baseline } : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
/** Coalesced render: top (run-activity) + bottom (telemetry) + context bar → editor. */
|
|
46
57
|
render(): void {
|
|
47
58
|
const now = Date.now();
|
|
@@ -113,14 +124,43 @@ export class LiveBorder {
|
|
|
113
124
|
private refreshTopBorder(): void {
|
|
114
125
|
const editor = this.deps.getEditor();
|
|
115
126
|
const ctx = this.deps.getCtx();
|
|
127
|
+
const cfg = this.deps.getConfig();
|
|
116
128
|
if (!editor || !ctx) return;
|
|
117
129
|
try {
|
|
118
|
-
// SAFETY: theme is live pi TUI theme read at render time
|
|
130
|
+
// SAFETY: theme is live pi TUI theme read at render time — intentional unsafe cast, validated at runtime
|
|
119
131
|
const theme = (
|
|
120
132
|
ctx.ui as unknown as { theme: { fg(s: string, t: string): string } }
|
|
121
|
-
).theme; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
133
|
+
).theme; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
122
134
|
const snap = this.deps.runActivityTracker.getSnapshot();
|
|
123
|
-
|
|
135
|
+
let text = formatRunActivityTopRight(snap, theme as never);
|
|
136
|
+
// Relocate stall to the right of tool use with pipe separator — agent-run live (option B), not bottom telemetry
|
|
137
|
+
if (cfg.telemetry.enabled && cfg.telemetry.stalls) {
|
|
138
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — telemetry tracker for stall (agent run, option B)
|
|
139
|
+
const tracker = this.deps.telemetryTracker as unknown as {
|
|
140
|
+
peekAgentLive(): import("./telemetry.js").TurnTelemetry | null;
|
|
141
|
+
getLastTelemetry(): import("./telemetry.js").TurnTelemetry | null;
|
|
142
|
+
};
|
|
143
|
+
const tel = tracker.peekAgentLive() ?? tracker.getLastTelemetry();
|
|
144
|
+
if (tel && tel.stallMs > 0) {
|
|
145
|
+
const glyphs = resolveGlyphs(cfg.icons.mode);
|
|
146
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — theme fg
|
|
147
|
+
const stallText = (
|
|
148
|
+
theme as unknown as { fg: (s: string, t: string) => string }
|
|
149
|
+
).fg(
|
|
150
|
+
"warning",
|
|
151
|
+
`${glyphs.stall}${tel.stallCount}×${formatTurnDuration(tel.stallMs).trim()}`,
|
|
152
|
+
);
|
|
153
|
+
if (text) {
|
|
154
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — theme fg for pipe
|
|
155
|
+
const pipe = (
|
|
156
|
+
theme as unknown as { fg: (s: string, t: string) => string }
|
|
157
|
+
).fg("dim", " | ");
|
|
158
|
+
text = `${text}${pipe}${stallText}`;
|
|
159
|
+
} else {
|
|
160
|
+
text = stallText;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
124
164
|
editor.setTopRightText(text);
|
|
125
165
|
} catch {
|
|
126
166
|
// SAFETY: best-effort UI, ignore recoverable error
|
|
@@ -142,18 +182,20 @@ export class LiveBorder {
|
|
|
142
182
|
return;
|
|
143
183
|
}
|
|
144
184
|
try {
|
|
145
|
-
// SAFETY: peekLive ?? getLastTelemetry preserves cost after toggle (AGENTS.md gotcha)
|
|
185
|
+
// SAFETY: peekLive ?? getLastTelemetry preserves cost after toggle (AGENTS.md gotcha) — intentional unsafe cast, validated at runtime
|
|
146
186
|
const live =
|
|
147
187
|
this.deps.telemetryTracker.peekLive() ??
|
|
148
188
|
this.deps.telemetryTracker.getLastTelemetry();
|
|
149
189
|
if (!live) return;
|
|
150
|
-
// SAFETY: pi
|
|
151
|
-
const theme = (ctx as unknown as { ui?: { theme?: unknown } })?.ui?.theme;
|
|
190
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — theme from extension context
|
|
191
|
+
const theme = (ctx as unknown as { ui?: { theme?: unknown } })?.ui?.theme;
|
|
152
192
|
const glyphs = resolveGlyphs(cfg.icons.mode);
|
|
193
|
+
// Stall relocated to top right of tool use with pipe — suppress in bottom telemetry
|
|
194
|
+
const bottomCfg = { ...cfg.telemetry, stalls: false };
|
|
153
195
|
const right = formatTurnTelemetry(
|
|
154
196
|
live,
|
|
155
197
|
theme as never,
|
|
156
|
-
|
|
198
|
+
bottomCfg as never,
|
|
157
199
|
glyphs as never,
|
|
158
200
|
);
|
|
159
201
|
if (live.totalMs > 0) {
|
|
@@ -202,17 +244,52 @@ export class LiveBorder {
|
|
|
202
244
|
// Tokens line above model info — left aligned, no border; hidden at startup per user request
|
|
203
245
|
let tokensText = "";
|
|
204
246
|
if (cfg.telemetry.enabled && cfg.telemetry.tokens) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (
|
|
247
|
+
const isRunning = this.deps.runActivityTracker.isRunning();
|
|
248
|
+
// When idle (settled) and baseline available, show per-agent delta (input/output/cost) = cur totals - baseline at agent_start.
|
|
249
|
+
// This yields 18k for the example (279k session - 261k baseline = 18k agent) instead of 279k total.
|
|
250
|
+
// When running, use live agent tracker (sum of turns in this agent + live turn) which is already per-agent after guard fix.
|
|
251
|
+
if (!isRunning && this.agentBaseline) {
|
|
252
|
+
const cur = snapshot.totals;
|
|
253
|
+
const base = this.agentBaseline;
|
|
254
|
+
const deltaInput = Math.max(0, cur.input - base.input);
|
|
255
|
+
const deltaOutput = Math.max(0, cur.output - base.output);
|
|
256
|
+
// Build minimal telemetry for formatting (only tokens matter for formatTelemetryTokens)
|
|
257
|
+
const deltaTel: TurnTelemetry = {
|
|
258
|
+
tps: null,
|
|
259
|
+
ttftMs: 0,
|
|
260
|
+
totalMs: 0,
|
|
261
|
+
inputTokens: deltaInput,
|
|
262
|
+
outputTokens: deltaOutput,
|
|
263
|
+
stallMs: 0,
|
|
264
|
+
stallCount: 0,
|
|
265
|
+
rateUsdPerMTokens: null,
|
|
266
|
+
generationMs: 0,
|
|
267
|
+
totalTokens: deltaInput + deltaOutput,
|
|
268
|
+
costUsd: Math.max(0, cur.cost - base.cost),
|
|
269
|
+
measurementMs: null,
|
|
270
|
+
};
|
|
210
271
|
tokensText = formatTelemetryTokens(
|
|
211
|
-
|
|
272
|
+
deltaTel,
|
|
212
273
|
theme as never,
|
|
213
274
|
cfg.telemetry,
|
|
214
275
|
glyphs as never,
|
|
215
276
|
);
|
|
277
|
+
} else {
|
|
278
|
+
// Live or fallback — per-agent sum from tracker (option B), correctly reset per agent_start
|
|
279
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — telemetry tracker for top tokens line (agent run)
|
|
280
|
+
const tracker = this.deps.telemetryTracker as unknown as {
|
|
281
|
+
peekAgentLive(): import("./telemetry.js").TurnTelemetry | null;
|
|
282
|
+
getLastTelemetry(): import("./telemetry.js").TurnTelemetry | null;
|
|
283
|
+
};
|
|
284
|
+
const live = tracker.peekAgentLive() ?? tracker.getLastTelemetry();
|
|
285
|
+
if (live) {
|
|
286
|
+
tokensText = formatTelemetryTokens(
|
|
287
|
+
live,
|
|
288
|
+
theme as never,
|
|
289
|
+
cfg.telemetry,
|
|
290
|
+
glyphs as never,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
216
293
|
}
|
|
217
294
|
}
|
|
218
295
|
editor.setTopContextText(contextText);
|
package/src/telemetry.ts
CHANGED
|
@@ -60,6 +60,7 @@ type AgentMessage = { role: string } & AssistantMessage;
|
|
|
60
60
|
|
|
61
61
|
export type TelemetryEvent =
|
|
62
62
|
| { type: "agent_start" }
|
|
63
|
+
| { type: "agent_end" }
|
|
63
64
|
| { type: "agent_settled" }
|
|
64
65
|
| {
|
|
65
66
|
type: "turn_start";
|
|
@@ -150,9 +151,9 @@ export class TurnTelemetryTracker {
|
|
|
150
151
|
private agentStartMs: number | null = null;
|
|
151
152
|
private agentTurns: TurnTelemetry[] = [];
|
|
152
153
|
private lastTelemetry: TurnTelemetry | null = null;
|
|
154
|
+
private lastTurnTelemetry: TurnTelemetry | null = null;
|
|
153
155
|
private decayBaseTps: number | null = null;
|
|
154
156
|
private decayStartMs: number | null = null;
|
|
155
|
-
|
|
156
157
|
constructor(now: () => number = () => performance.now()) {
|
|
157
158
|
this.now = now;
|
|
158
159
|
}
|
|
@@ -160,6 +161,79 @@ export class TurnTelemetryTracker {
|
|
|
160
161
|
getLastTelemetry(): TurnTelemetry | null {
|
|
161
162
|
return this.lastTelemetry;
|
|
162
163
|
}
|
|
164
|
+
|
|
165
|
+
getLastTurnTelemetry(): TurnTelemetry | null {
|
|
166
|
+
return this.lastTurnTelemetry;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Agent-run live: cumulative across turns in current agent, including current live turn. Option B. */
|
|
170
|
+
peekAgentLive(): TurnTelemetry | null {
|
|
171
|
+
// No agent active -> show last agent sum (option B) or current live turn if any
|
|
172
|
+
if (this.agentStartMs === null) {
|
|
173
|
+
const live = this.peekLive();
|
|
174
|
+
if (live) return live;
|
|
175
|
+
return this.lastTelemetry ?? this.lastTurnTelemetry;
|
|
176
|
+
}
|
|
177
|
+
const live = this.peekLive();
|
|
178
|
+
const hasCompleted = this.agentTurns.length > 0;
|
|
179
|
+
if (!hasCompleted && !live) return null;
|
|
180
|
+
let inputTokens = 0;
|
|
181
|
+
let outputTokens = 0;
|
|
182
|
+
let totalTokens = 0;
|
|
183
|
+
let costUsd = 0;
|
|
184
|
+
let stallMs = 0;
|
|
185
|
+
let stallCount = 0;
|
|
186
|
+
let generationMs = 0;
|
|
187
|
+
let ttftMs = 0;
|
|
188
|
+
// sum completed turns
|
|
189
|
+
for (const t of this.agentTurns) {
|
|
190
|
+
inputTokens += t.inputTokens;
|
|
191
|
+
outputTokens += t.outputTokens;
|
|
192
|
+
totalTokens += t.totalTokens;
|
|
193
|
+
costUsd += t.costUsd;
|
|
194
|
+
stallMs += t.stallMs;
|
|
195
|
+
stallCount += t.stallCount;
|
|
196
|
+
generationMs += t.generationMs;
|
|
197
|
+
}
|
|
198
|
+
if (this.agentTurns.length > 0) ttftMs = this.agentTurns[0]!.ttftMs;
|
|
199
|
+
if (live) {
|
|
200
|
+
inputTokens += live.inputTokens;
|
|
201
|
+
outputTokens += live.outputTokens;
|
|
202
|
+
totalTokens += live.totalTokens;
|
|
203
|
+
costUsd += live.costUsd;
|
|
204
|
+
stallMs += live.stallMs;
|
|
205
|
+
stallCount += live.stallCount;
|
|
206
|
+
generationMs += live.generationMs;
|
|
207
|
+
if (ttftMs === 0) ttftMs = live.ttftMs;
|
|
208
|
+
}
|
|
209
|
+
const now = this.now();
|
|
210
|
+
const totalMs = Math.max(0, now - this.agentStartMs);
|
|
211
|
+
const measurementMs =
|
|
212
|
+
outputTokens > 0 && generationMs > 0 ? generationMs : null;
|
|
213
|
+
const tps =
|
|
214
|
+
measurementMs === null
|
|
215
|
+
? null
|
|
216
|
+
: round(outputTokens / (measurementMs / 1000), 1);
|
|
217
|
+
const validCost = Number.isFinite(costUsd) && costUsd > 0;
|
|
218
|
+
const validTokens = Number.isFinite(totalTokens) && totalTokens > 0;
|
|
219
|
+
return {
|
|
220
|
+
tps,
|
|
221
|
+
ttftMs,
|
|
222
|
+
totalMs,
|
|
223
|
+
inputTokens,
|
|
224
|
+
outputTokens,
|
|
225
|
+
stallMs,
|
|
226
|
+
stallCount,
|
|
227
|
+
rateUsdPerMTokens:
|
|
228
|
+
validCost && validTokens
|
|
229
|
+
? round(costUsd / (totalTokens / 1_000_000), 2)
|
|
230
|
+
: null,
|
|
231
|
+
generationMs,
|
|
232
|
+
totalTokens,
|
|
233
|
+
costUsd: validCost ? costUsd : 0,
|
|
234
|
+
measurementMs,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
163
237
|
/** Live snapshot while a turn is running — for real-time border refresh. Returns null when idle. */
|
|
164
238
|
peekLive(): TurnTelemetry | null {
|
|
165
239
|
const turn = this.turn;
|
|
@@ -256,11 +330,10 @@ export class TurnTelemetryTracker {
|
|
|
256
330
|
handle(event: TelemetryEvent): TurnTelemetry | undefined {
|
|
257
331
|
switch (event.type) {
|
|
258
332
|
case "agent_start":
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
this.agentTurns = [];
|
|
262
|
-
}
|
|
333
|
+
this.agentStartMs = this.now();
|
|
334
|
+
this.agentTurns = [];
|
|
263
335
|
return;
|
|
336
|
+
case "agent_end":
|
|
264
337
|
case "agent_settled":
|
|
265
338
|
return this.endAgent();
|
|
266
339
|
case "turn_start":
|
|
@@ -386,7 +459,10 @@ export class TurnTelemetryTracker {
|
|
|
386
459
|
const telemetry = this.endTurn();
|
|
387
460
|
if (telemetry && this.agentStartMs !== null)
|
|
388
461
|
this.agentTurns.push(telemetry);
|
|
389
|
-
if (telemetry)
|
|
462
|
+
if (telemetry) {
|
|
463
|
+
this.lastTurnTelemetry = telemetry;
|
|
464
|
+
this.lastTelemetry = telemetry;
|
|
465
|
+
}
|
|
390
466
|
return telemetry;
|
|
391
467
|
}
|
|
392
468
|
|
|
@@ -478,11 +554,12 @@ export class TurnTelemetryTracker {
|
|
|
478
554
|
measurementMs,
|
|
479
555
|
};
|
|
480
556
|
this.lastTelemetry = result;
|
|
557
|
+
// keep lastTurnTelemetry as last turn's per-turn telemetry (live input stays per-turn, not agent total)
|
|
481
558
|
return result;
|
|
482
559
|
}
|
|
483
560
|
}
|
|
484
561
|
|
|
485
|
-
function formatTurnDuration(ms: number): string {
|
|
562
|
+
export function formatTurnDuration(ms: number): string {
|
|
486
563
|
if (ms < 60_000) {
|
|
487
564
|
// TTFT fixed width: 2-digit integer + one decimal -> padded 4 + "s" =5, then overall duration field padded to 7 for telemetry totalMs
|
|
488
565
|
// For TTFT we want 5, for duration we want 7 — caller will pad accordingly, so here return 5 for <60s case
|