pi-editor-footer 0.4.0 → 0.5.0

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,19 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.0] - 2026-08-22
8
+
9
+ ### Changed
10
+
11
+ - Remove duration section positioned to the right of `TTFT` in bottom telemetry (`telemetry:formatTurnTelemetry` now `TPS · TTFT` only, `duration` config ignored)
12
+ - Use `<number> turns` to replace `T<number>` in top run-activity (`run-activity:formatRunActivityTopRight` now `1 turns` / `N turns` instead of `T1`/`T N`)
13
+ - Exchange positions of cache rate section (`c %`) and context bar section (`pct · tokens/window` + bar) in top border — now `c % | pct · tokens/window` instead of `pct · tokens/window | c %` (`chrome-state:formatContextBar`)
14
+
15
+ ### Fixed
16
+
17
+ - Clamp live TPS window and throttle `LiveBorder` for multi-turn runs
18
+ - Add inline `SAFETY` for live-border casts (pi-lens)
19
+
7
20
  ## [0.4.0] - 2026-08-22
8
21
 
9
22
  ### Added
@@ -49,7 +49,7 @@ export function formatContextBar(contextUsage, theme, glyphs, isAscii, barWidth
49
49
  ? cacheHitRate
50
50
  : 0;
51
51
  const cacheText = `${glyphs.cacheHit} ${rate.toFixed(1)}%`;
52
- return `${base} ${theme.fg("dim", "|")} ${theme.fg(cacheHitColor(rate), cacheText)}`;
52
+ return `${theme.fg(cacheHitColor(rate), cacheText)} ${theme.fg("dim", "|")} ${base}`;
53
53
  }
54
54
  /** Border adapter helper — one call from snapshot + theme/glyphs. */
