pi-editor-footer 0.8.0 → 0.12.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 +43 -1
- package/dist/agent-run-ledger.js +149 -13
- package/dist/border-renderer.js +10 -16
- package/dist/chrome-composition.js +1 -1
- package/dist/chrome-state.js +10 -5
- package/dist/config.js +3 -3
- package/dist/cursor-policy.js +3 -5
- package/dist/detail-chrome.js +117 -2
- package/dist/detail-render.js +83 -7
- package/dist/footer.js +22 -11
- package/dist/git.js +7 -11
- package/dist/header.js +2 -5
- package/dist/layout.js +2 -4
- package/dist/live-border.js +12 -10
- package/dist/model-info.js +2 -8
- 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 +119 -29
- package/dist/telemetry.js +8 -28
- package/dist/theme-settings.js +2 -4
- package/dist/tip-policy.js +2 -4
- package/dist/tracking-editor.js +8 -3
- package/dist/transcript-timeline.js +57 -36
- package/dist/utils.js +4 -4
- package/dist/window-presentation.js +30 -1
- package/package.json +23 -4
- package/src/agent-run-ledger.ts +172 -48
- package/src/border-renderer.ts +32 -52
- package/src/chrome-composition.ts +2 -8
- package/src/chrome-state.ts +15 -23
- package/src/color-policy.ts +1 -3
- package/src/config.ts +13 -28
- package/src/cursor-policy.ts +11 -20
- package/src/detail-chrome.ts +113 -14
- package/src/detail-render.ts +139 -75
- package/src/footer.ts +37 -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 +7 -23
- package/src/live-border.ts +10 -14
- package/src/model-info.ts +182 -213
- 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 +146 -139
- 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 +21 -21
- package/src/transcript-timeline.ts +112 -79
- package/src/utils.ts +33 -4
- package/src/window-presentation.ts +51 -26
- package/dist/session-kernel.js +0 -178
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,50 @@
|
|
|
1
|
+
## [0.12.1](https://github.com/Rianico/pi-editor-footer/compare/v0.12.0...v0.12.1) (2026-09-07)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* add .npmrc and clean allowScripts for npm install-scripts ([fc362cb](https://github.com/Rianico/pi-editor-footer/commit/fc362cbb654dbf886f92f774e2a5e87aa4ec5914))
|
|
6
|
+
|
|
7
|
+
## [0.12.0](https://github.com/Rianico/pi-editor-footer/compare/v0.11.0...v0.12.0) (2026-09-07)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **detail:** show model invocation badge to the right of skill path ([0f03593](https://github.com/Rianico/pi-editor-footer/commit/0f0359347d7ae3de740400c587c5b0ff4a6fb57e))
|
|
12
|
+
* **detail:** use distinct highlight for human vs model badge ([e242df1](https://github.com/Rianico/pi-editor-footer/commit/e242df14975e10b6bfd32f8a48e5f943d7579864))
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* allow pnpm builds for esbuild and genai ([36e139e](https://github.com/Rianico/pi-editor-footer/commit/36e139e9443ef928a4366c3d7c8afe494b6e0850))
|
|
17
|
+
* make prepare tolerant to missing husky for pi install ([23ebb62](https://github.com/Rianico/pi-editor-footer/commit/23ebb62cd8d732656fd113da27c807ffa34579fc))
|
|
18
|
+
|
|
1
19
|
# Changelog
|
|
2
20
|
|
|
3
21
|
All notable changes to this project will be documented in this file.
|
|
4
22
|
|
|
5
|
-
## [
|
|
23
|
+
## [0.11.0] - 2026-08-29
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Release workflow trigger clarified — tag `v*` remains the release trigger (semantic-release on tag push), reverted tentative `main` trigger
|
|
28
|
+
|
|
29
|
+
## [0.10.0] - 2026-08-28
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Detail header shows slash command path dimmed at right of type — workspace-relative/~/ tildified, middle-truncated with ellipsis, hidden when <10 cols remain, auto-resolved for skill items across project and home candidates with fallback to global symlink path
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Resolve pre-existing blocking diagnostics (`require-safety-comment`, `no-unknown-returns`, `zizmor cache-poisoning`) to keep lens clean
|
|
38
|
+
|
|
39
|
+
## [0.9.0] - 2026-08-27
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- Incremental per-agent_run live input accounting — `trigger+Σ(outputs+tools)+liveDelta` per run (`~` during live, authoritative `deltaFromBaseline` after), `AgentRunLedger` owns `triggerTokens/accumOutput/accumTool` reset in `startRun`, `getIncrementalLiveDisplayTotals`/`getIdleAuthoritativeDisplay` (grill Q6→a Q7→b Q8→a Q9→a Q11→a)
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- `turn_start` input now synthetic `trigger+Σ` (independent per run) instead of `getContextUsage` window; `tool_result`/`tool_execution_end` fold `chars/4` into accumulation; `LiveBorder` top `↑` hybrid (`~` live, no `~` idle), context bar stays real window (Q10); co-coded with `typescript-expert` (`unknown` boundaries, `SAFETY` on pi seams)
|
|
6
48
|
|
|
7
49
|
## [0.8.0] - 2026-08-26
|
|
8
50
|
|
package/dist/agent-run-ledger.js
CHANGED
|
@@ -15,11 +15,29 @@
|
|
|
15
15
|
* / cap helpers) hides baseline delta, max-vs-sum, predictive vs authoritative capping,
|
|
16
16
|
* and tps derivation. Two adapters (LiveBorder token bar, index timeline) justify the seam.
|
|
17
17
|
* Internal seams (aggregateTurns, capIdle, capLive) stay private.
|
|
18
|
+
*
|
|
19
|
+
* Incremental live input (per-agent_run):
|
|
20
|
+
* liveInput = triggerTokens + Σ(completed turn outputs) + Σ(toolResult tokens) + liveDelta
|
|
21
|
+
* Each agent_run resets independently. triggerTokens = ceil(triggerChars/4) or 0 if
|
|
22
|
+
* no user message (Q11 a). Tool results are ceil(chars/4) per tool_result (Q8 a).
|
|
23
|
+
* Live input is predictive (estimated=true, "~" prefix) and capped only to contextWindow.
|
|
24
|
+
* Settled/idle input snaps to authoritative deltaFromBaseline (hybrid Q7 b, Q6 a).
|
|
18
25
|
*/
|
|
19
26
|
function round(value, decimals) {
|
|
20
27
|
const factor = 10 ** decimals;
|
|
21
28
|
return Math.round(value * factor) / factor;
|
|
22
29
|
}
|
|
30
|
+
/** Estimate tokens from char length — ~4 chars/token, same as TurnTelemetryTracker live estimate. */
|
|
31
|
+
export function estimateTokensFromChars(chars) {
|
|
32
|
+
if (!Number.isFinite(chars) || chars <= 0)
|
|
33
|
+
return 0;
|
|
34
|
+
return Math.ceil(chars / 4);
|
|
35
|
+
}
|
|
36
|
+
export function estimateTokensFromText(text) {
|
|
37
|
+
if (typeof text !== "string" || text.length === 0)
|
|
38
|
+
return 0;
|
|
39
|
+
return estimateTokensFromChars(text.length);
|
|
40
|
+
}
|
|
23
41
|
/**
|
|
24
42
|
* Pure aggregation: max(input) + sum(output/cost/stalls) across turns, plus optional live turn.
|
|
25
43
|
* Shared by TurnTelemetryTracker.peekAgentLive / endAgent and ledger — single source for
|
|
@@ -65,11 +83,9 @@ export function aggregateAgentTurns(turns, live, startMs, now) {
|
|
|
65
83
|
ttftMs = live.ttftMs;
|
|
66
84
|
}
|
|
67
85
|
totalTokens = inputTokens + outputTokens;
|
|
68
|
-
const totalMs = startMs
|
|
86
|
+
const totalMs = startMs === null ? 0 : Math.max(0, now - startMs);
|
|
69
87
|
const measurementMs = outputTokens > 0 && generationMs > 0 ? generationMs : null;
|
|
70
|
-
const tps = measurementMs === null
|
|
71
|
-
? null
|
|
72
|
-
: round(outputTokens / (measurementMs / 1000), 1);
|
|
88
|
+
const tps = measurementMs === null ? null : round(outputTokens / (measurementMs / 1000), 1);
|
|
73
89
|
const validCost = Number.isFinite(costUsd) && costUsd > 0;
|
|
74
90
|
const validTokens = Number.isFinite(totalTokens) && totalTokens > 0;
|
|
75
91
|
return {
|
|
@@ -80,9 +96,7 @@ export function aggregateAgentTurns(turns, live, startMs, now) {
|
|
|
80
96
|
outputTokens,
|
|
81
97
|
stallMs,
|
|
82
98
|
stallCount,
|
|
83
|
-
rateUsdPerMTokens: validCost && validTokens
|
|
84
|
-
? round(costUsd / (totalTokens / 1_000_000), 2)
|
|
85
|
-
: null,
|
|
99
|
+
rateUsdPerMTokens: validCost && validTokens ? round(costUsd / (totalTokens / 1_000_000), 2) : null,
|
|
86
100
|
generationMs,
|
|
87
101
|
totalTokens,
|
|
88
102
|
costUsd: validCost ? costUsd : 0,
|
|
@@ -99,9 +113,7 @@ export function aggregateAgentTurns(turns, live, startMs, now) {
|
|
|
99
113
|
*/
|
|
100
114
|
export function capInputForIdle(input, contextTokens, sessionTotalInput) {
|
|
101
115
|
let capped = input;
|
|
102
|
-
if (typeof contextTokens === "number" &&
|
|
103
|
-
Number.isFinite(contextTokens) &&
|
|
104
|
-
contextTokens > 0) {
|
|
116
|
+
if (typeof contextTokens === "number" && Number.isFinite(contextTokens) && contextTokens > 0) {
|
|
105
117
|
capped = Math.min(capped, contextTokens);
|
|
106
118
|
}
|
|
107
119
|
if (sessionTotalInput > 0) {
|
|
@@ -111,9 +123,7 @@ export function capInputForIdle(input, contextTokens, sessionTotalInput) {
|
|
|
111
123
|
}
|
|
112
124
|
export function capInputForLive(input, contextTokens) {
|
|
113
125
|
let capped = input;
|
|
114
|
-
if (typeof contextTokens === "number" &&
|
|
115
|
-
Number.isFinite(contextTokens) &&
|
|
116
|
-
contextTokens > 0) {
|
|
126
|
+
if (typeof contextTokens === "number" && Number.isFinite(contextTokens) && contextTokens > 0) {
|
|
117
127
|
capped = Math.min(capped, contextTokens);
|
|
118
128
|
}
|
|
119
129
|
return Math.max(0, capped);
|
|
@@ -131,6 +141,10 @@ export class AgentRunLedger {
|
|
|
131
141
|
turns = [];
|
|
132
142
|
startMs = null;
|
|
133
143
|
now;
|
|
144
|
+
// Incremental per-agent_run accumulation (Q1/Q6/Q9)
|
|
145
|
+
triggerTokens = 0;
|
|
146
|
+
accumOutputTokens = 0;
|
|
147
|
+
accumToolTokens = 0;
|
|
134
148
|
constructor(now = () => Date.now()) {
|
|
135
149
|
this.now = now;
|
|
136
150
|
}
|
|
@@ -144,10 +158,52 @@ export class AgentRunLedger {
|
|
|
144
158
|
startRun(startMs) {
|
|
145
159
|
this.startMs = typeof startMs === "number" ? startMs : this.now();
|
|
146
160
|
this.turns = [];
|
|
161
|
+
this.triggerTokens = 0;
|
|
162
|
+
this.accumOutputTokens = 0;
|
|
163
|
+
this.accumToolTokens = 0;
|
|
164
|
+
}
|
|
165
|
+
/** Set trigger message tokens for this agent_run (chars/4, or 0 per Q11 a). */
|
|
166
|
+
setTriggerTokens(tokens) {
|
|
167
|
+
if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens < 0)
|
|
168
|
+
return;
|
|
169
|
+
this.triggerTokens = Math.round(tokens);
|
|
170
|
+
}
|
|
171
|
+
getTriggerTokens() {
|
|
172
|
+
return this.triggerTokens;
|
|
173
|
+
}
|
|
174
|
+
/** Add tool result tokens (chars/4) to accumulation (Q8 a). */
|
|
175
|
+
addToolResultTokens(tokens) {
|
|
176
|
+
if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens <= 0)
|
|
177
|
+
return;
|
|
178
|
+
this.accumToolTokens += Math.round(tokens);
|
|
179
|
+
}
|
|
180
|
+
getAccumOutputTokens() {
|
|
181
|
+
return this.accumOutputTokens;
|
|
182
|
+
}
|
|
183
|
+
getAccumToolTokens() {
|
|
184
|
+
return this.accumToolTokens;
|
|
185
|
+
}
|
|
186
|
+
/** Synthetic completed input without live delta: trigger + Σ(outputs) + Σ(tools). */
|
|
187
|
+
getSyntheticCompletedInput() {
|
|
188
|
+
return this.triggerTokens + this.accumOutputTokens + this.accumToolTokens;
|
|
189
|
+
}
|
|
190
|
+
/** Synthetic live input including live delta: trigger + Σ(outputs+tools) + liveDelta. */
|
|
191
|
+
getSyntheticLiveInput(liveDeltaTokens) {
|
|
192
|
+
const d = typeof liveDeltaTokens === "number" && Number.isFinite(liveDeltaTokens) ? liveDeltaTokens : 0;
|
|
193
|
+
return (this.triggerTokens +
|
|
194
|
+
this.accumOutputTokens +
|
|
195
|
+
this.accumToolTokens +
|
|
196
|
+
Math.max(0, Math.round(d)));
|
|
147
197
|
}
|
|
148
198
|
/** Record a completed turn's telemetry (called by telemetryTracker or directly). */
|
|
149
199
|
recordTurn(turn) {
|
|
150
200
|
this.turns.push({ ...turn });
|
|
201
|
+
// Accumulate authoritative output for synthetic input (trigger + Σ outputs)
|
|
202
|
+
if (typeof turn.outputTokens === "number" &&
|
|
203
|
+
Number.isFinite(turn.outputTokens) &&
|
|
204
|
+
turn.outputTokens > 0) {
|
|
205
|
+
this.accumOutputTokens += Math.round(turn.outputTokens);
|
|
206
|
+
}
|
|
151
207
|
}
|
|
152
208
|
/** Settled totals without live turn — used at agent_settled for timeline. */
|
|
153
209
|
getSettledTotals(now) {
|
|
@@ -231,9 +287,46 @@ export class AgentRunLedger {
|
|
|
231
287
|
estimated: false,
|
|
232
288
|
};
|
|
233
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Idle authoritative display (hybrid Q7 b): snapshot delta capped, for use when
|
|
292
|
+
* not running to show billed total without ~ after agent_end.
|
|
293
|
+
*/
|
|
294
|
+
getIdleAuthoritativeDisplay(snapshotTotals, contextTokens) {
|
|
295
|
+
let displayInput;
|
|
296
|
+
let displayOutput;
|
|
297
|
+
let displayCost;
|
|
298
|
+
if (this.baseline) {
|
|
299
|
+
const d = deltaFromBaseline(snapshotTotals, this.baseline);
|
|
300
|
+
displayInput = d.input;
|
|
301
|
+
displayOutput = d.output;
|
|
302
|
+
displayCost = d.cost;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
displayInput = snapshotTotals.input;
|
|
306
|
+
displayOutput = snapshotTotals.output;
|
|
307
|
+
displayCost = snapshotTotals.cost;
|
|
308
|
+
}
|
|
309
|
+
const cappedInput = capInputForIdle(displayInput, contextTokens, snapshotTotals.input);
|
|
310
|
+
return {
|
|
311
|
+
tps: null,
|
|
312
|
+
ttftMs: 0,
|
|
313
|
+
totalMs: 0,
|
|
314
|
+
inputTokens: cappedInput,
|
|
315
|
+
outputTokens: displayOutput,
|
|
316
|
+
stallMs: 0,
|
|
317
|
+
stallCount: 0,
|
|
318
|
+
rateUsdPerMTokens: null,
|
|
319
|
+
generationMs: 0,
|
|
320
|
+
totalTokens: cappedInput + displayOutput,
|
|
321
|
+
costUsd: displayCost,
|
|
322
|
+
measurementMs: null,
|
|
323
|
+
estimated: false,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
234
326
|
/**
|
|
235
327
|
* Live display totals when running: liveTurn input (current window) replaces
|
|
236
328
|
* agentLive input, output/cost remain per-agent sum, capped to live context only.
|
|
329
|
+
* @deprecated — retained for backward compat; prefer getIncrementalLiveDisplayTotals for Q1/Q9.
|
|
237
330
|
*/
|
|
238
331
|
getLiveDisplayTotals(liveTurn, agentLive, contextTokens) {
|
|
239
332
|
if (!agentLive && !liveTurn)
|
|
@@ -260,10 +353,53 @@ export class AgentRunLedger {
|
|
|
260
353
|
estimated: true,
|
|
261
354
|
};
|
|
262
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Incremental live display (Q1/Q6/Q9): trigger + Σ(outputs+tools) + liveDelta, capped to live.
|
|
358
|
+
* Replaces max(input) with synthetic accumulation, per-agent_run independent.
|
|
359
|
+
*/
|
|
360
|
+
getIncrementalLiveDisplayTotals(liveTurn, agentLive, contextTokens) {
|
|
361
|
+
if (!agentLive && !liveTurn)
|
|
362
|
+
return null;
|
|
363
|
+
const liveDelta = liveTurn?.outputTokens ?? 0;
|
|
364
|
+
const syntheticInput = this.getSyntheticLiveInput(liveDelta);
|
|
365
|
+
const cappedInput = capInputForLive(syntheticInput, contextTokens);
|
|
366
|
+
// Output/cost/stalls come from agentLive (sum) when available, else liveTurn
|
|
367
|
+
const base = agentLive ?? liveTurn;
|
|
368
|
+
if (!base)
|
|
369
|
+
return null;
|
|
370
|
+
// When both exist, agentLive already sums outputs, so use it; else liveTurn
|
|
371
|
+
const displayOutput = agentLive ? agentLive.outputTokens : liveTurn.outputTokens;
|
|
372
|
+
const displayCost = agentLive ? agentLive.costUsd : liveTurn.costUsd;
|
|
373
|
+
const displayStallMs = agentLive ? agentLive.stallMs : liveTurn.stallMs;
|
|
374
|
+
const displayStallCount = agentLive ? agentLive.stallCount : liveTurn.stallCount;
|
|
375
|
+
const displayGenerationMs = agentLive ? agentLive.generationMs : liveTurn.generationMs;
|
|
376
|
+
const displayTtft = agentLive ? agentLive.ttftMs : liveTurn.ttftMs;
|
|
377
|
+
const displayTps = agentLive ? agentLive.tps : liveTurn.tps;
|
|
378
|
+
const displayTotalMs = agentLive ? agentLive.totalMs : liveTurn.totalMs;
|
|
379
|
+
const displayMeasurementMs = agentLive ? agentLive.measurementMs : liveTurn.measurementMs;
|
|
380
|
+
return {
|
|
381
|
+
tps: displayTps,
|
|
382
|
+
ttftMs: displayTtft,
|
|
383
|
+
totalMs: displayTotalMs,
|
|
384
|
+
inputTokens: cappedInput,
|
|
385
|
+
outputTokens: displayOutput,
|
|
386
|
+
stallMs: displayStallMs,
|
|
387
|
+
stallCount: displayStallCount,
|
|
388
|
+
rateUsdPerMTokens: base.rateUsdPerMTokens,
|
|
389
|
+
generationMs: displayGenerationMs,
|
|
390
|
+
totalTokens: cappedInput + displayOutput,
|
|
391
|
+
costUsd: displayCost,
|
|
392
|
+
measurementMs: displayMeasurementMs,
|
|
393
|
+
estimated: true,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
263
396
|
reset() {
|
|
264
397
|
this.baseline = null;
|
|
265
398
|
this.turns = [];
|
|
266
399
|
this.startMs = null;
|
|
400
|
+
this.triggerTokens = 0;
|
|
401
|
+
this.accumOutputTokens = 0;
|
|
402
|
+
this.accumToolTokens = 0;
|
|
267
403
|
}
|
|
268
404
|
isActive() {
|
|
269
405
|
return this.startMs !== null;
|
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,7 +30,8 @@ export class BorderRenderer {
|
|
|
30
30
|
const info = this.getModelInfo();
|
|
31
31
|
const glow = (s) => {
|
|
32
32
|
try {
|
|
33
|
-
const maybeGlow =
|
|
33
|
+
const maybeGlow = // SAFETY: intentional unsafe cast — validated at runtime
|
|
34
|
+
theme.getThinkingBorderColor;
|
|
34
35
|
if (typeof maybeGlow === "function")
|
|
35
36
|
return maybeGlow.call(theme, info.level)(s);
|
|
36
37
|
}
|
|
@@ -79,7 +80,7 @@ export class BorderRenderer {
|
|
|
79
80
|
if (tokensLeft) {
|
|
80
81
|
// Use | as separator between ↑/↓ and tool turns (dimmed) per user request
|
|
81
82
|
// SAFETY: pi theme seam — fg is optional theme method
|
|
82
|
-
const themeForTokens = this.getLiveTheme();
|
|
83
|
+
const themeForTokens = this.getLiveTheme(); // SAFETY: intentional unsafe cast — validated at runtime
|
|
83
84
|
const fg = themeForTokens.fg;
|
|
84
85
|
const dimPipe = typeof fg === "function" ? fg.call(themeForTokens, "dim", " | ") : " | ";
|
|
85
86
|
const combined = tokensRight ? `${tokensLeft}${dimPipe}${tokensRight}` : tokensLeft;
|
|
@@ -91,7 +92,8 @@ export class BorderRenderer {
|
|
|
91
92
|
const theme = this.getLiveTheme();
|
|
92
93
|
const glow = (s) => {
|
|
93
94
|
try {
|
|
94
|
-
const maybeGlow =
|
|
95
|
+
const maybeGlow = // SAFETY: intentional unsafe cast — validated at runtime
|
|
96
|
+
theme.getThinkingBorderColor;
|
|
95
97
|
if (typeof maybeGlow === "function")
|
|
96
98
|
return maybeGlow.call(theme, this.getModelInfo().level)(s);
|
|
97
99
|
}
|
|
@@ -132,12 +134,8 @@ function embedTopWithLeftAndRight(lines, width, leftText, rightText, getGlow) {
|
|
|
132
134
|
displayLeft = truncateToWidth(leftText, maxLeft, "");
|
|
133
135
|
if (rightW > maxRight)
|
|
134
136
|
displayRight = truncateToWidth(rightText, maxRight, "");
|
|
135
|
-
const leftSegment = displayLeft
|
|
136
|
-
|
|
137
|
-
: getGlow("─");
|
|
138
|
-
const rightSegment = displayRight
|
|
139
|
-
? ` ${displayRight} ${getGlow("─")}`
|
|
140
|
-
: getGlow("─");
|
|
137
|
+
const leftSegment = displayLeft ? `${getGlow("─")} ${displayLeft} ` : getGlow("─");
|
|
138
|
+
const rightSegment = displayRight ? ` ${displayRight} ${getGlow("─")}` : getGlow("─");
|
|
141
139
|
const used = visibleWidth(leftSegment) + visibleWidth(rightSegment);
|
|
142
140
|
const middleWidth = Math.max(0, width - used);
|
|
143
141
|
const middle = getGlow("─".repeat(middleWidth));
|
|
@@ -170,12 +168,8 @@ function embedBottomBorder(lines, width, leftText, rightText, getGlow) {
|
|
|
170
168
|
displayLeft = truncateToWidth(leftText, maxLeft, "");
|
|
171
169
|
if (rightW > maxRight)
|
|
172
170
|
displayRight = truncateToWidth(rightText, maxRight, "");
|
|
173
|
-
const leftSegment = displayLeft
|
|
174
|
-
|
|
175
|
-
: getGlow("─");
|
|
176
|
-
const rightSegment = displayRight
|
|
177
|
-
? ` ${displayRight} ${getGlow("─")}`
|
|
178
|
-
: getGlow("─");
|
|
171
|
+
const leftSegment = displayLeft ? `${getGlow("─")} ${displayLeft} ` : getGlow("─");
|
|
172
|
+
const rightSegment = displayRight ? ` ${displayRight} ${getGlow("─")}` : getGlow("─");
|
|
179
173
|
const used = visibleWidth(leftSegment) + visibleWidth(rightSegment);
|
|
180
174
|
const middleWidth = Math.max(0, width - used);
|
|
181
175
|
const middle = getGlow("─".repeat(middleWidth));
|
|
@@ -20,7 +20,7 @@
|
|
|
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
24
|
/**
|
|
25
25
|
* Read the live pi theme into a typed { fg } surface. The cast is a SAFETY seam — a pi theme
|
|
26
26
|
* missing `fg` degrades to identity rather than throwing, keeping the chrome resilient.
|
package/dist/chrome-state.js
CHANGED
|
@@ -45,9 +45,7 @@ export function formatContextBar(contextUsage, theme, glyphs, isAscii, barWidth
|
|
|
45
45
|
const base = showIconBar
|
|
46
46
|
? `${theme.fg(contextColor, glyphs.context)} ${renderBar(theme, contextPct, barWidth, isAscii)} ${baseCore}`
|
|
47
47
|
: baseCore;
|
|
48
|
-
const rate = cacheHitRate !== undefined && Number.isFinite(cacheHitRate)
|
|
49
|
-
? cacheHitRate
|
|
50
|
-
: 0;
|
|
48
|
+
const rate = cacheHitRate !== undefined && Number.isFinite(cacheHitRate) ? cacheHitRate : 0;
|
|
51
49
|
const cacheText = `${glyphs.cacheHit} ${rate.toFixed(1)}%`;
|
|
52
50
|
return `${theme.fg(cacheHitColor(rate), cacheText)} ${theme.fg("dim", "|")} ${base}`;
|
|
53
51
|
}
|
|
@@ -66,13 +64,17 @@ export function formatTopContextFromSnapshot(snapshot, theme, glyphs, isAscii, s
|
|
|
66
64
|
export function createChromeSnapshot(ctx, footerState) {
|
|
67
65
|
const cwd = ctx?.sessionManager?.getCwd?.() ??
|
|
68
66
|
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
69
|
-
|
|
67
|
+
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
68
|
+
// SAFETY: intentional unsafe cast — validated at runtime
|
|
69
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ ctx?.cwd ?? // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
70
70
|
process.cwd();
|
|
71
71
|
const sessionName = ctx?.sessionManager?.getSessionName?.();
|
|
72
72
|
const contextUsage = ctx?.getContextUsage?.();
|
|
73
73
|
const totals = getUsageTotals(
|
|
74
|
+
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
74
75
|
// SAFETY: pi seam
|
|
75
|
-
(ctx ??
|
|
76
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ (ctx ??
|
|
77
|
+
{}));
|
|
76
78
|
// footerState may be absent when called from LiveBorder (context-only); use empty defaults
|
|
77
79
|
// SAFETY: pi seam
|
|
78
80
|
const git = footerState?.git ??
|
|
@@ -88,6 +90,9 @@ export function createChromeSnapshot(ctx, footerState) {
|
|
|
88
90
|
renamed: 0,
|
|
89
91
|
deleted: 0,
|
|
90
92
|
commit: null,
|
|
93
|
+
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
94
|
+
// SAFETY: intentional unsafe cast — validated at runtime
|
|
95
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */
|
|
91
96
|
}; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
92
97
|
const runtime = footerState?.runtime ?? null;
|
|
93
98
|
return {
|
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 };
|
|
@@ -99,6 +97,7 @@ export const CONFIG_SCHEMA = [
|
|
|
99
97
|
{ path: "timeline.tokens", kind: "boolean" },
|
|
100
98
|
{ path: "timeline.cost", kind: "boolean" },
|
|
101
99
|
];
|
|
100
|
+
// SAFETY: table-driven config access — path validated against CONFIG_SCHEMA, caller validates via validate()
|
|
102
101
|
function getByPath(obj, path) {
|
|
103
102
|
const parts = path.split(".");
|
|
104
103
|
let cur = obj;
|
|
@@ -123,6 +122,7 @@ function setByPath(obj, path, value) {
|
|
|
123
122
|
}
|
|
124
123
|
cur[parts[parts.length - 1]] = value;
|
|
125
124
|
}
|
|
125
|
+
// SAFETY: table-driven config access — path validated against CONFIG_SCHEMA, caller validates via validate()
|
|
126
126
|
function getDefaultByPath(path) {
|
|
127
127
|
return getByPath(DEFAULT_CONFIG, path);
|
|
128
128
|
}
|
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
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* DetailChrome — deep module owning the whole Detail-window flow behind one seam.
|
|
3
|
+
*
|
|
4
|
+
* Previously the Highlight → Detail window flow was fragmented: TrackingEditor
|
|
5
|
+
* observed the Candidate highlight, src/index.ts held widget state
|
|
6
|
+
* (currentItem/scrollOffset/lastWidth), scroll math, and header-marker
|
|
7
|
+
* re-parsing (contentLinesFrom); src/detail-render.ts owned wrapping;
|
|
8
|
+
* src/window-presentation.ts owned the border. Fixing a wrapping bug required
|
|
9
|
+
* holding 3 modules in one head — no locality.
|
|
10
|
+
*
|
|
11
|
+
* Depth: small interface (setItem / getItem / scrollBy / render) hides scroll
|
|
12
|
+
* offset, width, MAX_LINES, wrapping, border, and header-marker parsing inside.
|
|
13
|
+
* detail-render + window-presentation are INTERNAL seams, not part of the
|
|
14
|
+
* interface. One place to learn — one place to test, no TUI needed.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as os from "node:os";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { contentLineCount, renderDetail, scroll } from "./detail-render.js";
|
|
2
20
|
import { decorateWindow } from "./window-presentation.js";
|
|
3
21
|
/** Height cap of the detail window (the user's spec: up to 5 lines). */
|
|
4
22
|
export const MAX_LINES = 5;
|
|
@@ -6,12 +24,107 @@ export const MAX_LINES = 5;
|
|
|
6
24
|
function kindOf(value) {
|
|
7
25
|
return value.startsWith("skill:") ? "skill" : "command";
|
|
8
26
|
}
|
|
27
|
+
function resolveSkillPath(skillName) {
|
|
28
|
+
const name = skillName.trim();
|
|
29
|
+
if (!name)
|
|
30
|
+
return undefined;
|
|
31
|
+
const candidates = [];
|
|
32
|
+
const cwd = process.cwd();
|
|
33
|
+
const home = (() => {
|
|
34
|
+
try {
|
|
35
|
+
return os.homedir();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
// project-local candidates
|
|
42
|
+
candidates.push(path.join(cwd, ".pi", "agent", "skills", name, "SKILL.md"));
|
|
43
|
+
candidates.push(path.join(cwd, ".agents", "skills", name, "SKILL.md"));
|
|
44
|
+
candidates.push(path.join(cwd, ".pi", "skills", name, "SKILL.md"));
|
|
45
|
+
candidates.push(path.join(cwd, "skills", name, "SKILL.md"));
|
|
46
|
+
// user-global candidates
|
|
47
|
+
if (home) {
|
|
48
|
+
candidates.push(path.join(home, ".pi", "agent", "skills", name, "SKILL.md"));
|
|
49
|
+
candidates.push(path.join(home, ".agents", "skills", name, "SKILL.md"));
|
|
50
|
+
candidates.push(path.join(home, ".pi", ".agents", "skills", name, "SKILL.md"));
|
|
51
|
+
candidates.push(path.join(home, ".pi", "skills", name, "SKILL.md"));
|
|
52
|
+
candidates.push(path.join(home, "stowfiles", "dotfiles", ".pi", "agent", "skills", name, "SKILL.md"));
|
|
53
|
+
candidates.push(path.join(home, "stowfiles", "dotfiles", ".pi", "agent", ".agents", "skills", name, "SKILL.md"));
|
|
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
|
+
}
|
|
77
|
+
for (const p of candidates) {
|
|
78
|
+
try {
|
|
79
|
+
if (fs.existsSync(p))
|
|
80
|
+
return p;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// ignore
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// fallback to global symlink path so header always shows a path for skills
|
|
87
|
+
if (home)
|
|
88
|
+
return path.join(home, ".pi", "agent", "skills", name, "SKILL.md");
|
|
89
|
+
return path.join(cwd, ".pi", "agent", "skills", name, "SKILL.md");
|
|
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
|
+
}
|
|
9
106
|
function detailItemOf(item) {
|
|
10
|
-
|
|
107
|
+
const detail = {
|
|
11
108
|
label: item.label,
|
|
12
109
|
kind: kindOf(item.value),
|
|
13
110
|
description: item.description ?? "",
|
|
14
111
|
};
|
|
112
|
+
const rawPath = item.path?.trim() ?? "";
|
|
113
|
+
if (rawPath !== "") {
|
|
114
|
+
detail.path = rawPath;
|
|
115
|
+
}
|
|
116
|
+
else if (item.value.startsWith("skill:")) {
|
|
117
|
+
const skillName = item.value.slice("skill:".length);
|
|
118
|
+
const resolved = resolveSkillPath(skillName);
|
|
119
|
+
if (resolved !== undefined)
|
|
120
|
+
detail.path = resolved;
|
|
121
|
+
}
|
|
122
|
+
if (detail.kind === "skill" && detail.path) {
|
|
123
|
+
const inv = parseInvocation(detail.path);
|
|
124
|
+
if (inv)
|
|
125
|
+
detail.invocation = inv;
|
|
126
|
+
}
|
|
127
|
+
return detail;
|
|
15
128
|
}
|
|
16
129
|
export class DetailChrome {
|
|
17
130
|
item = null;
|
|
@@ -67,6 +180,8 @@ export class DetailChrome {
|
|
|
67
180
|
border: (s) => t.fg("border", s),
|
|
68
181
|
highlight: (s) => t.fg("accent", t.bold(s)),
|
|
69
182
|
dim: (s) => t.fg("dim", s),
|
|
183
|
+
model: (s) => t.fg("success", s),
|
|
184
|
+
human: (s) => t.fg("warning", s),
|
|
70
185
|
};
|
|
71
186
|
const innerWidth = Math.max(1, width - 4);
|
|
72
187
|
const lines = renderDetail(detailItemOf(item), innerWidth, this.maxLines, this.scrollOffset);
|