pi-editor-footer 0.6.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 +6 -0
- package/dist/index.js +49 -8
- package/dist/live-border.js +39 -6
- package/dist/telemetry.js +3 -4
- package/package.json +1 -2
- package/src/index.ts +98 -52
- package/src/live-border.ts +48 -10
- package/src/telemetry.ts +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ 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
|
+
|
|
7
13
|
## [0.6.0] - 2026-08-24
|
|
8
14
|
|
|
9
15
|
### Changed
|
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
|
@@ -20,9 +20,14 @@ export class LiveBorder {
|
|
|
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();
|
|
@@ -191,12 +196,40 @@ export class LiveBorder {
|
|
|
191
196
|
// Tokens line above model info — left aligned, no border; hidden at startup per user request
|
|
192
197
|
let tokensText = "";
|
|
193
198
|
if (cfg.telemetry.enabled && cfg.telemetry.tokens) {
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (
|
|
199
|
-
|
|
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
|
+
}
|
|
200
233
|
}
|
|
201
234
|
}
|
|
202
235
|
editor.setTopContextText(contextText);
|
package/dist/telemetry.js
CHANGED
|
@@ -225,11 +225,10 @@ export class TurnTelemetryTracker {
|
|
|
225
225
|
handle(event) {
|
|
226
226
|
switch (event.type) {
|
|
227
227
|
case "agent_start":
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this.agentTurns = [];
|
|
231
|
-
}
|
|
228
|
+
this.agentStartMs = this.now();
|
|
229
|
+
this.agentTurns = [];
|
|
232
230
|
return;
|
|
231
|
+
case "agent_end":
|
|
233
232
|
case "agent_settled":
|
|
234
233
|
return this.endAgent();
|
|
235
234
|
case "turn_start":
|
package/package.json
CHANGED
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
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
formatTurnDuration,
|
|
24
24
|
formatTurnTelemetry,
|
|
25
25
|
} from "./telemetry.js";
|
|
26
|
+
import type { UsageTotals } from "./state.js";
|
|
26
27
|
import type { TurnTelemetry } from "./telemetry.js";
|
|
27
28
|
import type { TurnTelemetryTracker } from "./telemetry.js";
|
|
28
29
|
import type { ThemeConfig } from "./config.js";
|
|
@@ -43,9 +44,15 @@ export class LiveBorder {
|
|
|
43
44
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
44
45
|
private lastRenderMs = 0;
|
|
45
46
|
private pendingRender: ReturnType<typeof setTimeout> | null = null;
|
|
47
|
+
private agentBaseline: UsageTotals | null = null;
|
|
46
48
|
|
|
47
49
|
constructor(private readonly deps: LiveBorderDeps) {}
|
|
48
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
|
+
|
|
49
56
|
/** Coalesced render: top (run-activity) + bottom (telemetry) + context bar → editor. */
|
|
50
57
|
render(): void {
|
|
51
58
|
const now = Date.now();
|
|
@@ -237,21 +244,52 @@ export class LiveBorder {
|
|
|
237
244
|
// Tokens line above model info — left aligned, no border; hidden at startup per user request
|
|
238
245
|
let tokensText = "";
|
|
239
246
|
if (cfg.telemetry.enabled && cfg.telemetry.tokens) {
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
};
|
|
249
271
|
tokensText = formatTelemetryTokens(
|
|
250
|
-
|
|
272
|
+
deltaTel,
|
|
251
273
|
theme as never,
|
|
252
274
|
cfg.telemetry,
|
|
253
275
|
glyphs as never,
|
|
254
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
|
+
}
|
|
255
293
|
}
|
|
256
294
|
}
|
|
257
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";
|
|
@@ -329,11 +330,10 @@ export class TurnTelemetryTracker {
|
|
|
329
330
|
handle(event: TelemetryEvent): TurnTelemetry | undefined {
|
|
330
331
|
switch (event.type) {
|
|
331
332
|
case "agent_start":
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.agentTurns = [];
|
|
335
|
-
}
|
|
333
|
+
this.agentStartMs = this.now();
|
|
334
|
+
this.agentTurns = [];
|
|
336
335
|
return;
|
|
336
|
+
case "agent_end":
|
|
337
337
|
case "agent_settled":
|
|
338
338
|
return this.endAgent();
|
|
339
339
|
case "turn_start":
|