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.
Files changed (56) hide show
  1. package/CHANGELOG.md +18 -2
  2. package/dist/agent-run-ledger.js +9 -27
  3. package/dist/border-renderer.js +9 -19
  4. package/dist/chrome-composition.js +1 -1
  5. package/dist/chrome-state.js +4 -5
  6. package/dist/config.js +1 -3
  7. package/dist/cursor-policy.js +3 -5
  8. package/dist/detail-chrome.js +45 -1
  9. package/dist/detail-render.js +20 -8
  10. package/dist/footer.js +10 -7
  11. package/dist/git.js +7 -11
  12. package/dist/header.js +2 -5
  13. package/dist/layout.js +2 -4
  14. package/dist/live-border.js +3 -3
  15. package/dist/model-info.js +2 -8
  16. package/dist/path-format.js +1 -2
  17. package/dist/run-activity.js +2 -6
  18. package/dist/runtime.js +1 -7
  19. package/dist/session-orchestrator.js +10 -15
  20. package/dist/telemetry.js +8 -28
  21. package/dist/theme-settings.js +2 -4
  22. package/dist/tip-policy.js +2 -4
  23. package/dist/transcript-timeline.js +15 -34
  24. package/dist/utils.js +4 -4
  25. package/dist/window-presentation.js +19 -3
  26. package/package.json +16 -5
  27. package/src/agent-run-ledger.ts +19 -71
  28. package/src/border-renderer.ts +32 -54
  29. package/src/chrome-composition.ts +2 -8
  30. package/src/chrome-state.ts +13 -26
  31. package/src/color-policy.ts +1 -3
  32. package/src/config.ts +6 -24
  33. package/src/cursor-policy.ts +11 -20
  34. package/src/detail-chrome.ts +48 -28
  35. package/src/detail-render.ts +132 -125
  36. package/src/footer.ts +30 -44
  37. package/src/git.ts +9 -26
  38. package/src/header.ts +6 -23
  39. package/src/icons.ts +1 -2
  40. package/src/index.ts +45 -65
  41. package/src/layout.ts +7 -23
  42. package/src/live-border.ts +4 -9
  43. package/src/model-info.ts +182 -213
  44. package/src/path-format.ts +1 -2
  45. package/src/run-activity.ts +8 -25
  46. package/src/runtime.ts +9 -25
  47. package/src/session-orchestrator.ts +50 -124
  48. package/src/state.ts +4 -9
  49. package/src/telemetry.ts +19 -65
  50. package/src/theme-settings.ts +20 -62
  51. package/src/tip-policy.ts +5 -9
  52. package/src/tracking-editor.ts +11 -19
  53. package/src/transcript-timeline.ts +23 -77
  54. package/src/utils.ts +33 -4
  55. package/src/window-presentation.ts +51 -38
  56. 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
@@ -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
- ? agentLive.stallCount
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,
@@ -1,5 +1,5 @@
1
1
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
- import { buildLabel, } from "./model-info.js";
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
- // SAFETY: intentional unsafe cast — validated at runtime
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
- // SAFETY: intentional unsafe cast — validated at runtime
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
- ? `${getGlow("─")} ${displayLeft} `
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
- ? `${getGlow("─")} ${displayLeft} `
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, } from "./telemetry.js";
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.
@@ -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 };
@@ -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
- // SAFETY: intentional unsafe cast — validated at runtime
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
  }
@@ -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, } from "./detail-render.js";
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);
@@ -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 baseWidth = Math.max(0, wrapWidth - pathSeg.length - markerLen);
61
- header = truncateToWidth(base, baseWidth) + pathSeg + marker;
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, } from "@earendil-works/pi-tui";
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, } from "./path-format.js";
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 /* SAFETY: intentional unsafe cast — validated at runtime */ ctx.ui.setFooter === // SAFETY: intentional unsafe cast — validated at runtime
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(// SAFETY: intentional unsafe cast — validated at runtime
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 (active.reduce((a, it) => a + it.w, 0) + Math.max(0, active.length - 1));
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) {
@@ -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()) ?? this.deps.telemetryTracker.getLastTelemetry();
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
@@ -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 (glow("─".repeat(leftWidth)) +
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];
@@ -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}`;
@@ -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"],