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 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 { // SAFETY: best-effort, ignore recoverable error
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 { // SAFETY: best-effort, ignore recoverable error
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 now mirror telemetry (per-agent) not session totals input known at turn_start via liveInputTokens
695
- const telInput = tel ? tel.inputTokens : totals.input;
696
- const telOutput = tel ? tel.outputTokens : totals.output;
697
- const telCost = tel ? tel.costUsd : totals.cost;
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 { // SAFETY: best-effort, ignore recoverable error
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
@@ -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
- // Live input/output per agent run (option B) — cumulative across turns in this agent, like live output
195
- // SAFETY: pi seam intentional unsafe cast, validated at runtime telemetry tracker for top tokens line (agent run)
196
- const tracker = this.deps.telemetryTracker;
197
- const live = tracker.peekAgentLive() ?? tracker.getLastTelemetry();
198
- if (live) {
199
- tokensText = formatTelemetryTokens(live, theme, cfg.telemetry, glyphs);
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
- if (this.agentStartMs === null) {
229
- this.agentStartMs = this.now();
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
@@ -18,8 +18,7 @@
18
18
  "@earendil-works/pi-coding-agent": "*",
19
19
  "@earendil-works/pi-tui": "*"
20
20
  },
21
- "version": "0.6.0",
22
- "main": "./dist/index.js",
21
+ "version": "0.6.1",
23
22
  "files": [
24
23
  "dist",
25
24
  "src",
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
- getLastSessionCtx: () => lastSessionCtx,
126
- getTuiRef: () => tuiRef,
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
- // SAFETY: best-effort, ignore recoverable error
271
+ // SAFETY: best-effort, ignore recoverable error
271
272
  return d.toLocaleString();
272
273
  }
273
274
  }
274
275
 
275
276
  function injectTimelineDimLine(
276
- _ctx: ExtensionUIContextLike,
277
- rawLine: string,
277
+ _ctx: ExtensionUIContextLike,
278
+ rawLine: string,
278
279
  ): void {
279
- // Use pi.appendEntry so it is interleaved in chat container (not aboveEditor stacked widget)
280
- try {
281
- // SAFETY: pi custom entry is TUI-only, not sent to LLM
282
- (extensionPi as unknown as { appendEntry?: (t:string,d:unknown)=>void })?.appendEntry?.("timeline", { text: rawLine });
283
- } catch {// SAFETY: best-effort, ignore recoverable error
284
- // SAFETY: best-effort, ignore recoverable error
285
- }
286
- // keep legacy array in sync
287
- wallTimeHistory = [...wallTimeHistory, rawLine];
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
- // No widget to clear — entries are interleaved and persist with session
291
- wallTimeHistory = [];
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
- // Timeline now uses pi.appendEntry("timeline") interleaved in chat container — not aboveEditor widget
439
- // Register renderer for timeline custom entries (dim, left-aligned, interleaved)
440
- try {
441
- // SAFETY: pi entry renderer is public API — timeline entries are TUI-only, not sent to LLM
442
- (pi as unknown as { registerEntryRenderer?: (t:string, r:unknown)=>void }).registerEntryRenderer?.(
443
- "timeline",
444
- (entry: unknown, _opts: unknown, theme: unknown) => {
445
- const data = (entry as { data?: { text?: string } }).data;
446
- const text = data?.text ?? "";
447
- const lines = text.split("\n").map((l: string) => {
448
- try {
449
- return (theme as { fg: (c:string,s:string)=>string }).fg("dim", " " + l);
450
- } catch {
451
- // SAFETY: best-effort, ignore recoverable error
452
- return " " + l;
453
- }
454
- });
455
- // SAFETY: intentional unsafe cast — validated at runtime
456
- return new Text(lines.join("\n")) as unknown as Component;
457
- },
458
- );
459
- } catch {// SAFETY: best-effort, ignore recoverable error
460
- // SAFETY: best-effort, ignore recoverable error
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
- pi.on("session_shutdown", () => {
707
- // timeline entries are custom entries interleaved — no aboveEditor widget to clear
708
- wallTimeHistory = [];
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 now mirror telemetry (per-agent) not session totals input known at turn_start via liveInputTokens
868
- const telInput = tel ? tel.inputTokens : totals.input;
869
- const telOutput = tel ? tel.outputTokens : totals.output;
870
- const telCost = tel ? tel.costUsd : totals.cost;
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 {// SAFETY: best-effort, ignore recoverable error
898
- // SAFETY: best-effort, ignore recoverable error
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 {
@@ -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
- // Live input/output per agent run (option B) — cumulative across turns in this agent, like live output
241
- // SAFETY: pi seam intentional unsafe cast, validated at runtime telemetry tracker for top tokens line (agent run)
242
- const tracker = this.deps.telemetryTracker as unknown as {
243
- // SAFETY: pi seam intentional unsafe cast, validated at runtime
244
- peekAgentLive(): import("./telemetry.js").TurnTelemetry | null;
245
- getLastTelemetry(): import("./telemetry.js").TurnTelemetry | null;
246
- };
247
- const live = tracker.peekAgentLive() ?? tracker.getLastTelemetry();
248
- if (live) {
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
- live,
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
- if (this.agentStartMs === null) {
333
- this.agentStartMs = this.now();
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":