pi-editor-footer 0.8.0 → 0.11.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 +26 -0
- package/dist/agent-run-ledger.js +155 -1
- package/dist/border-renderer.js +7 -3
- package/dist/chrome-state.js +8 -2
- package/dist/config.js +2 -0
- package/dist/detail-chrome.js +72 -1
- package/dist/detail-render.js +72 -8
- package/dist/footer.js +14 -6
- package/dist/live-border.js +9 -7
- package/dist/session-orchestrator.js +111 -16
- package/dist/tracking-editor.js +8 -3
- package/dist/transcript-timeline.js +66 -26
- package/dist/window-presentation.js +14 -1
- package/package.json +11 -3
- package/src/agent-run-ledger.ts +177 -1
- package/src/border-renderer.ts +5 -3
- package/src/chrome-state.ts +8 -3
- package/src/config.ts +7 -4
- package/src/detail-chrome.ts +81 -2
- package/src/detail-render.ts +69 -12
- package/src/footer.ts +13 -6
- package/src/live-border.ts +11 -10
- package/src/session-orchestrator.ts +103 -22
- package/src/tracking-editor.ts +11 -3
- package/src/transcript-timeline.ts +139 -52
- package/src/window-presentation.ts +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.11.0] - 2026-08-29
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Release workflow trigger clarified — tag `v*` remains the release trigger (semantic-release on tag push), reverted tentative `main` trigger
|
|
12
|
+
|
|
13
|
+
## [0.10.0] - 2026-08-28
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- 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
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Resolve pre-existing blocking diagnostics (`require-safety-comment`, `no-unknown-returns`, `zizmor cache-poisoning`) to keep lens clean
|
|
22
|
+
|
|
23
|
+
## [0.9.0] - 2026-08-27
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- 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)
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- `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)
|
|
32
|
+
|
|
7
33
|
## [0.8.0] - 2026-08-26
|
|
8
34
|
|
|
9
35
|
### Added
|
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,7 +83,7 @@ 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
88
|
const tps = measurementMs === null
|
|
71
89
|
? null
|
|
@@ -131,6 +149,10 @@ export class AgentRunLedger {
|
|
|
131
149
|
turns = [];
|
|
132
150
|
startMs = null;
|
|
133
151
|
now;
|
|
152
|
+
// Incremental per-agent_run accumulation (Q1/Q6/Q9)
|
|
153
|
+
triggerTokens = 0;
|
|
154
|
+
accumOutputTokens = 0;
|
|
155
|
+
accumToolTokens = 0;
|
|
134
156
|
constructor(now = () => Date.now()) {
|
|
135
157
|
this.now = now;
|
|
136
158
|
}
|
|
@@ -144,10 +166,54 @@ export class AgentRunLedger {
|
|
|
144
166
|
startRun(startMs) {
|
|
145
167
|
this.startMs = typeof startMs === "number" ? startMs : this.now();
|
|
146
168
|
this.turns = [];
|
|
169
|
+
this.triggerTokens = 0;
|
|
170
|
+
this.accumOutputTokens = 0;
|
|
171
|
+
this.accumToolTokens = 0;
|
|
172
|
+
}
|
|
173
|
+
/** Set trigger message tokens for this agent_run (chars/4, or 0 per Q11 a). */
|
|
174
|
+
setTriggerTokens(tokens) {
|
|
175
|
+
if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens < 0)
|
|
176
|
+
return;
|
|
177
|
+
this.triggerTokens = Math.round(tokens);
|
|
178
|
+
}
|
|
179
|
+
getTriggerTokens() {
|
|
180
|
+
return this.triggerTokens;
|
|
181
|
+
}
|
|
182
|
+
/** Add tool result tokens (chars/4) to accumulation (Q8 a). */
|
|
183
|
+
addToolResultTokens(tokens) {
|
|
184
|
+
if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens <= 0)
|
|
185
|
+
return;
|
|
186
|
+
this.accumToolTokens += Math.round(tokens);
|
|
187
|
+
}
|
|
188
|
+
getAccumOutputTokens() {
|
|
189
|
+
return this.accumOutputTokens;
|
|
190
|
+
}
|
|
191
|
+
getAccumToolTokens() {
|
|
192
|
+
return this.accumToolTokens;
|
|
193
|
+
}
|
|
194
|
+
/** Synthetic completed input without live delta: trigger + Σ(outputs) + Σ(tools). */
|
|
195
|
+
getSyntheticCompletedInput() {
|
|
196
|
+
return this.triggerTokens + this.accumOutputTokens + this.accumToolTokens;
|
|
197
|
+
}
|
|
198
|
+
/** Synthetic live input including live delta: trigger + Σ(outputs+tools) + liveDelta. */
|
|
199
|
+
getSyntheticLiveInput(liveDeltaTokens) {
|
|
200
|
+
const d = typeof liveDeltaTokens === "number" && Number.isFinite(liveDeltaTokens)
|
|
201
|
+
? liveDeltaTokens
|
|
202
|
+
: 0;
|
|
203
|
+
return (this.triggerTokens +
|
|
204
|
+
this.accumOutputTokens +
|
|
205
|
+
this.accumToolTokens +
|
|
206
|
+
Math.max(0, Math.round(d)));
|
|
147
207
|
}
|
|
148
208
|
/** Record a completed turn's telemetry (called by telemetryTracker or directly). */
|
|
149
209
|
recordTurn(turn) {
|
|
150
210
|
this.turns.push({ ...turn });
|
|
211
|
+
// Accumulate authoritative output for synthetic input (trigger + Σ outputs)
|
|
212
|
+
if (typeof turn.outputTokens === "number" &&
|
|
213
|
+
Number.isFinite(turn.outputTokens) &&
|
|
214
|
+
turn.outputTokens > 0) {
|
|
215
|
+
this.accumOutputTokens += Math.round(turn.outputTokens);
|
|
216
|
+
}
|
|
151
217
|
}
|
|
152
218
|
/** Settled totals without live turn — used at agent_settled for timeline. */
|
|
153
219
|
getSettledTotals(now) {
|
|
@@ -231,9 +297,46 @@ export class AgentRunLedger {
|
|
|
231
297
|
estimated: false,
|
|
232
298
|
};
|
|
233
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Idle authoritative display (hybrid Q7 b): snapshot delta capped, for use when
|
|
302
|
+
* not running to show billed total without ~ after agent_end.
|
|
303
|
+
*/
|
|
304
|
+
getIdleAuthoritativeDisplay(snapshotTotals, contextTokens) {
|
|
305
|
+
let displayInput;
|
|
306
|
+
let displayOutput;
|
|
307
|
+
let displayCost;
|
|
308
|
+
if (this.baseline) {
|
|
309
|
+
const d = deltaFromBaseline(snapshotTotals, this.baseline);
|
|
310
|
+
displayInput = d.input;
|
|
311
|
+
displayOutput = d.output;
|
|
312
|
+
displayCost = d.cost;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
displayInput = snapshotTotals.input;
|
|
316
|
+
displayOutput = snapshotTotals.output;
|
|
317
|
+
displayCost = snapshotTotals.cost;
|
|
318
|
+
}
|
|
319
|
+
const cappedInput = capInputForIdle(displayInput, contextTokens, snapshotTotals.input);
|
|
320
|
+
return {
|
|
321
|
+
tps: null,
|
|
322
|
+
ttftMs: 0,
|
|
323
|
+
totalMs: 0,
|
|
324
|
+
inputTokens: cappedInput,
|
|
325
|
+
outputTokens: displayOutput,
|
|
326
|
+
stallMs: 0,
|
|
327
|
+
stallCount: 0,
|
|
328
|
+
rateUsdPerMTokens: null,
|
|
329
|
+
generationMs: 0,
|
|
330
|
+
totalTokens: cappedInput + displayOutput,
|
|
331
|
+
costUsd: displayCost,
|
|
332
|
+
measurementMs: null,
|
|
333
|
+
estimated: false,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
234
336
|
/**
|
|
235
337
|
* Live display totals when running: liveTurn input (current window) replaces
|
|
236
338
|
* agentLive input, output/cost remain per-agent sum, capped to live context only.
|
|
339
|
+
* @deprecated — retained for backward compat; prefer getIncrementalLiveDisplayTotals for Q1/Q9.
|
|
237
340
|
*/
|
|
238
341
|
getLiveDisplayTotals(liveTurn, agentLive, contextTokens) {
|
|
239
342
|
if (!agentLive && !liveTurn)
|
|
@@ -260,10 +363,61 @@ export class AgentRunLedger {
|
|
|
260
363
|
estimated: true,
|
|
261
364
|
};
|
|
262
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Incremental live display (Q1/Q6/Q9): trigger + Σ(outputs+tools) + liveDelta, capped to live.
|
|
368
|
+
* Replaces max(input) with synthetic accumulation, per-agent_run independent.
|
|
369
|
+
*/
|
|
370
|
+
getIncrementalLiveDisplayTotals(liveTurn, agentLive, contextTokens) {
|
|
371
|
+
if (!agentLive && !liveTurn)
|
|
372
|
+
return null;
|
|
373
|
+
const liveDelta = liveTurn?.outputTokens ?? 0;
|
|
374
|
+
const syntheticInput = this.getSyntheticLiveInput(liveDelta);
|
|
375
|
+
const cappedInput = capInputForLive(syntheticInput, contextTokens);
|
|
376
|
+
// Output/cost/stalls come from agentLive (sum) when available, else liveTurn
|
|
377
|
+
const base = agentLive ?? liveTurn;
|
|
378
|
+
if (!base)
|
|
379
|
+
return null;
|
|
380
|
+
// When both exist, agentLive already sums outputs, so use it; else liveTurn
|
|
381
|
+
const displayOutput = agentLive
|
|
382
|
+
? agentLive.outputTokens
|
|
383
|
+
: liveTurn.outputTokens;
|
|
384
|
+
const displayCost = agentLive ? agentLive.costUsd : liveTurn.costUsd;
|
|
385
|
+
const displayStallMs = agentLive ? agentLive.stallMs : liveTurn.stallMs;
|
|
386
|
+
const displayStallCount = agentLive
|
|
387
|
+
? agentLive.stallCount
|
|
388
|
+
: liveTurn.stallCount;
|
|
389
|
+
const displayGenerationMs = agentLive
|
|
390
|
+
? agentLive.generationMs
|
|
391
|
+
: liveTurn.generationMs;
|
|
392
|
+
const displayTtft = agentLive ? agentLive.ttftMs : liveTurn.ttftMs;
|
|
393
|
+
const displayTps = agentLive ? agentLive.tps : liveTurn.tps;
|
|
394
|
+
const displayTotalMs = agentLive ? agentLive.totalMs : liveTurn.totalMs;
|
|
395
|
+
const displayMeasurementMs = agentLive
|
|
396
|
+
? agentLive.measurementMs
|
|
397
|
+
: liveTurn.measurementMs;
|
|
398
|
+
return {
|
|
399
|
+
tps: displayTps,
|
|
400
|
+
ttftMs: displayTtft,
|
|
401
|
+
totalMs: displayTotalMs,
|
|
402
|
+
inputTokens: cappedInput,
|
|
403
|
+
outputTokens: displayOutput,
|
|
404
|
+
stallMs: displayStallMs,
|
|
405
|
+
stallCount: displayStallCount,
|
|
406
|
+
rateUsdPerMTokens: base.rateUsdPerMTokens,
|
|
407
|
+
generationMs: displayGenerationMs,
|
|
408
|
+
totalTokens: cappedInput + displayOutput,
|
|
409
|
+
costUsd: displayCost,
|
|
410
|
+
measurementMs: displayMeasurementMs,
|
|
411
|
+
estimated: true,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
263
414
|
reset() {
|
|
264
415
|
this.baseline = null;
|
|
265
416
|
this.turns = [];
|
|
266
417
|
this.startMs = null;
|
|
418
|
+
this.triggerTokens = 0;
|
|
419
|
+
this.accumOutputTokens = 0;
|
|
420
|
+
this.accumToolTokens = 0;
|
|
267
421
|
}
|
|
268
422
|
isActive() {
|
|
269
423
|
return this.startMs !== null;
|
package/dist/border-renderer.js
CHANGED
|
@@ -30,7 +30,9 @@ export class BorderRenderer {
|
|
|
30
30
|
const info = this.getModelInfo();
|
|
31
31
|
const glow = (s) => {
|
|
32
32
|
try {
|
|
33
|
-
const maybeGlow =
|
|
33
|
+
const maybeGlow =
|
|
34
|
+
// SAFETY: intentional unsafe cast — validated at runtime
|
|
35
|
+
theme.getThinkingBorderColor;
|
|
34
36
|
if (typeof maybeGlow === "function")
|
|
35
37
|
return maybeGlow.call(theme, info.level)(s);
|
|
36
38
|
}
|
|
@@ -79,7 +81,7 @@ export class BorderRenderer {
|
|
|
79
81
|
if (tokensLeft) {
|
|
80
82
|
// Use | as separator between ↑/↓ and tool turns (dimmed) per user request
|
|
81
83
|
// SAFETY: pi theme seam — fg is optional theme method
|
|
82
|
-
const themeForTokens = this.getLiveTheme();
|
|
84
|
+
const themeForTokens = this.getLiveTheme(); // SAFETY: intentional unsafe cast — validated at runtime
|
|
83
85
|
const fg = themeForTokens.fg;
|
|
84
86
|
const dimPipe = typeof fg === "function" ? fg.call(themeForTokens, "dim", " | ") : " | ";
|
|
85
87
|
const combined = tokensRight ? `${tokensLeft}${dimPipe}${tokensRight}` : tokensLeft;
|
|
@@ -91,7 +93,9 @@ export class BorderRenderer {
|
|
|
91
93
|
const theme = this.getLiveTheme();
|
|
92
94
|
const glow = (s) => {
|
|
93
95
|
try {
|
|
94
|
-
const maybeGlow =
|
|
96
|
+
const maybeGlow =
|
|
97
|
+
// SAFETY: intentional unsafe cast — validated at runtime
|
|
98
|
+
theme.getThinkingBorderColor;
|
|
95
99
|
if (typeof maybeGlow === "function")
|
|
96
100
|
return maybeGlow.call(theme, this.getModelInfo().level)(s);
|
|
97
101
|
}
|
package/dist/chrome-state.js
CHANGED
|
@@ -66,13 +66,16 @@ export function formatTopContextFromSnapshot(snapshot, theme, glyphs, isAscii, s
|
|
|
66
66
|
export function createChromeSnapshot(ctx, footerState) {
|
|
67
67
|
const cwd = ctx?.sessionManager?.getCwd?.() ??
|
|
68
68
|
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
69
|
-
|
|
69
|
+
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
70
|
+
// SAFETY: intentional unsafe cast — validated at runtime
|
|
71
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ ctx?.cwd ?? // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
70
72
|
process.cwd();
|
|
71
73
|
const sessionName = ctx?.sessionManager?.getSessionName?.();
|
|
72
74
|
const contextUsage = ctx?.getContextUsage?.();
|
|
73
75
|
const totals = getUsageTotals(
|
|
76
|
+
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
74
77
|
// SAFETY: pi seam
|
|
75
|
-
(ctx ?? {}));
|
|
78
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ (ctx ?? {}));
|
|
76
79
|
// footerState may be absent when called from LiveBorder (context-only); use empty defaults
|
|
77
80
|
// SAFETY: pi seam
|
|
78
81
|
const git = footerState?.git ??
|
|
@@ -88,6 +91,9 @@ export function createChromeSnapshot(ctx, footerState) {
|
|
|
88
91
|
renamed: 0,
|
|
89
92
|
deleted: 0,
|
|
90
93
|
commit: null,
|
|
94
|
+
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
95
|
+
// SAFETY: intentional unsafe cast — validated at runtime
|
|
96
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */
|
|
91
97
|
}; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
92
98
|
const runtime = footerState?.runtime ?? null;
|
|
93
99
|
return {
|
package/dist/config.js
CHANGED
|
@@ -99,6 +99,7 @@ export const CONFIG_SCHEMA = [
|
|
|
99
99
|
{ path: "timeline.tokens", kind: "boolean" },
|
|
100
100
|
{ path: "timeline.cost", kind: "boolean" },
|
|
101
101
|
];
|
|
102
|
+
// SAFETY: table-driven config access — path validated against CONFIG_SCHEMA, caller validates via validate()
|
|
102
103
|
function getByPath(obj, path) {
|
|
103
104
|
const parts = path.split(".");
|
|
104
105
|
let cur = obj;
|
|
@@ -123,6 +124,7 @@ function setByPath(obj, path, value) {
|
|
|
123
124
|
}
|
|
124
125
|
cur[parts[parts.length - 1]] = value;
|
|
125
126
|
}
|
|
127
|
+
// SAFETY: table-driven config access — path validated against CONFIG_SCHEMA, caller validates via validate()
|
|
126
128
|
function getDefaultByPath(path) {
|
|
127
129
|
return getByPath(DEFAULT_CONFIG, path);
|
|
128
130
|
}
|
package/dist/detail-chrome.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
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";
|
|
1
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). */
|
|
@@ -6,12 +24,65 @@ 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
|
+
for (const p of candidates) {
|
|
56
|
+
try {
|
|
57
|
+
if (fs.existsSync(p))
|
|
58
|
+
return p;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// ignore
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// fallback to global symlink path so header always shows a path for skills
|
|
65
|
+
if (home)
|
|
66
|
+
return path.join(home, ".pi", "agent", "skills", name, "SKILL.md");
|
|
67
|
+
return path.join(cwd, ".pi", "agent", "skills", name, "SKILL.md");
|
|
68
|
+
}
|
|
9
69
|
function detailItemOf(item) {
|
|
10
|
-
|
|
70
|
+
const detail = {
|
|
11
71
|
label: item.label,
|
|
12
72
|
kind: kindOf(item.value),
|
|
13
73
|
description: item.description ?? "",
|
|
14
74
|
};
|
|
75
|
+
const rawPath = item.path?.trim() ?? "";
|
|
76
|
+
if (rawPath !== "") {
|
|
77
|
+
detail.path = rawPath;
|
|
78
|
+
}
|
|
79
|
+
else if (item.value.startsWith("skill:")) {
|
|
80
|
+
const skillName = item.value.slice("skill:".length);
|
|
81
|
+
const resolved = resolveSkillPath(skillName);
|
|
82
|
+
if (resolved !== undefined)
|
|
83
|
+
detail.path = resolved;
|
|
84
|
+
}
|
|
85
|
+
return detail;
|
|
15
86
|
}
|
|
16
87
|
export class DetailChrome {
|
|
17
88
|
item = null;
|
package/dist/detail-render.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* lines to render in the 5-line detail window above the input box.
|
|
6
6
|
* No pi imports — this is the single testable seam of the extension.
|
|
7
7
|
*/
|
|
8
|
+
import * as os from "node:os";
|
|
8
9
|
/**
|
|
9
10
|
* Rendered lines for the detail window: `[header, ...contentLines]`.
|
|
10
11
|
*
|
|
@@ -36,16 +37,40 @@ export function renderDetail(item, width, maxLines, scrollOffset) {
|
|
|
36
37
|
visibleLines[visibleLines.length - 1] = "...";
|
|
37
38
|
}
|
|
38
39
|
const overflows = contentLines.length > capacity;
|
|
39
|
-
const
|
|
40
|
+
const base = `${item.label} · ${item.kind}`;
|
|
41
|
+
const marker = overflows ? ` ${offset + 1}/${contentLines.length}` : "";
|
|
42
|
+
const markerLen = marker.length;
|
|
40
43
|
let header;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
44
|
+
const rawPath = item.path?.trim() ?? "";
|
|
45
|
+
if (rawPath !== "") {
|
|
46
|
+
const normalized = normalizePath(rawPath);
|
|
47
|
+
const availableForPath = Math.max(0, wrapWidth - base.length - markerLen - 2);
|
|
48
|
+
let pathSeg = "";
|
|
49
|
+
if (availableForPath >= 10) {
|
|
50
|
+
const truncated = normalized.length <= availableForPath
|
|
51
|
+
? normalized
|
|
52
|
+
: truncateMiddle(normalized, availableForPath);
|
|
53
|
+
pathSeg = ` ${truncated}`;
|
|
54
|
+
}
|
|
55
|
+
const totalLen = base.length + pathSeg.length + markerLen;
|
|
56
|
+
if (totalLen <= wrapWidth) {
|
|
57
|
+
header = base + pathSeg + marker;
|
|
58
|
+
}
|
|
59
|
+
else if (pathSeg !== "") {
|
|
60
|
+
const baseWidth = Math.max(0, wrapWidth - pathSeg.length - markerLen);
|
|
61
|
+
header = truncateToWidth(base, baseWidth) + pathSeg + marker;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const nameWidth = Math.max(0, wrapWidth - markerLen);
|
|
65
|
+
header = truncateToWidth(base, nameWidth) + marker;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (overflows) {
|
|
69
|
+
const nameWidth = Math.max(0, wrapWidth - markerLen);
|
|
70
|
+
header = truncateToWidth(base, nameWidth) + marker;
|
|
46
71
|
}
|
|
47
72
|
else {
|
|
48
|
-
header = truncateToWidth(
|
|
73
|
+
header = truncateToWidth(base, wrapWidth);
|
|
49
74
|
}
|
|
50
75
|
return [header, ...visibleLines];
|
|
51
76
|
}
|
|
@@ -58,7 +83,8 @@ export function contentLineCount(item, width) {
|
|
|
58
83
|
if (!item || item.description.trim() === "") {
|
|
59
84
|
return 0;
|
|
60
85
|
}
|
|
61
|
-
return wrapDescription(item.description, Math.max(1, Math.floor(width)))
|
|
86
|
+
return wrapDescription(item.description, Math.max(1, Math.floor(width)))
|
|
87
|
+
.length;
|
|
62
88
|
}
|
|
63
89
|
/**
|
|
64
90
|
* Next scroll offset after moving by `delta` (-1 = back/up, +1 = forward/down).
|
|
@@ -99,6 +125,44 @@ function wrapDescription(description, width) {
|
|
|
99
125
|
function truncateToWidth(text, width) {
|
|
100
126
|
return text.length <= width ? text : text.slice(0, width);
|
|
101
127
|
}
|
|
128
|
+
export function normalizePath(raw) {
|
|
129
|
+
if (!raw)
|
|
130
|
+
return raw;
|
|
131
|
+
let p = raw.replace(/\\/g, "/");
|
|
132
|
+
try {
|
|
133
|
+
const cwd = process.cwd().replace(/\\/g, "/");
|
|
134
|
+
if (p === cwd)
|
|
135
|
+
p = ".";
|
|
136
|
+
else if (p.startsWith(`${cwd}/`))
|
|
137
|
+
p = p.slice(cwd.length + 1);
|
|
138
|
+
else {
|
|
139
|
+
const home = os.homedir();
|
|
140
|
+
if (home) {
|
|
141
|
+
const homePosix = home.replace(/\\/g, "/");
|
|
142
|
+
if (p === homePosix)
|
|
143
|
+
p = "~";
|
|
144
|
+
else if (p.startsWith(`${homePosix}/`))
|
|
145
|
+
p = `~${p.slice(homePosix.length)}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// ignore homedir/cwd failures
|
|
151
|
+
}
|
|
152
|
+
return p;
|
|
153
|
+
}
|
|
154
|
+
export function truncateMiddle(text, maxWidth) {
|
|
155
|
+
if (text.length <= maxWidth)
|
|
156
|
+
return text;
|
|
157
|
+
if (maxWidth <= 1)
|
|
158
|
+
return text.slice(0, maxWidth);
|
|
159
|
+
if (maxWidth === 2)
|
|
160
|
+
return `${text.slice(0, 1)}…`;
|
|
161
|
+
const keep = maxWidth - 1;
|
|
162
|
+
const left = Math.ceil(keep / 2);
|
|
163
|
+
const right = Math.floor(keep / 2);
|
|
164
|
+
return `${text.slice(0, left)}…${text.slice(text.length - right)}`;
|
|
165
|
+
}
|
|
102
166
|
function clamp(value, min, max) {
|
|
103
167
|
return Math.max(min, Math.min(max, value));
|
|
104
168
|
}
|
package/dist/footer.js
CHANGED
|
@@ -188,8 +188,9 @@ export function installFooter(ctx, getState, getConfig, getModelMeta, hooks) {
|
|
|
188
188
|
const config = getConfig();
|
|
189
189
|
const cwd = ctx.sessionManager?.getCwd() ?? process.cwd();
|
|
190
190
|
const totals = getUsageTotals(
|
|
191
|
+
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
191
192
|
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
192
|
-
ctx);
|
|
193
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ ctx);
|
|
193
194
|
return renderFooter(width, state, config, themeStub, {
|
|
194
195
|
cwd,
|
|
195
196
|
sessionName: ctx.sessionManager?.getSessionName?.(),
|
|
@@ -202,10 +203,13 @@ export function installFooter(ctx, getState, getConfig, getModelMeta, hooks) {
|
|
|
202
203
|
// Prefer native footer if available
|
|
203
204
|
if (
|
|
204
205
|
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
205
|
-
|
|
206
|
+
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
207
|
+
// SAFETY: intentional unsafe cast — validated at runtime
|
|
208
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ typeof /* SAFETY: intentional unsafe cast — validated at runtime */ ctx.ui.setFooter === // SAFETY: intentional unsafe cast — validated at runtime
|
|
206
209
|
"function") {
|
|
210
|
+
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
207
211
|
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
208
|
-
const ui = ctx.ui;
|
|
212
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ const ui = ctx.ui;
|
|
209
213
|
ui.setFooter((tui, _theme, footerData) => {
|
|
210
214
|
hooks.setRequestRender(() => tui.requestRender());
|
|
211
215
|
const unsub = footerData.onBranchChange(() => {
|
|
@@ -220,14 +224,16 @@ export function installFooter(ctx, getState, getConfig, getModelMeta, hooks) {
|
|
|
220
224
|
invalidate() { },
|
|
221
225
|
render(width) {
|
|
222
226
|
// Use real theme when rendering
|
|
227
|
+
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
223
228
|
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
224
|
-
const theme = _theme;
|
|
229
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ const theme = _theme; // SAFETY: intentional unsafe cast — validated at runtime
|
|
225
230
|
const state = getState();
|
|
226
231
|
const config = getConfig();
|
|
227
232
|
const cwd = ctx.sessionManager?.getCwd() ?? process.cwd();
|
|
228
233
|
const totals = getUsageTotals(
|
|
234
|
+
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
229
235
|
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
230
|
-
ctx);
|
|
236
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ ctx);
|
|
231
237
|
return renderFooter(width, state, config, theme, {
|
|
232
238
|
cwd,
|
|
233
239
|
sessionName: ctx.sessionManager?.getSessionName?.(),
|
|
@@ -241,8 +247,10 @@ export function installFooter(ctx, getState, getConfig, getModelMeta, hooks) {
|
|
|
241
247
|
};
|
|
242
248
|
});
|
|
243
249
|
return () => {
|
|
250
|
+
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
244
251
|
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
245
|
-
ctx.ui.setFooter(
|
|
252
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ ctx.ui.setFooter(// SAFETY: intentional unsafe cast — validated at runtime
|
|
253
|
+
undefined);
|
|
246
254
|
};
|
|
247
255
|
}
|
|
248
256
|
// Fallback: widget belowEditor
|
package/dist/live-border.js
CHANGED
|
@@ -58,7 +58,10 @@ export class LiveBorder {
|
|
|
58
58
|
const comp = this.composition();
|
|
59
59
|
if (!comp)
|
|
60
60
|
return;
|
|
61
|
-
|
|
61
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
62
|
+
const snapshot = createChromeSnapshot(
|
|
63
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
64
|
+
ctx, // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
62
65
|
undefined);
|
|
63
66
|
this.refreshTopBorder(comp);
|
|
64
67
|
this.refreshLiveTelemetry(comp);
|
|
@@ -179,9 +182,8 @@ export class LiveBorder {
|
|
|
179
182
|
if (cfg.footerSegments.context &&
|
|
180
183
|
snapshot.contextUsage &&
|
|
181
184
|
snapshot.contextUsage.contextWindow) {
|
|
182
|
-
|
|
183
|
-
// SAFETY: intentional unsafe cast
|
|
184
|
-
cfg.contextIconBar ?? // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
185
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
186
|
+
contextText = comp.formatTopContext(snapshot, cfg.contextIconBar ?? // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
185
187
|
false);
|
|
186
188
|
}
|
|
187
189
|
let tokensText = "";
|
|
@@ -189,15 +191,15 @@ export class LiveBorder {
|
|
|
189
191
|
const isRunning = this.deps.runActivityTracker.isRunning();
|
|
190
192
|
const contextTokens = snapshot.contextUsage?.tokens;
|
|
191
193
|
if (!isRunning) {
|
|
192
|
-
|
|
193
|
-
const display = this.deps.agentLedger.
|
|
194
|
+
// Hybrid Q7 b: idle shows authoritative billed total (no ~), live shows synthetic incremental (~)
|
|
195
|
+
const display = this.deps.agentLedger.getIdleAuthoritativeDisplay(snapshot.totals, contextTokens);
|
|
194
196
|
tokensText = comp.formatTelemetryTokens(display, cfg.telemetry);
|
|
195
197
|
}
|
|
196
198
|
else {
|
|
197
199
|
const liveTurn = this.deps.telemetryTracker.peekLive();
|
|
198
200
|
const agentLive = this.deps.agentLedger.getLiveTotals(liveTurn) ??
|
|
199
201
|
this.deps.telemetryTracker.getLastTelemetry();
|
|
200
|
-
const display = this.deps.agentLedger.
|
|
202
|
+
const display = this.deps.agentLedger.getIncrementalLiveDisplayTotals(liveTurn, agentLive, contextTokens);
|
|
201
203
|
if (display) {
|
|
202
204
|
tokensText = comp.formatTelemetryTokens(display, cfg.telemetry);
|
|
203
205
|
}
|