pi-editor-footer 0.11.0 → 0.12.2
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 +26 -2
- package/dist/agent-run-ledger.js +9 -27
- package/dist/ansi-color.js +142 -0
- package/dist/border-renderer.js +9 -19
- package/dist/chrome-composition.js +9 -4
- package/dist/chrome-state.js +22 -13
- package/dist/color-policy.js +31 -18
- package/dist/config.js +1 -3
- package/dist/cursor-policy.js +3 -5
- package/dist/detail-chrome.js +45 -1
- package/dist/detail-render.js +20 -8
- package/dist/footer.js +10 -7
- package/dist/git.js +7 -11
- package/dist/header.js +2 -5
- package/dist/layout.js +2 -4
- package/dist/live-border.js +3 -3
- package/dist/model-info.js +10 -122
- package/dist/path-format.js +1 -2
- package/dist/run-activity.js +2 -6
- package/dist/runtime.js +1 -7
- package/dist/session-orchestrator.js +10 -15
- package/dist/telemetry.js +8 -28
- package/dist/theme-settings.js +2 -4
- package/dist/tip-policy.js +2 -4
- package/dist/transcript-timeline.js +15 -34
- package/dist/utils.js +5 -5
- package/dist/window-presentation.js +19 -3
- package/package.json +16 -5
- package/src/agent-run-ledger.ts +19 -71
- package/src/ansi-color.ts +186 -0
- package/src/border-renderer.ts +32 -54
- package/src/chrome-composition.ts +17 -13
- package/src/chrome-state.ts +36 -27
- package/src/color-policy.ts +35 -18
- package/src/config.ts +6 -24
- package/src/cursor-policy.ts +11 -20
- package/src/detail-chrome.ts +48 -28
- package/src/detail-render.ts +132 -125
- package/src/footer.ts +30 -44
- package/src/git.ts +9 -26
- package/src/header.ts +6 -23
- package/src/icons.ts +1 -2
- package/src/index.ts +45 -65
- package/src/layout.ts +13 -23
- package/src/live-border.ts +4 -9
- package/src/model-info.ts +94 -248
- package/src/path-format.ts +1 -2
- package/src/run-activity.ts +8 -25
- package/src/runtime.ts +9 -25
- package/src/session-orchestrator.ts +50 -124
- package/src/state.ts +4 -9
- package/src/telemetry.ts +19 -65
- package/src/theme-settings.ts +20 -62
- package/src/tip-policy.ts +5 -9
- package/src/tracking-editor.ts +11 -19
- package/src/transcript-timeline.ts +23 -77
- package/src/utils.ts +37 -6
- package/src/window-presentation.ts +51 -38
- package/dist/session-kernel.js +0 -178
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
## [0.12.2](https://github.com/Rianico/pi-editor-footer/compare/v0.12.1...v0.12.2) (2026-09-13)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **context:** tier-color the context window section by usage ([#30](https://github.com/Rianico/pi-editor-footer/issues/30)) ([8152d90](https://github.com/Rianico/pi-editor-footer/commit/8152d90b3c1b4013154eff95cc885e79e7564f6f))
|
|
4
6
|
|
|
5
7
|
## [Unreleased]
|
|
6
8
|
|
|
9
|
+
## [0.12.1](https://github.com/Rianico/pi-editor-footer/compare/v0.12.0...v0.12.1) (2026-09-07)
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* add .npmrc and clean allowScripts for npm install-scripts ([fc362cb](https://github.com/Rianico/pi-editor-footer/commit/fc362cbb654dbf886f92f774e2a5e87aa4ec5914))
|
|
14
|
+
|
|
15
|
+
## [0.12.0](https://github.com/Rianico/pi-editor-footer/compare/v0.11.0...v0.12.0) (2026-09-07)
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **detail:** show model invocation badge to the right of skill path ([0f03593](https://github.com/Rianico/pi-editor-footer/commit/0f0359347d7ae3de740400c587c5b0ff4a6fb57e))
|
|
20
|
+
* **detail:** use distinct highlight for human vs model badge ([e242df1](https://github.com/Rianico/pi-editor-footer/commit/e242df14975e10b6bfd32f8a48e5f943d7579864))
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* allow pnpm builds for esbuild and genai ([36e139e](https://github.com/Rianico/pi-editor-footer/commit/36e139e9443ef928a4366c3d7c8afe494b6e0850))
|
|
25
|
+
* make prepare tolerant to missing husky for pi install ([23ebb62](https://github.com/Rianico/pi-editor-footer/commit/23ebb62cd8d732656fd113da27c807ffa34579fc))
|
|
26
|
+
|
|
27
|
+
# Changelog
|
|
28
|
+
|
|
29
|
+
All notable changes to this project will be documented in this file.
|
|
30
|
+
|
|
7
31
|
## [0.11.0] - 2026-08-29
|
|
8
32
|
|
|
9
33
|
### Changed
|
package/dist/agent-run-ledger.js
CHANGED
|
@@ -85,9 +85,7 @@ export function aggregateAgentTurns(turns, live, startMs, now) {
|
|
|
85
85
|
totalTokens = inputTokens + outputTokens;
|
|
86
86
|
const totalMs = startMs === null ? 0 : Math.max(0, now - startMs);
|
|
87
87
|
const measurementMs = outputTokens > 0 && generationMs > 0 ? generationMs : null;
|
|
88
|
-
const tps = measurementMs === null
|
|
89
|
-
? null
|
|
90
|
-
: round(outputTokens / (measurementMs / 1000), 1);
|
|
88
|
+
const tps = measurementMs === null ? null : round(outputTokens / (measurementMs / 1000), 1);
|
|
91
89
|
const validCost = Number.isFinite(costUsd) && costUsd > 0;
|
|
92
90
|
const validTokens = Number.isFinite(totalTokens) && totalTokens > 0;
|
|
93
91
|
return {
|
|
@@ -98,9 +96,7 @@ export function aggregateAgentTurns(turns, live, startMs, now) {
|
|
|
98
96
|
outputTokens,
|
|
99
97
|
stallMs,
|
|
100
98
|
stallCount,
|
|
101
|
-
rateUsdPerMTokens: validCost && validTokens
|
|
102
|
-
? round(costUsd / (totalTokens / 1_000_000), 2)
|
|
103
|
-
: null,
|
|
99
|
+
rateUsdPerMTokens: validCost && validTokens ? round(costUsd / (totalTokens / 1_000_000), 2) : null,
|
|
104
100
|
generationMs,
|
|
105
101
|
totalTokens,
|
|
106
102
|
costUsd: validCost ? costUsd : 0,
|
|
@@ -117,9 +113,7 @@ export function aggregateAgentTurns(turns, live, startMs, now) {
|
|
|
117
113
|
*/
|
|
118
114
|
export function capInputForIdle(input, contextTokens, sessionTotalInput) {
|
|
119
115
|
let capped = input;
|
|
120
|
-
if (typeof contextTokens === "number" &&
|
|
121
|
-
Number.isFinite(contextTokens) &&
|
|
122
|
-
contextTokens > 0) {
|
|
116
|
+
if (typeof contextTokens === "number" && Number.isFinite(contextTokens) && contextTokens > 0) {
|
|
123
117
|
capped = Math.min(capped, contextTokens);
|
|
124
118
|
}
|
|
125
119
|
if (sessionTotalInput > 0) {
|
|
@@ -129,9 +123,7 @@ export function capInputForIdle(input, contextTokens, sessionTotalInput) {
|
|
|
129
123
|
}
|
|
130
124
|
export function capInputForLive(input, contextTokens) {
|
|
131
125
|
let capped = input;
|
|
132
|
-
if (typeof contextTokens === "number" &&
|
|
133
|
-
Number.isFinite(contextTokens) &&
|
|
134
|
-
contextTokens > 0) {
|
|
126
|
+
if (typeof contextTokens === "number" && Number.isFinite(contextTokens) && contextTokens > 0) {
|
|
135
127
|
capped = Math.min(capped, contextTokens);
|
|
136
128
|
}
|
|
137
129
|
return Math.max(0, capped);
|
|
@@ -197,9 +189,7 @@ export class AgentRunLedger {
|
|
|
197
189
|
}
|
|
198
190
|
/** Synthetic live input including live delta: trigger + Σ(outputs+tools) + liveDelta. */
|
|
199
191
|
getSyntheticLiveInput(liveDeltaTokens) {
|
|
200
|
-
const d = typeof liveDeltaTokens === "number" && Number.isFinite(liveDeltaTokens)
|
|
201
|
-
? liveDeltaTokens
|
|
202
|
-
: 0;
|
|
192
|
+
const d = typeof liveDeltaTokens === "number" && Number.isFinite(liveDeltaTokens) ? liveDeltaTokens : 0;
|
|
203
193
|
return (this.triggerTokens +
|
|
204
194
|
this.accumOutputTokens +
|
|
205
195
|
this.accumToolTokens +
|
|
@@ -378,23 +368,15 @@ export class AgentRunLedger {
|
|
|
378
368
|
if (!base)
|
|
379
369
|
return null;
|
|
380
370
|
// When both exist, agentLive already sums outputs, so use it; else liveTurn
|
|
381
|
-
const displayOutput = agentLive
|
|
382
|
-
? agentLive.outputTokens
|
|
383
|
-
: liveTurn.outputTokens;
|
|
371
|
+
const displayOutput = agentLive ? agentLive.outputTokens : liveTurn.outputTokens;
|
|
384
372
|
const displayCost = agentLive ? agentLive.costUsd : liveTurn.costUsd;
|
|
385
373
|
const displayStallMs = agentLive ? agentLive.stallMs : liveTurn.stallMs;
|
|
386
|
-
const displayStallCount = agentLive
|
|
387
|
-
|
|
388
|
-
: liveTurn.stallCount;
|
|
389
|
-
const displayGenerationMs = agentLive
|
|
390
|
-
? agentLive.generationMs
|
|
391
|
-
: liveTurn.generationMs;
|
|
374
|
+
const displayStallCount = agentLive ? agentLive.stallCount : liveTurn.stallCount;
|
|
375
|
+
const displayGenerationMs = agentLive ? agentLive.generationMs : liveTurn.generationMs;
|
|
392
376
|
const displayTtft = agentLive ? agentLive.ttftMs : liveTurn.ttftMs;
|
|
393
377
|
const displayTps = agentLive ? agentLive.tps : liveTurn.tps;
|
|
394
378
|
const displayTotalMs = agentLive ? agentLive.totalMs : liveTurn.totalMs;
|
|
395
|
-
const displayMeasurementMs = agentLive
|
|
396
|
-
? agentLive.measurementMs
|
|
397
|
-
: liveTurn.measurementMs;
|
|
379
|
+
const displayMeasurementMs = agentLive ? agentLive.measurementMs : liveTurn.measurementMs;
|
|
398
380
|
return {
|
|
399
381
|
tps: displayTps,
|
|
400
382
|
ttftMs: displayTtft,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const BASIC16 = [
|
|
2
|
+
[0, 0, 0],
|
|
3
|
+
[128, 0, 0],
|
|
4
|
+
[0, 128, 0],
|
|
5
|
+
[128, 128, 0],
|
|
6
|
+
[0, 0, 128],
|
|
7
|
+
[128, 0, 128],
|
|
8
|
+
[0, 128, 128],
|
|
9
|
+
[192, 192, 192],
|
|
10
|
+
[128, 128, 128],
|
|
11
|
+
[255, 0, 0],
|
|
12
|
+
[0, 255, 0],
|
|
13
|
+
[255, 255, 0],
|
|
14
|
+
[0, 0, 255],
|
|
15
|
+
[255, 0, 255],
|
|
16
|
+
[0, 255, 255],
|
|
17
|
+
[255, 255, 255],
|
|
18
|
+
];
|
|
19
|
+
/** 6x6x6 colour-cube channel values (indices 0-5). */
|
|
20
|
+
const CUBE_VALUES = [0, 95, 135, 175, 215, 255];
|
|
21
|
+
/** Grayscale ramp values (indices 232-255: 24 grays from 8 to 238). */
|
|
22
|
+
const GRAY_VALUES = Array.from({ length: 24 }, (_, i) => 8 + i * 10);
|
|
23
|
+
/** Anything but the truecolor marker means the 256-colour ramp. */
|
|
24
|
+
export function normalizeColorMode(mode) {
|
|
25
|
+
return mode === "truecolor" ? "truecolor" : "256color";
|
|
26
|
+
}
|
|
27
|
+
export function indexToRgb(n) {
|
|
28
|
+
if (n >= 0 && n < 16) {
|
|
29
|
+
const [r, g, b] = BASIC16[n] ?? [0, 0, 0];
|
|
30
|
+
return { r, g, b };
|
|
31
|
+
}
|
|
32
|
+
if (n >= 16 && n <= 231) {
|
|
33
|
+
const v = n - 16;
|
|
34
|
+
return {
|
|
35
|
+
r: CUBE_VALUES[Math.floor(v / 36)] ?? 0,
|
|
36
|
+
g: CUBE_VALUES[Math.floor(v / 6) % 6] ?? 0,
|
|
37
|
+
b: CUBE_VALUES[v % 6] ?? 0,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (n >= 232 && n <= 255) {
|
|
41
|
+
const gray = 8 + (n - 232) * 10;
|
|
42
|
+
return { r: gray, g: gray, b: gray };
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function findClosestCubeIndex(value) {
|
|
47
|
+
let minDist = Infinity;
|
|
48
|
+
let minIdx = 0;
|
|
49
|
+
for (let i = 0; i < CUBE_VALUES.length; i++) {
|
|
50
|
+
const dist = Math.abs(value - (CUBE_VALUES[i] ?? 0));
|
|
51
|
+
if (dist < minDist) {
|
|
52
|
+
minDist = dist;
|
|
53
|
+
minIdx = i;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return minIdx;
|
|
57
|
+
}
|
|
58
|
+
function findClosestGrayIndex(gray) {
|
|
59
|
+
let minDist = Infinity;
|
|
60
|
+
let minIdx = 0;
|
|
61
|
+
for (let i = 0; i < GRAY_VALUES.length; i++) {
|
|
62
|
+
const dist = Math.abs(gray - (GRAY_VALUES[i] ?? 0));
|
|
63
|
+
if (dist < minDist) {
|
|
64
|
+
minDist = dist;
|
|
65
|
+
minIdx = i;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return minIdx;
|
|
69
|
+
}
|
|
70
|
+
function colorDistance(r1, g1, b1, r2, g2, b2) {
|
|
71
|
+
const dr = r1 - r2;
|
|
72
|
+
const dg = g1 - g2;
|
|
73
|
+
const db = b1 - b2;
|
|
74
|
+
// Weighted Euclidean distance (the eye is more sensitive to green).
|
|
75
|
+
return dr * dr * 0.299 + dg * dg * 0.587 + db * db * 0.114;
|
|
76
|
+
}
|
|
77
|
+
/** Quantise RGB to the closest xterm-256 index (same rule as the theme loader). */
|
|
78
|
+
export function rgbTo256(r, g, b) {
|
|
79
|
+
const rIdx = findClosestCubeIndex(r);
|
|
80
|
+
const gIdx = findClosestCubeIndex(g);
|
|
81
|
+
const bIdx = findClosestCubeIndex(b);
|
|
82
|
+
const cubeR = CUBE_VALUES[rIdx] ?? 0;
|
|
83
|
+
const cubeG = CUBE_VALUES[gIdx] ?? 0;
|
|
84
|
+
const cubeB = CUBE_VALUES[bIdx] ?? 0;
|
|
85
|
+
const cubeIndex = 16 + 36 * rIdx + 6 * gIdx + bIdx;
|
|
86
|
+
const cubeDist = colorDistance(r, g, b, cubeR, cubeG, cubeB);
|
|
87
|
+
const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
|
|
88
|
+
const grayIdx = findClosestGrayIndex(gray);
|
|
89
|
+
const grayValue = GRAY_VALUES[grayIdx] ?? 0;
|
|
90
|
+
const grayIndex = 232 + grayIdx;
|
|
91
|
+
const grayDist = colorDistance(r, g, b, grayValue, grayValue, grayValue);
|
|
92
|
+
const spread = Math.max(r, g, b) - Math.min(r, g, b);
|
|
93
|
+
// Only consider grayscale when the colour is nearly neutral AND closer.
|
|
94
|
+
if (spread < 10 && grayDist < cubeDist)
|
|
95
|
+
return grayIndex;
|
|
96
|
+
return cubeIndex;
|
|
97
|
+
}
|
|
98
|
+
/** Parse `#RRGGBB`. Throws on malformed input — callers pass literals, not user data. */
|
|
99
|
+
export function hexToRgb(hex) {
|
|
100
|
+
const cleaned = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
101
|
+
if (cleaned.length !== 6)
|
|
102
|
+
throw new Error(`Invalid hex color: ${hex}`);
|
|
103
|
+
const { r, g, b } = {
|
|
104
|
+
r: parseInt(cleaned.slice(0, 2), 16),
|
|
105
|
+
g: parseInt(cleaned.slice(2, 4), 16),
|
|
106
|
+
b: parseInt(cleaned.slice(4, 6), 16),
|
|
107
|
+
};
|
|
108
|
+
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
|
|
109
|
+
throw new Error(`Invalid hex color: ${hex}`);
|
|
110
|
+
}
|
|
111
|
+
return { r, g, b };
|
|
112
|
+
}
|
|
113
|
+
/** Parse a theme token's ANSI escape back into RGB (truecolor or 256 index). */
|
|
114
|
+
export function parseFgAnsiToRgb(theme, color) {
|
|
115
|
+
const ansi = theme.getFgAnsi(color);
|
|
116
|
+
const trueColor = ansi.match(/38;2;(\d+);(\d+);(\d+)/);
|
|
117
|
+
if (trueColor) {
|
|
118
|
+
return {
|
|
119
|
+
r: Number(trueColor[1]),
|
|
120
|
+
g: Number(trueColor[2]),
|
|
121
|
+
b: Number(trueColor[3]),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const palette = ansi.match(/38;5;(\d+)/);
|
|
125
|
+
if (palette)
|
|
126
|
+
return indexToRgb(Number(palette[1]));
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
/** SGR foreground sequence for an RGB value at the terminal's colour fidelity. */
|
|
130
|
+
export function rgbToFgAnsi({ r, g, b }, mode) {
|
|
131
|
+
return normalizeColorMode(mode) === "truecolor"
|
|
132
|
+
? `\x1b[38;2;${r};${g};${b}m`
|
|
133
|
+
: `\x1b[38;5;${rgbTo256(r, g, b)}m`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Colour `s` with an exact hex, bypassing the theme's named-token lookup.
|
|
137
|
+
* Returned painter resets only the foreground (`\x1b[39m`), matching `theme.fg`.
|
|
138
|
+
*/
|
|
139
|
+
export function hexFg(hex, mode) {
|
|
140
|
+
const ansi = rgbToFgAnsi(hexToRgb(hex), mode);
|
|
141
|
+
return (s) => `${ansi}${s}\x1b[39m`;
|
|
142
|
+
}
|
package/dist/border-renderer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
import {
|
|
2
|
+
import { applyModelInfo, buildLabel } from "./model-info.js";
|
|
3
3
|
function stripAnsi(s) {
|
|
4
4
|
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
5
5
|
}
|
|
@@ -30,9 +30,8 @@ export class BorderRenderer {
|
|
|
30
30
|
const info = this.getModelInfo();
|
|
31
31
|
const glow = (s) => {
|
|
32
32
|
try {
|
|
33
|
-
const maybeGlow =
|
|
34
|
-
|
|
35
|
-
theme.getThinkingBorderColor;
|
|
33
|
+
const maybeGlow = // SAFETY: intentional unsafe cast — validated at runtime
|
|
34
|
+
theme.getThinkingBorderColor;
|
|
36
35
|
if (typeof maybeGlow === "function")
|
|
37
36
|
return maybeGlow.call(theme, info.level)(s);
|
|
38
37
|
}
|
|
@@ -93,9 +92,8 @@ export class BorderRenderer {
|
|
|
93
92
|
const theme = this.getLiveTheme();
|
|
94
93
|
const glow = (s) => {
|
|
95
94
|
try {
|
|
96
|
-
const maybeGlow =
|
|
97
|
-
|
|
98
|
-
theme.getThinkingBorderColor;
|
|
95
|
+
const maybeGlow = // SAFETY: intentional unsafe cast — validated at runtime
|
|
96
|
+
theme.getThinkingBorderColor;
|
|
99
97
|
if (typeof maybeGlow === "function")
|
|
100
98
|
return maybeGlow.call(theme, this.getModelInfo().level)(s);
|
|
101
99
|
}
|
|
@@ -136,12 +134,8 @@ function embedTopWithLeftAndRight(lines, width, leftText, rightText, getGlow) {
|
|
|
136
134
|
displayLeft = truncateToWidth(leftText, maxLeft, "");
|
|
137
135
|
if (rightW > maxRight)
|
|
138
136
|
displayRight = truncateToWidth(rightText, maxRight, "");
|
|
139
|
-
const leftSegment = displayLeft
|
|
140
|
-
|
|
141
|
-
: getGlow("─");
|
|
142
|
-
const rightSegment = displayRight
|
|
143
|
-
? ` ${displayRight} ${getGlow("─")}`
|
|
144
|
-
: getGlow("─");
|
|
137
|
+
const leftSegment = displayLeft ? `${getGlow("─")} ${displayLeft} ` : getGlow("─");
|
|
138
|
+
const rightSegment = displayRight ? ` ${displayRight} ${getGlow("─")}` : getGlow("─");
|
|
145
139
|
const used = visibleWidth(leftSegment) + visibleWidth(rightSegment);
|
|
146
140
|
const middleWidth = Math.max(0, width - used);
|
|
147
141
|
const middle = getGlow("─".repeat(middleWidth));
|
|
@@ -174,12 +168,8 @@ function embedBottomBorder(lines, width, leftText, rightText, getGlow) {
|
|
|
174
168
|
displayLeft = truncateToWidth(leftText, maxLeft, "");
|
|
175
169
|
if (rightW > maxRight)
|
|
176
170
|
displayRight = truncateToWidth(rightText, maxRight, "");
|
|
177
|
-
const leftSegment = displayLeft
|
|
178
|
-
|
|
179
|
-
: getGlow("─");
|
|
180
|
-
const rightSegment = displayRight
|
|
181
|
-
? ` ${displayRight} ${getGlow("─")}`
|
|
182
|
-
: getGlow("─");
|
|
171
|
+
const leftSegment = displayLeft ? `${getGlow("─")} ${displayLeft} ` : getGlow("─");
|
|
172
|
+
const rightSegment = displayRight ? ` ${displayRight} ${getGlow("─")}` : getGlow("─");
|
|
183
173
|
const used = visibleWidth(leftSegment) + visibleWidth(rightSegment);
|
|
184
174
|
const middleWidth = Math.max(0, width - used);
|
|
185
175
|
const middle = getGlow("─".repeat(middleWidth));
|
|
@@ -20,15 +20,20 @@
|
|
|
20
20
|
import { resolveGlyphs, resolveIconMode } from "./icons.js";
|
|
21
21
|
import { formatTopContextFromSnapshot } from "./chrome-state.js";
|
|
22
22
|
import { formatRunActivityTopRight } from "./run-activity.js";
|
|
23
|
-
import { formatTelemetryTokens, formatTurnDuration, formatTurnTelemetry
|
|
23
|
+
import { formatTelemetryTokens, formatTurnDuration, formatTurnTelemetry } from "./telemetry.js";
|
|
24
|
+
import { hexFg } from "./ansi-color.js";
|
|
24
25
|
/**
|
|
25
|
-
* Read the live pi theme into a typed { fg } surface. The cast is a SAFETY
|
|
26
|
-
* missing `fg` degrades to identity rather than throwing, keeping
|
|
26
|
+
* Read the live pi theme into a typed { fg, fgHex } surface. The cast is a SAFETY
|
|
27
|
+
* seam — a pi theme missing `fg` degrades to identity rather than throwing, keeping
|
|
28
|
+
* the chrome resilient. `fgHex` needs no theme call at all: the named-token lookup
|
|
29
|
+
* rejects raw hex, so hex is rendered straight to SGR at the theme's colour mode.
|
|
27
30
|
*/
|
|
28
31
|
export function adaptTheme(rawTheme) {
|
|
29
|
-
const t = rawTheme;
|
|
32
|
+
const t = rawTheme;
|
|
33
|
+
const mode = typeof t.getColorMode === "function" ? t.getColorMode() : undefined;
|
|
30
34
|
return {
|
|
31
35
|
fg: (style, s) => (typeof t.fg === "function" ? t.fg(style, s) : s),
|
|
36
|
+
fgHex: (hex, s) => hexFg(hex, mode)(s),
|
|
32
37
|
};
|
|
33
38
|
}
|
|
34
39
|
/** Optional thinking-border glow (pi theme extension). Falls back to identity. */
|
package/dist/chrome-state.js
CHANGED
|
@@ -15,20 +15,30 @@
|
|
|
15
15
|
* inside this module, not part of its external seam.
|
|
16
16
|
*/
|
|
17
17
|
import { getUsageTotals } from "./state.js";
|
|
18
|
-
import { cacheHitColor,
|
|
18
|
+
import { cacheHitColor, CONTEXT_TIER_HEX, CONTEXT_TIER_THEME_COLOR, contextUsageTier, } from "./color-policy.js";
|
|
19
19
|
import { fmtTokens } from "./format.js";
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
// Context bar formatting — moved from footer.ts to centralize chrome rendering.
|
|
22
22
|
// Re-exported from footer.ts for backward compatibility.
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Painter for one context tier: exact hex when the live theme can emit raw hex,
|
|
26
|
+
* else the nearest semantic token (tests/mocks, themes without colour mode).
|
|
27
|
+
*/
|
|
28
|
+
function contextPainter(theme, tier) {
|
|
29
|
+
const hex = CONTEXT_TIER_HEX[tier];
|
|
30
|
+
const fgHex = theme.fgHex;
|
|
31
|
+
if (fgHex)
|
|
32
|
+
return (s) => fgHex.call(theme, hex, s);
|
|
33
|
+
return (s) => theme.fg(CONTEXT_TIER_THEME_COLOR[tier], s);
|
|
34
|
+
}
|
|
35
|
+
function renderBar(theme, paint, pct, barWidth, ascii) {
|
|
25
36
|
const filled = Math.max(0, Math.min(barWidth, Math.round((pct / 100) * barWidth)));
|
|
26
37
|
const empty = barWidth - filled;
|
|
27
|
-
const color = contextUsageColor(pct);
|
|
28
38
|
const filledCell = ascii ? "#" : "█";
|
|
29
39
|
const emptyCell = ascii ? "-" : "░";
|
|
30
40
|
return (theme.fg("dim", "[") +
|
|
31
|
-
|
|
41
|
+
paint(filledCell.repeat(filled)) +
|
|
32
42
|
theme.fg("dim", emptyCell.repeat(empty)) +
|
|
33
43
|
theme.fg("dim", "]"));
|
|
34
44
|
}
|
|
@@ -37,17 +47,15 @@ export function formatContextBar(contextUsage, theme, glyphs, isAscii, barWidth
|
|
|
37
47
|
if (contextWindow <= 0)
|
|
38
48
|
return "";
|
|
39
49
|
const contextPct = contextUsage?.percent ?? 0;
|
|
40
|
-
const
|
|
41
|
-
const pctText =
|
|
50
|
+
const paint = contextPainter(theme, contextUsageTier(contextPct, contextWindow));
|
|
51
|
+
const pctText = paint(`${contextPct.toFixed(1)}%`);
|
|
42
52
|
const contextTokens = contextUsage?.tokens ?? 0;
|
|
43
|
-
const ctxText = `${
|
|
53
|
+
const ctxText = `${paint(fmtTokens(contextTokens))}${theme.fg("dim", "/")}${paint(fmtTokens(contextWindow))}`;
|
|
44
54
|
const baseCore = `${pctText} ${theme.fg("dim", "·")} ${ctxText}`;
|
|
45
55
|
const base = showIconBar
|
|
46
|
-
? `${
|
|
56
|
+
? `${paint(glyphs.context)} ${renderBar(theme, paint, contextPct, barWidth, isAscii)} ${baseCore}`
|
|
47
57
|
: baseCore;
|
|
48
|
-
const rate = cacheHitRate !== undefined && Number.isFinite(cacheHitRate)
|
|
49
|
-
? cacheHitRate
|
|
50
|
-
: 0;
|
|
58
|
+
const rate = cacheHitRate !== undefined && Number.isFinite(cacheHitRate) ? cacheHitRate : 0;
|
|
51
59
|
const cacheText = `${glyphs.cacheHit} ${rate.toFixed(1)}%`;
|
|
52
60
|
return `${theme.fg(cacheHitColor(rate), cacheText)} ${theme.fg("dim", "|")} ${base}`;
|
|
53
61
|
}
|
|
@@ -75,7 +83,8 @@ export function createChromeSnapshot(ctx, footerState) {
|
|
|
75
83
|
const totals = getUsageTotals(
|
|
76
84
|
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
77
85
|
// SAFETY: pi seam
|
|
78
|
-
/* SAFETY: intentional unsafe cast — validated at runtime */ (ctx ??
|
|
86
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ (ctx ??
|
|
87
|
+
{}));
|
|
79
88
|
// footerState may be absent when called from LiveBorder (context-only); use empty defaults
|
|
80
89
|
// SAFETY: pi seam
|
|
81
90
|
const git = footerState?.git ??
|
|
@@ -93,7 +102,7 @@ export function createChromeSnapshot(ctx, footerState) {
|
|
|
93
102
|
commit: null,
|
|
94
103
|
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
95
104
|
// SAFETY: intentional unsafe cast — validated at runtime
|
|
96
|
-
/* SAFETY: intentional unsafe cast — validated at runtime */
|
|
105
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */
|
|
97
106
|
}; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
98
107
|
const runtime = footerState?.runtime ?? null;
|
|
99
108
|
return {
|
package/dist/color-policy.js
CHANGED
|
@@ -5,24 +5,37 @@ export function stressColor(value, warn = 70, danger = 90) {
|
|
|
5
5
|
return "warning";
|
|
6
6
|
return "accent";
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Fixed tier palette (nord green / nord yellow / dark red). Hex, not theme tokens:
|
|
10
|
+
* the tiers are an alarm scale the user picked explicitly, so they must read the
|
|
11
|
+
* same on every theme. `CONTEXT_TIER_THEME_COLOR` is only the fallback for themes
|
|
12
|
+
* that cannot emit raw hex.
|
|
13
|
+
*/
|
|
14
|
+
export const CONTEXT_TIER_HEX = {
|
|
15
|
+
ok: "#A3BE8C",
|
|
16
|
+
warn: "#EBCB8B",
|
|
17
|
+
critical: "#9A3939",
|
|
18
|
+
};
|
|
19
|
+
/** Semantic token approximating each tier — used when raw hex is unavailable. */
|
|
20
|
+
export const CONTEXT_TIER_THEME_COLOR = {
|
|
21
|
+
ok: "success",
|
|
22
|
+
warn: "warning",
|
|
23
|
+
critical: "error",
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Tier from the share of the context window consumed.
|
|
27
|
+
*
|
|
28
|
+
* Budgets scale with the window: a 1M window only earns 12.5 / 25 % of slack
|
|
29
|
+
* before the same alarm, while smaller windows (compacted far sooner) keep the
|
|
30
|
+
* looser 25 / 50 % steps.
|
|
31
|
+
*/
|
|
32
|
+
export function contextUsageTier(pct, contextWindow) {
|
|
33
|
+
const [warnAt, criticalAt] = contextWindow >= 1_000_000 ? [12.5, 25] : [25, 50];
|
|
34
|
+
if (pct >= criticalAt)
|
|
35
|
+
return "critical";
|
|
36
|
+
if (pct >= warnAt)
|
|
37
|
+
return "warn";
|
|
38
|
+
return "ok";
|
|
26
39
|
}
|
|
27
40
|
export function cacheHitColor(value) {
|
|
28
41
|
if (value < 30)
|
package/dist/config.js
CHANGED
|
@@ -46,9 +46,7 @@ function deepMerge(base, override) {
|
|
|
46
46
|
if (typeof base !== "object" || base === null || Array.isArray(base)) {
|
|
47
47
|
return override ?? base;
|
|
48
48
|
}
|
|
49
|
-
if (typeof override !== "object" ||
|
|
50
|
-
override === null ||
|
|
51
|
-
Array.isArray(override)) {
|
|
49
|
+
if (typeof override !== "object" || override === null || Array.isArray(override)) {
|
|
52
50
|
return base;
|
|
53
51
|
}
|
|
54
52
|
const result = { ...base };
|
package/dist/cursor-policy.js
CHANGED
|
@@ -17,15 +17,13 @@ function removeSoftwareCursor(line, cursorMarker = "") {
|
|
|
17
17
|
function configureCursor(tui, cursorStyle) {
|
|
18
18
|
if (cursorStyle === "block")
|
|
19
19
|
return;
|
|
20
|
-
const setShow =
|
|
21
|
-
|
|
22
|
-
tui.setShowHardwareCursor;
|
|
20
|
+
const setShow = // SAFETY: intentional unsafe cast — validated at runtime
|
|
21
|
+
tui.setShowHardwareCursor;
|
|
23
22
|
if (typeof setShow === "function")
|
|
24
23
|
setShow.call(tui, true);
|
|
25
24
|
const seq = CURSOR_STYLE_SEQUENCES[cursorStyle];
|
|
26
25
|
// SAFETY: intentional unsafe cast — validated at runtime
|
|
27
|
-
const term = tui
|
|
28
|
-
.terminal;
|
|
26
|
+
const term = tui.terminal;
|
|
29
27
|
if (seq && term && typeof term.write === "function")
|
|
30
28
|
term.write(seq);
|
|
31
29
|
}
|
package/dist/detail-chrome.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
17
|
import * as os from "node:os";
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
-
import { contentLineCount, renderDetail, scroll
|
|
19
|
+
import { contentLineCount, renderDetail, scroll } from "./detail-render.js";
|
|
20
20
|
import { decorateWindow } from "./window-presentation.js";
|
|
21
21
|
/** Height cap of the detail window (the user's spec: up to 5 lines). */
|
|
22
22
|
export const MAX_LINES = 5;
|
|
@@ -52,6 +52,28 @@ function resolveSkillPath(skillName) {
|
|
|
52
52
|
candidates.push(path.join(home, "stowfiles", "dotfiles", ".pi", "agent", "skills", name, "SKILL.md"));
|
|
53
53
|
candidates.push(path.join(home, "stowfiles", "dotfiles", ".pi", "agent", ".agents", "skills", name, "SKILL.md"));
|
|
54
54
|
}
|
|
55
|
+
// subskill candidates (any parent skill -> subskills/<name>)
|
|
56
|
+
const bases = [
|
|
57
|
+
path.join(cwd, ".agents", "skills"),
|
|
58
|
+
path.join(cwd, ".pi", "agent", "skills"),
|
|
59
|
+
path.join(cwd, ".pi", "skills"),
|
|
60
|
+
path.join(cwd, "skills"),
|
|
61
|
+
];
|
|
62
|
+
if (home) {
|
|
63
|
+
bases.push(path.join(home, ".agents", "skills"), path.join(home, ".pi", "agent", "skills"), path.join(home, ".pi", "skills"));
|
|
64
|
+
}
|
|
65
|
+
for (const base of bases) {
|
|
66
|
+
try {
|
|
67
|
+
const entries = fs.readdirSync(base, { withFileTypes: true });
|
|
68
|
+
for (const e of entries) {
|
|
69
|
+
const isDir = e.isDirectory() || e.isSymbolicLink();
|
|
70
|
+
if (!isDir)
|
|
71
|
+
continue;
|
|
72
|
+
candidates.push(path.join(base, e.name, "subskills", name, "SKILL.md"));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch { }
|
|
76
|
+
}
|
|
55
77
|
for (const p of candidates) {
|
|
56
78
|
try {
|
|
57
79
|
if (fs.existsSync(p))
|
|
@@ -66,6 +88,21 @@ function resolveSkillPath(skillName) {
|
|
|
66
88
|
return path.join(home, ".pi", "agent", "skills", name, "SKILL.md");
|
|
67
89
|
return path.join(cwd, ".pi", "agent", "skills", name, "SKILL.md");
|
|
68
90
|
}
|
|
91
|
+
function parseInvocation(skillPath) {
|
|
92
|
+
try {
|
|
93
|
+
const content = fs.readFileSync(skillPath, "utf-8");
|
|
94
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
95
|
+
if (!fmMatch)
|
|
96
|
+
return "model";
|
|
97
|
+
const fm = fmMatch[1] ?? "";
|
|
98
|
+
if (/^\s*disable-model-invocation\s*:\s*true\b/m.test(fm))
|
|
99
|
+
return "human";
|
|
100
|
+
return "model";
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
69
106
|
function detailItemOf(item) {
|
|
70
107
|
const detail = {
|
|
71
108
|
label: item.label,
|
|
@@ -82,6 +119,11 @@ function detailItemOf(item) {
|
|
|
82
119
|
if (resolved !== undefined)
|
|
83
120
|
detail.path = resolved;
|
|
84
121
|
}
|
|
122
|
+
if (detail.kind === "skill" && detail.path) {
|
|
123
|
+
const inv = parseInvocation(detail.path);
|
|
124
|
+
if (inv)
|
|
125
|
+
detail.invocation = inv;
|
|
126
|
+
}
|
|
85
127
|
return detail;
|
|
86
128
|
}
|
|
87
129
|
export class DetailChrome {
|
|
@@ -138,6 +180,8 @@ export class DetailChrome {
|
|
|
138
180
|
border: (s) => t.fg("border", s),
|
|
139
181
|
highlight: (s) => t.fg("accent", t.bold(s)),
|
|
140
182
|
dim: (s) => t.fg("dim", s),
|
|
183
|
+
model: (s) => t.fg("success", s),
|
|
184
|
+
human: (s) => t.fg("warning", s),
|
|
141
185
|
};
|
|
142
186
|
const innerWidth = Math.max(1, width - 4);
|
|
143
187
|
const lines = renderDetail(detailItemOf(item), innerWidth, this.maxLines, this.scrollOffset);
|
package/dist/detail-render.js
CHANGED
|
@@ -42,9 +42,11 @@ export function renderDetail(item, width, maxLines, scrollOffset) {
|
|
|
42
42
|
const markerLen = marker.length;
|
|
43
43
|
let header;
|
|
44
44
|
const rawPath = item.path?.trim() ?? "";
|
|
45
|
+
const invocationSeg = item.invocation ? ` · ${item.invocation}` : "";
|
|
46
|
+
const invocationLen = invocationSeg.length;
|
|
45
47
|
if (rawPath !== "") {
|
|
46
48
|
const normalized = normalizePath(rawPath);
|
|
47
|
-
const availableForPath = Math.max(0, wrapWidth - base.length - markerLen - 2);
|
|
49
|
+
const availableForPath = Math.max(0, wrapWidth - base.length - markerLen - invocationLen - 2);
|
|
48
50
|
let pathSeg = "";
|
|
49
51
|
if (availableForPath >= 10) {
|
|
50
52
|
const truncated = normalized.length <= availableForPath
|
|
@@ -52,19 +54,30 @@ export function renderDetail(item, width, maxLines, scrollOffset) {
|
|
|
52
54
|
: truncateMiddle(normalized, availableForPath);
|
|
53
55
|
pathSeg = ` ${truncated}`;
|
|
54
56
|
}
|
|
55
|
-
const totalLen = base.length + pathSeg.length + markerLen;
|
|
57
|
+
const totalLen = base.length + pathSeg.length + invocationLen + markerLen;
|
|
56
58
|
if (totalLen <= wrapWidth) {
|
|
57
|
-
header = base + pathSeg + marker;
|
|
59
|
+
header = base + pathSeg + invocationSeg + marker;
|
|
58
60
|
}
|
|
59
|
-
else if (pathSeg !== "") {
|
|
60
|
-
const
|
|
61
|
-
|
|
61
|
+
else if (pathSeg !== "" || invocationSeg !== "") {
|
|
62
|
+
const suffixLen = pathSeg.length + invocationLen + markerLen;
|
|
63
|
+
const baseWidth = Math.max(0, wrapWidth - suffixLen);
|
|
64
|
+
header = truncateToWidth(base, baseWidth) + pathSeg + invocationSeg + marker;
|
|
62
65
|
}
|
|
63
66
|
else {
|
|
64
67
|
const nameWidth = Math.max(0, wrapWidth - markerLen);
|
|
65
68
|
header = truncateToWidth(base, nameWidth) + marker;
|
|
66
69
|
}
|
|
67
70
|
}
|
|
71
|
+
else if (invocationSeg !== "") {
|
|
72
|
+
const totalLen = base.length + invocationLen + markerLen;
|
|
73
|
+
if (totalLen <= wrapWidth) {
|
|
74
|
+
header = base + invocationSeg + marker;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const baseWidth = Math.max(0, wrapWidth - invocationLen - markerLen);
|
|
78
|
+
header = truncateToWidth(base, baseWidth) + invocationSeg + marker;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
68
81
|
else if (overflows) {
|
|
69
82
|
const nameWidth = Math.max(0, wrapWidth - markerLen);
|
|
70
83
|
header = truncateToWidth(base, nameWidth) + marker;
|
|
@@ -83,8 +96,7 @@ export function contentLineCount(item, width) {
|
|
|
83
96
|
if (!item || item.description.trim() === "") {
|
|
84
97
|
return 0;
|
|
85
98
|
}
|
|
86
|
-
return wrapDescription(item.description, Math.max(1, Math.floor(width)))
|
|
87
|
-
.length;
|
|
99
|
+
return wrapDescription(item.description, Math.max(1, Math.floor(width))).length;
|
|
88
100
|
}
|
|
89
101
|
/**
|
|
90
102
|
* Next scroll offset after moving by `delta` (-1 = back/up, +1 = forward/down).
|