55
55
  export function formatTopContextFromSnapshot(snapshot, theme, glyphs, isAscii, showIconBar = false) {
@@ -65,7 +65,7 @@ export function formatTopContextFromSnapshot(snapshot, theme, glyphs, isAscii, s
65
65
  */
66
66
  export function createChromeSnapshot(ctx, footerState) {
67
67
  const cwd = ctx?.sessionManager?.getCwd?.() ??
68
- // SAFETY: pi seam
68
+ // SAFETY: pi seam — intentional unsafe cast, validated at runtime
69
69
  ctx?.cwd ?? // SAFETY: pi seam — intentional unsafe cast, validated at runtime
70
70
  process.cwd();
71
71
  const sessionName = ctx?.sessionManager?.getSessionName?.();
@@ -18,11 +18,33 @@ export const REFRESH_MS = 1000;
18
18
  export class LiveBorder {
19
19
  deps;
20
20
  timer = null;
21
+ lastRenderMs = 0;
22
+ pendingRender = null;
21
23
  constructor(deps) {
22
24
  this.deps = deps;
23
25
  }
24
26
  /** Coalesced render: top (run-activity) + bottom (telemetry) + context bar → editor. */
25
27
  render() {
28
+ const now = Date.now();
29
+ if (now - this.lastRenderMs < REFRESH_MS) {
30
+ if (this.pendingRender === null) {
31
+ const delay = REFRESH_MS - (now - this.lastRenderMs);
32
+ this.pendingRender = setTimeout(() => {
33
+ this.pendingRender = null;
34
+ this.lastRenderMs = Date.now();
35
+ this.doRender();
36
+ }, delay);
37
+ // SAFETY: pi TUI seam - timer unref is optional NodeJS API
38
+ const t = this.pendingRender; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
39
+ if (typeof t.unref === "function")
40
+ t.unref();
41
+ }
42
+ return;
43
+ }
44
+ this.lastRenderMs = now;
45
+ this.doRender();
46
+ }
47
+ doRender() {
26
48
  this.refreshTopBorder();
27
49
  this.refreshLiveTelemetry();
28
50
  this.refreshContextBar();
@@ -37,9 +59,7 @@ export class LiveBorder {
37
59
  this.timer = setInterval(() => {
38
60
  try {
39
61
  if (this.deps.runActivityTracker.isRunning()) {
40
- this.refreshTopBorder();
41
- this.refreshLiveTelemetry();
42
- this.refreshContextBar();
62
+ this.render();
43
63
  }
44
64
  else {
45
65
  this.refreshContextBar();
@@ -51,11 +71,15 @@ export class LiveBorder {
51
71
  }, REFRESH_MS);
52
72
  // don't block process exit
53
73
  // SAFETY: pi TUI seam - timer unref is optional NodeJS API
54
- const t = this.timer;
74
+ const t = this.timer; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
55
75
  if (typeof t.unref === "function")
56
76
  t.unref();
57
77
  }
58
78
  stopTick() {
79
+ if (this.pendingRender !== null) {
80
+ clearTimeout(this.pendingRender);
81
+ this.pendingRender = null;
82
+ }
59
83
  if (this.timer !== null) {
60
84
  clearInterval(this.timer);
61
85
  this.timer = null;
@@ -72,7 +96,7 @@ export class LiveBorder {
72
96
  return;
73
97
  try {
74
98
  // SAFETY: theme is live pi TUI theme read at render time
75
- const theme = ctx.ui.theme; // SAFETY: pi seam
99
+ const theme = ctx.ui.theme; // SAFETY: pi seam — intentional unsafe cast, validated at runtime // SAFETY: pi seam
76
100
  const snap = this.deps.runActivityTracker.getSnapshot();
77
101
  const text = formatRunActivityTopRight(snap, theme);
78
102
  editor.setTopRightText(text);
@@ -104,7 +128,7 @@ export class LiveBorder {
104
128
  if (!live)
105
129
  return;
106
130
  // SAFETY: pi TUI seam read-only - theme from extension context
107
- const theme = ctx?.ui?.theme;
131
+ const theme = ctx?.ui?.theme; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
108
132
  const glyphs = resolveGlyphs(cfg.icons.mode);
109
133
  const right = formatTurnTelemetry(live, theme, cfg.telemetry, glyphs);
110
134
  if (live.totalMs > 0) {
@@ -126,10 +150,11 @@ export class LiveBorder {
126
150
  // Deepened via ChromeState: snapshot owns contextUsage + totals derivation behind one seam.
127
151
  // Two adapters (footer + border) now share the same snapshot — proves the seam.
128
152
  // SAFETY: pi seam - snapshot derivation from extension context
129
- const snapshot = createChromeSnapshot(ctx, undefined);
153
+ const snapshot = createChromeSnapshot(ctx, // SAFETY: pi seam — intentional unsafe cast, validated at runtime
154
+ undefined);
130
155
  // SAFETY: pi seam - theme from extension context
131
- const theme = ctx.ui
132
- .theme;
156
+ const theme = ctx.ui // SAFETY: pi seam — intentional unsafe cast, validated at runtime
157
+ .theme; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
133
158
  const glyphs = resolveGlyphs(cfg.icons.mode);
134
159
  const isAscii = resolveIconMode(cfg.icons.mode) === "ascii";
135
160
  let contextText = "";
@@ -138,7 +163,7 @@ export class LiveBorder {
138
163
  snapshot.contextUsage.contextWindow) {
139
164
  contextText = formatTopContextFromSnapshot(snapshot, theme, glyphs, isAscii,
140
165
  // SAFETY: intentional unsafe cast — validated at runtime
141
- cfg.contextIconBar ??
166
+ cfg.contextIconBar ?? // SAFETY: pi seam — intentional unsafe cast, validated at runtime
142
167
  false);
143
168
  }
144
169
  // Tokens line above model info — left aligned, no border; hidden at startup per user request
@@ -142,10 +142,10 @@ export function formatRunActivityTopRight(snap, theme, now) {
142
142
  const parts = [];
143
143
  // turn
144
144
  if (snap.turnNumber !== undefined) {
145
- parts.push(theme.fg("accent", `T${snap.turnNumber}`));
145
+ parts.push(theme.fg("accent", `${snap.turnNumber} turns`));
146
146
  }
147
147
  else if (snap.phase === "running") {
148
- parts.push(theme.fg("accent", `T1`));
148
+ parts.push(theme.fg("accent", `1 turns`));
149
149
  }
150
150
  // duration
151
151
  const dur = snap.durationMs ??
package/dist/telemetry.js CHANGED
@@ -125,7 +125,8 @@ export class TurnTelemetryTracker {
125
125
  outputTokens = turn.liveEstimatedTokens;
126
126
  totalTokens = Math.max(totalTokens, inputTokens + outputTokens);
127
127
  }
128
- const measurementMs = genMs > 0 && outputTokens > 0 ? genMs : null;
128
+ // Require minimum window to avoid spike on tiny genMs (multi-turn fast second turn)
129
+ const measurementMs = genMs >= 500 && outputTokens > 0 ? genMs : null;
129
130
  const tps = measurementMs === null
130
131
  ? null
131
132
  : round(outputTokens / (measurementMs / 1000), 1);
@@ -382,12 +383,6 @@ export function formatTurnTelemetry(telemetry, theme, config, glyphs) {
382
383
  const padded = sec.padStart(4, " ");
383
384
  parts.push(theme.fg("text", `${padded}s TTFT`));
384
385
  }
385
- if (config.duration) {
386
- // duration fixed width 7 (e.g. " 15m 31s" / " 24h 21m" / " 5.0s")
387
- const raw = formatTurnDuration(telemetry.totalMs);
388
- const padded = raw.padStart(7, " ");
389
- parts.push(theme.fg("success", `${padded}`));
390
- }
391
386
  if (config.stalls && telemetry.stallMs > 0) {
392
387
  parts.push(theme.fg("warning", `${g.stall}${telemetry.stallCount}×${formatTurnDuration(telemetry.stallMs).trim()}`));
393
388
  }
package/package.json CHANGED
@@ -18,7 +18,7 @@
18
18
  "@earendil-works/pi-coding-agent": "*",
19
19
  "@earendil-works/pi-tui": "*"
20
20
  },
21
- "version": "0.4.0",
21
+ "version": "0.5.0",
22
22
  "main": "./dist/index.js",
23
23
  "files": [
24
24
  "dist",
@@ -91,7 +91,7 @@ export function formatContextBar(
91
91
  ? cacheHitRate
92
92
  : 0;
93
93
  const cacheText = `${glyphs.cacheHit} ${rate.toFixed(1)}%`;
94
- return `${base} ${theme.fg("dim", "|")} ${theme.fg(cacheHitColor(rate), cacheText)}`;
94
+ return `${theme.fg(cacheHitColor(rate), cacheText)} ${theme.fg("dim", "|")} ${base}`;
95
95
  }
96
96
 
97
97
  /** Border adapter helper — one call from snapshot + theme/glyphs. */
@@ -139,7 +139,7 @@ export function createChromeSnapshot(
139
139
  ): ChromeSnapshot {
140
140
  const cwd =
141
141
  ctx?.sessionManager?.getCwd?.() ??
142
- // SAFETY: pi seam
142
+ // SAFETY: pi seam — intentional unsafe cast, validated at runtime
143
143
  (ctx as unknown as { cwd?: string })?.cwd ?? // SAFETY: pi seam — intentional unsafe cast, validated at runtime
144
144
  process.cwd();
145
145
  const sessionName = ctx?.sessionManager?.getSessionName?.();
@@ -37,11 +37,33 @@ export interface LiveBorderDeps {
37
37
 
38
38
  export class LiveBorder {
39
39
  private timer: ReturnType<typeof setInterval> | null = null;
40
+ private lastRenderMs = 0;
41
+ private pendingRender: ReturnType<typeof setTimeout> | null = null;
40
42
 
41
43
  constructor(private readonly deps: LiveBorderDeps) {}
42
44
 
43
45
  /** Coalesced render: top (run-activity) + bottom (telemetry) + context bar → editor. */
44
46
  render(): void {
47
+ const now = Date.now();
48
+ if (now - this.lastRenderMs < REFRESH_MS) {
49
+ if (this.pendingRender === null) {
50
+ const delay = REFRESH_MS - (now - this.lastRenderMs);
51
+ this.pendingRender = setTimeout(() => {
52
+ this.pendingRender = null;
53
+ this.lastRenderMs = Date.now();
54
+ this.doRender();
55
+ }, delay);
56
+ // SAFETY: pi TUI seam - timer unref is optional NodeJS API
57
+ const t = this.pendingRender as unknown as { unref?: () => void }; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
58
+ if (typeof t.unref === "function") t.unref();
59
+ }
60
+ return;
61
+ }
62
+ this.lastRenderMs = now;
63
+ this.doRender();
64
+ }
65
+
66
+ private doRender(): void {
45
67
  this.refreshTopBorder();
46
68
  this.refreshLiveTelemetry();
47
69
  this.refreshContextBar();
@@ -57,9 +79,7 @@ export class LiveBorder {
57
79
  this.timer = setInterval(() => {
58
80
  try {
59
81
  if (this.deps.runActivityTracker.isRunning()) {
60
- this.refreshTopBorder();
61
- this.refreshLiveTelemetry();
62
- this.refreshContextBar();
82
+ this.render();
63
83
  } else {
64
84
  this.refreshContextBar();
65
85
  }
@@ -69,11 +89,15 @@ export class LiveBorder {
69
89
  }, REFRESH_MS);
70
90
  // don't block process exit
71
91
  // SAFETY: pi TUI seam - timer unref is optional NodeJS API
72
- const t = this.timer as unknown as { unref?: () => void };
92
+ const t = this.timer as unknown as { unref?: () => void }; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
73
93
  if (typeof t.unref === "function") t.unref();
74
94
  }
75
95
 
76
96
  stopTick(): void {
97
+ if (this.pendingRender !== null) {
98
+ clearTimeout(this.pendingRender);
99
+ this.pendingRender = null;
100
+ }
77
101
  if (this.timer !== null) {
78
102
  clearInterval(this.timer);
79
103
  this.timer = null;
@@ -94,7 +118,7 @@ export class LiveBorder {
94
118
  // SAFETY: theme is live pi TUI theme read at render time
95
119
  const theme = (
96
120
  ctx.ui as unknown as { theme: { fg(s: string, t: string): string } }
97
- ).theme; // SAFETY: pi seam
121
+ ).theme; // SAFETY: pi seam — intentional unsafe cast, validated at runtime // SAFETY: pi seam
98
122
  const snap = this.deps.runActivityTracker.getSnapshot();
99
123
  const text = formatRunActivityTopRight(snap, theme as never);
100
124
  editor.setTopRightText(text);
@@ -124,7 +148,7 @@ export class LiveBorder {
124
148
  this.deps.telemetryTracker.getLastTelemetry();
125
149
  if (!live) return;
126
150
  // SAFETY: pi TUI seam read-only - theme from extension context
127
- const theme = (ctx as unknown as { ui?: { theme?: unknown } })?.ui?.theme;
151
+ const theme = (ctx as unknown as { ui?: { theme?: unknown } })?.ui?.theme; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
128
152
  const glyphs = resolveGlyphs(cfg.icons.mode);
129
153
  const right = formatTurnTelemetry(
130
154
  live,
@@ -151,12 +175,12 @@ export class LiveBorder {
151
175
  // Two adapters (footer + border) now share the same snapshot — proves the seam.
152
176
  // SAFETY: pi seam - snapshot derivation from extension context
153
177
  const snapshot = createChromeSnapshot(
154
- ctx as unknown as Parameters<typeof createChromeSnapshot>[0],
178
+ ctx as unknown as Parameters<typeof createChromeSnapshot>[0], // SAFETY: pi seam — intentional unsafe cast, validated at runtime
155
179
  undefined,
156
180
  );
157
181
  // SAFETY: pi seam - theme from extension context
158
- const theme = (ctx as unknown as { ui: { theme: unknown } }).ui
159
- .theme as unknown as never;
182
+ const theme = (ctx as unknown as { ui: { theme: unknown } }).ui // SAFETY: pi seam — intentional unsafe cast, validated at runtime
183
+ .theme as unknown as never; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
160
184
  const glyphs = resolveGlyphs(cfg.icons.mode);
161
185
  const isAscii = resolveIconMode(cfg.icons.mode) === "ascii";
162
186
  let contextText = "";
@@ -171,7 +195,7 @@ export class LiveBorder {
171
195
  glyphs,
172
196
  isAscii,
173
197
  // SAFETY: intentional unsafe cast — validated at runtime
174
- (cfg as unknown as { contextIconBar?: boolean }).contextIconBar ??
198
+ (cfg as unknown as { contextIconBar?: boolean }).contextIconBar ?? // SAFETY: pi seam — intentional unsafe cast, validated at runtime
175
199
  false,
176
200
  );
177
201
  }
@@ -186,9 +186,9 @@ export function formatRunActivityTopRight(
186
186
 
187
187
  // turn
188
188
  if (snap.turnNumber !== undefined) {
189
- parts.push(theme.fg("accent", `T${snap.turnNumber}`));
189
+ parts.push(theme.fg("accent", `${snap.turnNumber} turns`));
190
190
  } else if (snap.phase === "running") {
191
- parts.push(theme.fg("accent", `T1`));
191
+ parts.push(theme.fg("accent", `1 turns`));
192
192
  }
193
193
 
194
194
  // duration
package/src/telemetry.ts CHANGED
@@ -176,7 +176,8 @@ export class TurnTelemetryTracker {
176
176
  decayedTps = null;
177
177
  } else {
178
178
  // exponential decay, half-life ~5s (exp(-elapsed/7200) => half at ~5s ln2*7200≈5000)
179
- const decayed = this.decayBaseTps * Math.exp(-elapsedSinceDecay / 7200);
179
+ const decayed =
180
+ this.decayBaseTps * Math.exp(-elapsedSinceDecay / 7200);
180
181
  if (decayed >= 0.05) decayedTps = round(decayed, 1);
181
182
  }
182
183
  }
@@ -225,7 +226,8 @@ export class TurnTelemetryTracker {
225
226
  outputTokens = turn.liveEstimatedTokens;
226
227
  totalTokens = Math.max(totalTokens, inputTokens + outputTokens);
227
228
  }
228
- const measurementMs = genMs > 0 && outputTokens > 0 ? genMs : null;
229
+ // Require minimum window to avoid spike on tiny genMs (multi-turn fast second turn)
230
+ const measurementMs = genMs >= 500 && outputTokens > 0 ? genMs : null;
229
231
  const tps =
230
232
  measurementMs === null
231
233
  ? null
@@ -533,12 +535,6 @@ export function formatTurnTelemetry(
533
535
  const padded = sec.padStart(4, " ");
534
536
  parts.push(theme.fg("text", `${padded}s TTFT`));
535
537
  }
536
- if (config.duration) {
537
- // duration fixed width 7 (e.g. " 15m 31s" / " 24h 21m" / " 5.0s")
538
- const raw = formatTurnDuration(telemetry.totalMs);
539
- const padded = raw.padStart(7, " ");
540
- parts.push(theme.fg("success", `${padded}`));
541
- }
542
538
  if (config.stalls && telemetry.stallMs > 0) {
543
539
  parts.push(
544
540
  theme.fg(