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 +13 -0
- package/dist/chrome-state.js +2 -2
- package/dist/live-border.js +35 -10
- package/dist/run-activity.js +2 -2
- package/dist/telemetry.js +2 -7
- package/package.json +1 -1
- package/src/chrome-state.ts +2 -2
- package/src/live-border.ts +34 -10
- package/src/run-activity.ts +2 -2
- package/src/telemetry.ts +4 -8
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
|
package/dist/chrome-state.js
CHANGED
|
@@ -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 `${
|
|
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?.();
|
package/dist/live-border.js
CHANGED
|
@@ -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.
|
|
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,
|
|
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
|
package/dist/run-activity.js
CHANGED
|
@@ -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",
|
|
145
|
+
parts.push(theme.fg("accent", `${snap.turnNumber} turns`));
|
|
146
146
|
}
|
|
147
147
|
else if (snap.phase === "running") {
|
|
148
|
-
parts.push(theme.fg("accent", `
|
|
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
|
-
|
|
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
package/src/chrome-state.ts
CHANGED
|
@@ -91,7 +91,7 @@ export function formatContextBar(
|
|
|
91
91
|
? cacheHitRate
|
|
92
92
|
: 0;
|
|
93
93
|
const cacheText = `${glyphs.cacheHit} ${rate.toFixed(1)}%`;
|
|
94
|
-
return `${
|
|
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?.();
|
package/src/live-border.ts
CHANGED
|
@@ -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.
|
|
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
|
}
|
package/src/run-activity.ts
CHANGED
|
@@ -186,9 +186,9 @@ export function formatRunActivityTopRight(
|
|
|
186
186
|
|
|
187
187
|
// turn
|
|
188
188
|
if (snap.turnNumber !== undefined) {
|
|
189
|
-
parts.push(theme.fg("accent",
|
|
189
|
+
parts.push(theme.fg("accent", `${snap.turnNumber} turns`));
|
|
190
190
|
} else if (snap.phase === "running") {
|
|
191
|
-
parts.push(theme.fg("accent", `
|
|
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 =
|
|
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
|
-
|
|
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(
|