pi-editor-footer 0.11.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 +18 -2
- package/dist/agent-run-ledger.js +9 -27
- package/dist/border-renderer.js +9 -19
- package/dist/chrome-composition.js +1 -1
- package/dist/chrome-state.js +4 -5
- 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 +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 +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 +4 -4
- package/dist/window-presentation.js +19 -3
- package/package.json +16 -5
- package/src/agent-run-ledger.ts +19 -71
- package/src/border-renderer.ts +32 -54
- package/src/chrome-composition.ts +2 -8
- package/src/chrome-state.ts +13 -26
- package/src/color-policy.ts +1 -3
- 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 +7 -23
- package/src/live-border.ts +4 -9
- 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 +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 +33 -4
- package/src/window-presentation.ts +51 -38
- package/dist/session-kernel.js +0 -178
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
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
|
-
## [Unreleased]
|
|
6
|
-
|
|
7
23
|
## [0.11.0] - 2026-08-29
|
|
8
24
|
|
|
9
25
|
### 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,
|
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,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
|
}
|
|
@@ -75,7 +73,8 @@ export function createChromeSnapshot(ctx, footerState) {
|
|
|
75
73
|
const totals = getUsageTotals(
|
|
76
74
|
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
77
75
|
// SAFETY: pi seam
|
|
78
|
-
/* SAFETY: intentional unsafe cast — validated at runtime */ (ctx ??
|
|
76
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ (ctx ??
|
|
77
|
+
{}));
|
|
79
78
|
// footerState may be absent when called from LiveBorder (context-only); use empty defaults
|
|
80
79
|
// SAFETY: pi seam
|
|
81
80
|
const git = footerState?.git ??
|
|
@@ -93,7 +92,7 @@ export function createChromeSnapshot(ctx, footerState) {
|
|
|
93
92
|
commit: null,
|
|
94
93
|
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
95
94
|
// SAFETY: intentional unsafe cast — validated at runtime
|
|
96
|
-
/* SAFETY: intentional unsafe cast — validated at runtime */
|
|
95
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */
|
|
97
96
|
}; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
98
97
|
const runtime = footerState?.runtime ?? null;
|
|
99
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 };
|
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).
|
package/dist/footer.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { truncateToWidth, visibleWidth, wrapTextWithAnsi
|
|
1
|
+
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
2
|
import { getUsageTotals } from "./state.js";
|
|
3
3
|
import { runtimeSymbol } from "./icons.js";
|
|
4
4
|
import { ChromeComposition } from "./chrome-composition.js";
|
|
5
5
|
import { alignRight, fitSegmentsByPriority, } from "./layout.js";
|
|
6
|
-
import { basenamePath, formatCwd, truncatePath
|
|
6
|
+
import { basenamePath, formatCwd, truncatePath } from "./path-format.js";
|
|
7
7
|
import { fmtTokens, formatDuration, sanitizeStatus } from "./format.js";
|
|
8
8
|
// ChromeState owns context bar formatting — re-export for backward compat.
|
|
9
9
|
export { formatContextBar } from "./chrome-state.js";
|
|
@@ -22,6 +22,9 @@ function renderGitSegment(theme, git, glyphs, segments) {
|
|
|
22
22
|
parts.push(theme.fg("dim", `${shortHash}${tag}`));
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
else {
|
|
26
|
+
parts.push(theme.fg("dim", "(no git)"));
|
|
27
|
+
}
|
|
25
28
|
}
|
|
26
29
|
if (segments.gitStatus) {
|
|
27
30
|
const statusIcons = [];
|
|
@@ -170,9 +173,7 @@ export function renderFooter(width, state, config, theme, ctx) {
|
|
|
170
173
|
.filter((text) => text.length > 0);
|
|
171
174
|
if (statuses.length > 0) {
|
|
172
175
|
const separator = ` ${theme.fg("dim", "|")} `;
|
|
173
|
-
const statusText = statuses
|
|
174
|
-
.map((status) => theme.fg("muted", status))
|
|
175
|
-
.join(separator);
|
|
176
|
+
const statusText = statuses.map((status) => theme.fg("muted", status)).join(separator);
|
|
176
177
|
const line = `${theme.fg("mdLink", glyphs.extensions)} ${statusText}`;
|
|
177
178
|
return [...mainLines, ...wrapTextWithAnsi(line, width)];
|
|
178
179
|
}
|
|
@@ -205,7 +206,8 @@ export function installFooter(ctx, getState, getConfig, getModelMeta, hooks) {
|
|
|
205
206
|
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
206
207
|
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
207
208
|
// SAFETY: intentional unsafe cast — validated at runtime
|
|
208
|
-
/* SAFETY: intentional unsafe cast — validated at runtime */ typeof
|
|
209
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ typeof (
|
|
210
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ ctx.ui.setFooter) === // SAFETY: intentional unsafe cast — validated at runtime
|
|
209
211
|
"function") {
|
|
210
212
|
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
211
213
|
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
@@ -249,7 +251,8 @@ export function installFooter(ctx, getState, getConfig, getModelMeta, hooks) {
|
|
|
249
251
|
return () => {
|
|
250
252
|
// ast-grep-ignore: require-safety-comment-for-as-unknown-as
|
|
251
253
|
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
252
|
-
/* SAFETY: intentional unsafe cast — validated at runtime */ ctx.ui.setFooter(
|
|
254
|
+
/* SAFETY: intentional unsafe cast — validated at runtime */ ctx.ui.setFooter(
|
|
255
|
+
// SAFETY: intentional unsafe cast — validated at runtime
|
|
253
256
|
undefined);
|
|
254
257
|
};
|
|
255
258
|
}
|
package/dist/git.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
2
|
import { promisify } from "node:util";
|
|
5
3
|
const execFileAsync = promisify(execFile);
|
|
6
4
|
const GIT_TIMEOUT_MS = 2000;
|
|
@@ -34,9 +32,6 @@ async function gitExec(args, cwd) {
|
|
|
34
32
|
}
|
|
35
33
|
}
|
|
36
34
|
export async function readGitStatus(cwd, options = {}) {
|
|
37
|
-
if (!existsSync(join(cwd, ".git"))) {
|
|
38
|
-
return emptyGitStatus();
|
|
39
|
-
}
|
|
40
35
|
const stdout = await gitExec(["status", "--porcelain=v1", "--branch", "--show-stash"], cwd);
|
|
41
36
|
if (stdout === null) {
|
|
42
37
|
return emptyGitStatus();
|
|
@@ -51,6 +46,11 @@ export async function readGitStatus(cwd, options = {}) {
|
|
|
51
46
|
status.branch = undefined;
|
|
52
47
|
status.commit = { oid: null, detached: true, tag: null };
|
|
53
48
|
}
|
|
49
|
+
else if (branchPart.startsWith("No commits yet on ")) {
|
|
50
|
+
const m = branchPart.match(/^No commits yet on (\S+)/);
|
|
51
|
+
if (m?.[1])
|
|
52
|
+
status.branch = m[1];
|
|
53
|
+
}
|
|
54
54
|
else {
|
|
55
55
|
const branchMatch = branchPart.match(/^(\S+?)(?:\.\.\.(\S+))?(?:\s+\[(ahead|behind) (\d+)\])?$/);
|
|
56
56
|
if (branchMatch) {
|
|
@@ -91,14 +91,10 @@ export async function readGitStatus(cwd, options = {}) {
|
|
|
91
91
|
status.modified++;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
-
if (options.readCounts !== false &&
|
|
95
|
-
status.stashed === 0 &&
|
|
96
|
-
!stdout.includes("# stash")) {
|
|
94
|
+
if (options.readCounts !== false && status.stashed === 0 && !stdout.includes("# stash")) {
|
|
97
95
|
const stashOut = await gitExec(["stash", "list"], cwd);
|
|
98
96
|
if (stashOut !== null) {
|
|
99
|
-
const count = stashOut
|
|
100
|
-
.split("\n")
|
|
101
|
-
.filter((l) => l.trim().length > 0).length;
|
|
97
|
+
const count = stashOut.split("\n").filter((l) => l.trim().length > 0).length;
|
|
102
98
|
if (!Number.isNaN(count))
|
|
103
99
|
status.stashed = count;
|
|
104
100
|
}
|
package/dist/header.js
CHANGED
|
@@ -7,8 +7,7 @@ export function formatCwd(cwd) {
|
|
|
7
7
|
const resolvedCwd = resolve(cwd);
|
|
8
8
|
const resolvedHome = resolve(home);
|
|
9
9
|
const rel = relative(resolvedHome, resolvedCwd);
|
|
10
|
-
const insideHome = rel === "" ||
|
|
11
|
-
(rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
10
|
+
const insideHome = rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
12
11
|
if (!insideHome)
|
|
13
12
|
return cwd;
|
|
14
13
|
return rel === "" ? "~" : `~${sep}${rel}`;
|
|
@@ -52,9 +51,7 @@ export function renderHeader(width, deps, theme) {
|
|
|
52
51
|
const icon = deps.iconCwd ?? "@";
|
|
53
52
|
const cwdText = displayCwd(deps.cwd, deps.workspaceDisplay);
|
|
54
53
|
// Single-line content: "icon cwd | tip0 tip1 tip2"
|
|
55
|
-
const tips = (deps.tipCommands ?? [])
|
|
56
|
-
.slice(0, 3)
|
|
57
|
-
.map((t) => (t.startsWith("/") ? t : `/${t}`));
|
|
54
|
+
const tips = (deps.tipCommands ?? []).slice(0, 3).map((t) => (t.startsWith("/") ? t : `/${t}`));
|
|
58
55
|
const tipsText = tips.length > 0 ? tips.map((t) => theme.fg("dim", t)).join(" ") : "";
|
|
59
56
|
// At very narrow widths, just show cwd with icon, truncated.
|
|
60
57
|
if (width < 24 || tipsText === "") {
|
package/dist/layout.js
CHANGED
|
@@ -14,9 +14,7 @@ export function alignRight(left, right, width, theme) {
|
|
|
14
14
|
return left + " ".repeat(pad) + right;
|
|
15
15
|
}
|
|
16
16
|
const availableForLeft = Math.max(0, width - rightW2 - 1);
|
|
17
|
-
const truncatedLeft = availableForLeft > 0
|
|
18
|
-
? truncateToWidth(left, availableForLeft, theme.fg("dim", "..."))
|
|
19
|
-
: "";
|
|
17
|
+
const truncatedLeft = availableForLeft > 0 ? truncateToWidth(left, availableForLeft, theme.fg("dim", "...")) : "";
|
|
20
18
|
return truncatedLeft ? truncatedLeft + " " + right : right;
|
|
21
19
|
}
|
|
22
20
|
export function fitSegmentsByPriority(segs, maxW, ellipsis = "...") {
|
|
@@ -29,7 +27,7 @@ export function fitSegmentsByPriority(segs, maxW, ellipsis = "...") {
|
|
|
29
27
|
}));
|
|
30
28
|
const totalW = () => {
|
|
31
29
|
const active = items.filter((it) => it.text !== "");
|
|
32
|
-
return
|
|
30
|
+
return active.reduce((a, it) => a + it.w, 0) + Math.max(0, active.length - 1);
|
|
33
31
|
};
|
|
34
32
|
if (totalW() > maxW) {
|
|
35
33
|
for (const item of items) {
|
package/dist/live-border.js
CHANGED
|
@@ -122,7 +122,8 @@ export class LiveBorder {
|
|
|
122
122
|
// Relocate stall to the right of tool use with pipe separator — agent-run live (option B), not bottom telemetry
|
|
123
123
|
if (cfg.telemetry.enabled && cfg.telemetry.stalls) {
|
|
124
124
|
// Deepened via AgentRunLedger — stall is agent-run live, single source
|
|
125
|
-
const tel = this.deps.agentLedger.getLiveTotals(this.deps.telemetryTracker.peekLive()) ??
|
|
125
|
+
const tel = this.deps.agentLedger.getLiveTotals(this.deps.telemetryTracker.peekLive()) ??
|
|
126
|
+
this.deps.telemetryTracker.getLastTelemetry();
|
|
126
127
|
if (tel && tel.stallMs > 0) {
|
|
127
128
|
const stallText = comp.formatStall(tel);
|
|
128
129
|
if (text) {
|
|
@@ -156,8 +157,7 @@ export class LiveBorder {
|
|
|
156
157
|
}
|
|
157
158
|
try {
|
|
158
159
|
// peekLive ?? getLastTelemetry preserves cost after toggle (AGENTS.md gotcha)
|
|
159
|
-
const live = this.deps.telemetryTracker.peekLive() ??
|
|
160
|
-
this.deps.telemetryTracker.getLastTelemetry();
|
|
160
|
+
const live = this.deps.telemetryTracker.peekLive() ?? this.deps.telemetryTracker.getLastTelemetry();
|
|
161
161
|
if (!live)
|
|
162
162
|
return;
|
|
163
163
|
// Stall relocated to top right of tool use with pipe — suppress in bottom telemetry
|
package/dist/model-info.js
CHANGED
|
@@ -208,11 +208,7 @@ function embedLabel(width, label, glow) {
|
|
|
208
208
|
const leftWidth = Math.max(0, Math.min(2, width - lw - padding));
|
|
209
209
|
const rightWidth = Math.max(0, width - lw - leftWidth - padding);
|
|
210
210
|
const pad = " ".repeat(padCount);
|
|
211
|
-
return
|
|
212
|
-
pad +
|
|
213
|
-
labelText +
|
|
214
|
-
pad +
|
|
215
|
-
glow("─".repeat(rightWidth)));
|
|
211
|
+
return glow("─".repeat(leftWidth)) + pad + labelText + pad + glow("─".repeat(rightWidth));
|
|
216
212
|
}
|
|
217
213
|
/**
|
|
218
214
|
* Apply the glow + embedded model label to the editor's rendered lines
|
|
@@ -224,9 +220,7 @@ export function applyModelInfo(lines, width, theme, info) {
|
|
|
224
220
|
// Top border (always lines[0]): embed the label into plain borders,
|
|
225
221
|
// keep scroll indicators but recolor them.
|
|
226
222
|
const top = lines[0];
|
|
227
|
-
lines[0] = isScrollBorder(top)
|
|
228
|
-
? glow(stripAnsi(top))
|
|
229
|
-
: embedLabel(width, label, glow);
|
|
223
|
+
lines[0] = isScrollBorder(top) ? glow(stripAnsi(top)) : embedLabel(width, label, glow);
|
|
230
224
|
// Bottom border: the last border-like line (autocomplete lines may follow).
|
|
231
225
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
232
226
|
const line = lines[i];
|
package/dist/path-format.js
CHANGED
|
@@ -7,8 +7,7 @@ export function formatCwd(cwd) {
|
|
|
7
7
|
const resolvedCwd = resolve(cwd);
|
|
8
8
|
const resolvedHome = resolve(home);
|
|
9
9
|
const rel = relative(resolvedHome, resolvedCwd);
|
|
10
|
-
const insideHome = rel === "" ||
|
|
11
|
-
(rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
10
|
+
const insideHome = rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
12
11
|
if (!insideHome)
|
|
13
12
|
return cwd;
|
|
14
13
|
return rel === "" ? "~" : `~${sep}${rel}`;
|
package/dist/run-activity.js
CHANGED
|
@@ -116,9 +116,7 @@ class DefaultRunActivityTracker {
|
|
|
116
116
|
getSnapshot(now) {
|
|
117
117
|
const cur = normalizeTimestamp(now ?? this.now());
|
|
118
118
|
let duration = this.durationMs;
|
|
119
|
-
if (duration === undefined &&
|
|
120
|
-
this.startedAt !== undefined &&
|
|
121
|
-
this.phase === "running") {
|
|
119
|
+
if (duration === undefined && this.startedAt !== undefined && this.phase === "running") {
|
|
122
120
|
duration = Math.max(0, cur - this.startedAt);
|
|
123
121
|
}
|
|
124
122
|
return Object.freeze({
|
|
@@ -149,9 +147,7 @@ export function formatRunActivityTopRight(snap, theme, now) {
|
|
|
149
147
|
}
|
|
150
148
|
// duration
|
|
151
149
|
const dur = snap.durationMs ??
|
|
152
|
-
(snap.startedAt === undefined
|
|
153
|
-
? undefined
|
|
154
|
-
: Math.max(0, (now ?? Date.now()) - snap.startedAt));
|
|
150
|
+
(snap.startedAt === undefined ? undefined : Math.max(0, (now ?? Date.now()) - snap.startedAt));
|
|
155
151
|
if (dur !== undefined) {
|
|
156
152
|
parts.push(theme.fg("text", formatDurationShort(dur)));
|
|
157
153
|
}
|
package/dist/runtime.js
CHANGED
|
@@ -34,13 +34,7 @@ const RUNTIMES = [
|
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
name: "python",
|
|
37
|
-
files: [
|
|
38
|
-
"pyproject.toml",
|
|
39
|
-
"requirements.txt",
|
|
40
|
-
"setup.py",
|
|
41
|
-
"Pipfile",
|
|
42
|
-
".python-version",
|
|
43
|
-
],
|
|
37
|
+
files: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile", ".python-version"],
|
|
44
38
|
versionCommand: {
|
|
45
39
|
cmd: "python3",
|
|
46
40
|
args: ["--version"],
|