pi-editor-footer 0.3.0 → 0.4.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 CHANGED
@@ -4,8 +4,29 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.4.0] - 2026-08-22
8
+
9
+ ### Added
10
+
11
+ - Tiered context usage colors — `contextUsageColor(pct)` with 25%/50%/75% thresholds (`dim` <25 → `accent` 25–50 → `warning` 50–75 → `error` ≥75) applied to both `percent` and `tokens/window` sections and to bar/icon, so low usage stays dimmed and high usage highlights for quota visibility (`color-policy`, `chrome-state:formatContextBar`, `utils` barrel)
12
+ - Deepened architecture — candidates 1–5 (`TrackingEditor`, `TranscriptTimeline`, `LiveBorder`, `ChromeState`, `SessionKernel`) behind single seams, plus sync contract extension for transcript timeline seam (candidate 2)
13
+ - Telemetry formatting — `TPS`/`TTFT` with units and fixed width (`60.6 tok/s TPS`, `2.5s TTFT`), default tokens at start, and `↑`/`↓` relocated to right of cache with `|` separator
14
+
15
+ ### Changed
16
+
17
+ - Hide `↑`/`↓` tokens at startup (only after `turn_start` via `liveInputTokens`)
18
+ - Dim line padding and fixed-width `TPS`/`TTFT`/`duration` with gradual TPS decay (half-life ~5s, reset after 2s idle)
19
+
20
+ ### Fixed
21
+
22
+ - Reset TPS to default after 2s without incoming tokens before first token
23
+ - Interleave timeline in transcript (`chatContainer` injection, left-aligned dim `·`/`↑`/`↓`/`c`/`$` with `|`, close right) and correct `pi` extension path (`src/index.ts`)
24
+ - Satisfy `pi-lens` blocking — `SAFETY` comments for `as unknown as` casts (including `chrome-state`, `live-border`) and best-effort catches
25
+
7
26
  ## [0.3.0] - 2026-08-21
27
+
8
28
  ### Changed
29
+
9
30
  - Timeline format now two-line dim per user spec: `2026-08-21 13:48:46 GMT+8 · 11s · ↑ 495 · ↓ 708 · c 85.3% · $0.00` + `3 turns · 5 tools · 1 failed` (datetime with timezone via `Intl` `en-CA` `short` TZ, wall `formatDuration`, `↑`/`↓` `fmtTokens`, cache `glyphs.cacheHit` `latestCacheHitRate`, cost `$`, turn/tools/failed from `runActivity` snapshot)
10
31
  - Dimmed timeline now in transcript between runs — `User: hello? / Assistant: hi. / <dim timeline>` left-aligned dim, injected into `chatContainer` (scrollable) via `InteractiveMode` patch, not `aboveEditor` widget, so it sits between each `User/Assistant` pair and scrolls with history
11
32
  - Dimmed timeline between each agent run in transcript — `chatContainer` injection `User/Assistant/<dim timeline>` left-aligned dim `· 8s wall · ↑ 1.2k · ↓ 800 · $0.12` via `InteractiveMode` patch (not `aboveEditor` widget), one per `agent_settled` per `timeline.*`, permanent in scroll history, never exposed to model (between transcript bottom and input), one per `agent_settled` (`· 8s wall · ↑ 1.2k · ↓ 800 · $0.12`), permanent between runs, never exposed to model (pi-tui only exposes `aboveEditor`/`belowEditor` widgets, so stacking aboveEditor is the non-exposing compromise)
@@ -22,15 +43,15 @@ All notable changes to this project will be documented in this file.
22
43
  - Refresh rate set to one second (`REFRESH_MS = 1000` for `liveTickTimer` telemetry/top/context and `watchTimer` editor ownership watchdog, was hardcoded `1000`)
23
44
  - Context icon bar now switchable via config `contextIconBar` (default `false` disabled) — `0.0% · 0/1.0M | c 0.0%` by default, `# [████░░] 39.6% · 416k/1.0M | c 85.3%` when enabled (footer top, `nerd`/`ascii` glyphs)
24
45
  - Context bar format now `0.0% · 0/1.0M | c 0.0%` (was `# [#####-------] 39.6% · 416k/1.0M | c`; no icon/bar, compact `pct · tokens/W`)
25
- - Context bar and cache now follow icon mode (`nerd` ``/``/`█` vs `ascii` ` #`/`c`/`#`) — `refreshContextBar` called in `onConfigChanged` so top ` # [...] | c` updates immediately when `icons.mode` changes
46
+ - Context bar and cache now follow icon mode (`nerd` ``/``/`█` vs `ascii` `#`/`c`/`#`) — `refreshContextBar` called in `onConfigChanged` so top `# [...] | c` updates immediately when `icons.mode` changes
26
47
  - Telemetry `>`, `~`, `+` glyphs removed (`TPS 60.6 tok/s · TTFT 2.5s · 8.3s · ↑395 · ↓505 · $0.16` not `> TPS … · ~ TTFT … · +8.3s`)
27
48
  - Telemetry bottom border compacted (`TPS 60.6 tok/s · TTFT 2.5s · 8.3s · ↑395 · ↓505 · $0.16` not `> TPS 60.6 tok/s | ~ TTFT 2.5s | + 8.3s | ↑ 395 | ↓ 505 | $0.16`; `TPS`/`TTFT` labels preserved, no space after `↑`/`↓`, joiner `·` not ` | `, stall `!2·3.3s` not `! stall 2x / 3.3s`)
28
- - Footer cache omitted to the right of input/output (`↑ 395 | ↓ 505 | $0.16` not `| c 85.3%`; cache remains only in top context bar ` # [...] | c 0.0%`)
29
- - Nerd mode cost now `$0.00` not ` $0.00` (always single `$` in both footer `renderFooter` and telemetry `formatTurnTelemetry`, was `glyph === "$" ? `$ / ` : `glyph $` -> ` $`)
30
- - Cache session now always shown with zero value (`c 0.0%` in footer `↑ | ↓ | $ | c` and ` | c 0.0%` in top context bar) instead of hidden when no cache tokens
49
+ - Footer cache omitted to the right of input/output (`↑ 395 | ↓ 505 | $0.16` not `| c 85.3%`; cache remains only in top context bar `# [...] | c 0.0%`)
50
+ - Nerd mode cost now `$0.00` not ` $0.00` (always single `$` in both footer `renderFooter` and telemetry `formatTurnTelemetry`, was `glyph === "$" ?`$ / ` : `glyph $` -> ` $`)
51
+ - Cache session now always shown with zero value (`c 0.0%` in footer `↑ | ↓ | $ | c` and `| c 0.0%` in top context bar) instead of hidden when no cache tokens
31
52
  - Footer cost now `toFixed(2)` (`$0.16` not `$0.160`) and moved directly after input/output (`↑ 395 | ↓ 505 | $0.16 | c 85.3%` not `c | $`; `theme.fg("dim","|")` pipe)
32
53
  - Top border model label no longer shows context window (`provider/model · thinking` not `· 1.0M`; `buildLabel` ignores `contextWindow`)
33
- - Context bar now appends cache hit rate with pipe (` # [#####-------] 39.6% · 416k/1.0M | c 85.3%` via `formatContextBar(..., cacheHitRate)` from `getUsageTotals`) and model/context are pipe-separated (`model | context` via dim `|`)
54
+ - Context bar now appends cache hit rate with pipe (`# [#####-------] 39.6% · 416k/1.0M | c 85.3%` via `formatContextBar(..., cacheHitRate)` from `getUsageTotals`) and model/context are pipe-separated (`model | context` via dim `|`)
34
55
  - Context bar moved from left bottom border to top border right of model info (`── model · thinking · 1.0M # [#####-------] 39.6% · 416k/1.0M ── T1 · 8s` via `embedTopWithLeftAndRight`, `TrackingEditor.setTopContextText`)
35
56
 
36
57
  ## [0.2.0] - 2026-08-21
@@ -24,7 +24,7 @@ export class BorderRenderer {
24
24
  if (out.length === 0)
25
25
  return out;
26
26
  // Top: model info left, context bar right of it, run-activity far right
27
- const hasTop = opts.glowEnabled || opts.topRightText || opts.topContextText;
27
+ const hasTop = opts.glowEnabled || opts.topContextText || (!opts.topTokensText && opts.topRightText);
28
28
  if (hasTop) {
29
29
  const theme = this.getLiveTheme();
30
30
  const info = this.getModelInfo();
@@ -35,10 +35,12 @@ export class BorderRenderer {
35
35
  return maybeGlow.call(theme, info.level)(s);
36
36
  }
37
37
  catch {
38
- // ignore
38
+ // SAFETY: best-effort UI, ignore recoverable error
39
39
  }
40
40
  return s;
41
41
  };
42
+ // When tokens line exists, right moves to that line (one padding) — don't duplicate on top
43
+ const topRightForTop = opts.topTokensText ? "" : opts.topRightText;
42
44
  if (opts.glowEnabled) {
43
45
  // Build left label; if context bar present, append it to the right of model info with pipe separator
44
46
  let leftLabel = buildLabel(theme, info.provider, info.modelId, info.level, info.contextWindow);
@@ -46,8 +48,8 @@ export class BorderRenderer {
46
48
  leftLabel = `${leftLabel} ${theme.fg("dim", "|")} ${opts.topContextText}`;
47
49
  }
48
50
  // Embed left (model+context) and optional right (run activity) in one pass to avoid double-truncate
49
- if (opts.topRightText) {
50
- out = embedTopWithLeftAndRight(out, width, leftLabel, opts.topRightText, glow);
51
+ if (topRightForTop) {
52
+ out = embedTopWithLeftAndRight(out, width, leftLabel, topRightForTop, glow);
51
53
  }
52
54
  else {
53
55
  // Only left (model+context), no right — use applyModelInfo replacement but with combined left
@@ -55,10 +57,10 @@ export class BorderRenderer {
55
57
  out = embedTopWithLeftAndRight(out, width, leftLabel, "", glow);
56
58
  }
57
59
  }
58
- else if (opts.topContextText || opts.topRightText) {
60
+ else if (opts.topContextText || topRightForTop) {
59
61
  // No glow, but still need top borders for context/run activity
60
62
  const left = opts.topContextText ?? "";
61
- const right = opts.topRightText ?? "";
63
+ const right = topRightForTop ?? "";
62
64
  if (left && right) {
63
65
  out = embedTopWithLeftAndRight(out, width, left, right, glow);
64
66
  }
@@ -70,6 +72,20 @@ export class BorderRenderer {
70
72
  }
71
73
  }
72
74
  }
75
+ // Tokens above model info — left aligned, no border; right moved close to tokens with | separator per user request
76
+ if (opts.topTokensText || opts.topRightText) {
77
+ const tokensLeft = opts.topTokensText ?? "";
78
+ const tokensRight = opts.topRightText ?? "";
79
+ if (tokensLeft) {
80
+ // Use | as separator between ↑/↓ and tool turns (dimmed) per user request
81
+ // SAFETY: pi theme seam — fg is optional theme method
82
+ const themeForTokens = this.getLiveTheme();
83
+ const fg = themeForTokens.fg;
84
+ const dimPipe = typeof fg === "function" ? fg.call(themeForTokens, "dim", " | ") : " | ";
85
+ const combined = tokensRight ? `${tokensLeft}${dimPipe}${tokensRight}` : tokensLeft;
86
+ out = embedTokensAbove(out, width, combined);
87
+ }
88
+ }
73
89
  // Bottom embedding (telemetry left/right)
74
90
  if (opts.telemetryText || opts.bottomLeftText) {
75
91
  const theme = this.getLiveTheme();
@@ -80,7 +96,7 @@ export class BorderRenderer {
80
96
  return maybeGlow.call(theme, this.getModelInfo().level)(s);
81
97
  }
82
98
  catch {
83
- // ignore
99
+ // SAFETY: best-effort UI, ignore recoverable error
84
100
  }
85
101
  return s;
86
102
  };
@@ -89,6 +105,16 @@ export class BorderRenderer {
89
105
  return out;
90
106
  }
91
107
  }
108
+ function embedTokensAbove(lines, width, tokensText) {
109
+ if (!tokensText || lines.length === 0)
110
+ return lines;
111
+ // Left aligned, no border; one leading padding as before
112
+ const raw = ` ${tokensText}`;
113
+ const display = truncateToWidth(raw, width, "");
114
+ const result = [...lines];
115
+ result.splice(0, 0, display);
116
+ return result;
117
+ }
92
118
  function embedTopWithLeftAndRight(lines, width, leftText, rightText, getGlow) {
93
119
  if (lines.length === 0)
94
120
  return lines;
@@ -106,8 +132,12 @@ function embedTopWithLeftAndRight(lines, width, leftText, rightText, getGlow) {
106
132
  displayLeft = truncateToWidth(leftText, maxLeft, "");
107
133
  if (rightW > maxRight)
108
134
  displayRight = truncateToWidth(rightText, maxRight, "");
109
- const leftSegment = displayLeft ? `${getGlow("─")} ${displayLeft} ` : getGlow("─");
110
- const rightSegment = displayRight ? ` ${displayRight} ${getGlow("─")}` : getGlow("─");
135
+ const leftSegment = displayLeft
136
+ ? `${getGlow("─")} ${displayLeft} `
137
+ : getGlow("─");
138
+ const rightSegment = displayRight
139
+ ? ` ${displayRight} ${getGlow("─")}`
140
+ : getGlow("─");
111
141
  const used = visibleWidth(leftSegment) + visibleWidth(rightSegment);
112
142
  const middleWidth = Math.max(0, width - used);
113
143
  const middle = getGlow("─".repeat(middleWidth));
@@ -0,0 +1,120 @@
1
+ /**
2
+ * ChromeState — deep module owning chrome snapshot derivation behind one seam.
3
+ *
4
+ * Problem it solves (C5): same chrome concepts (context %, git, runtime,
5
+ * tokens, cacheHitRate) render through two seams — footer below input and
6
+ * border on TrackingEditor — with derivation scattered across
7
+ * footer.ts:formatContextBar, state.ts:getUsageTotals, index.ts:refreshContextBar,
8
+ * and border-renderer. Tracing one token requires bouncing 4 modules.
9
+ *
10
+ * Depth: one interface (snapshot / format helpers) hides truncation, color
11
+ * policy, cache-hit derivation and cwd fallbacks. Two adapters (footer +
12
+ * border) justify the seam — with one it would be hypothetical.
13
+ *
14
+ * Internal seams (renderBar, stressColor, cacheHitColor, fmtTokens) stay
15
+ * inside this module, not part of its external seam.
16
+ */
17
+ import { getUsageTotals } from "./state.js";
18
+ import { cacheHitColor, contextUsageColor } from "./color-policy.js";
19
+ import { fmtTokens } from "./format.js";
20
+ // ---------------------------------------------------------------------------
21
+ // Context bar formatting — moved from footer.ts to centralize chrome rendering.
22
+ // Re-exported from footer.ts for backward compatibility.
23
+ // ---------------------------------------------------------------------------
24
+ function renderBar(theme, pct, barWidth, ascii) {
25
+ const filled = Math.max(0, Math.min(barWidth, Math.round((pct / 100) * barWidth)));
26
+ const empty = barWidth - filled;
27
+ const color = contextUsageColor(pct);
28
+ const filledCell = ascii ? "#" : "█";
29
+ const emptyCell = ascii ? "-" : "░";
30
+ return (theme.fg("dim", "[") +
31
+ theme.fg(color, filledCell.repeat(filled)) +
32
+ theme.fg("dim", emptyCell.repeat(empty)) +
33
+ theme.fg("dim", "]"));
34
+ }
35
+ export function formatContextBar(contextUsage, theme, glyphs, isAscii, barWidth = 10, cacheHitRate, showIconBar = false) {
36
+ const contextWindow = contextUsage?.contextWindow ?? 0;
37
+ if (contextWindow <= 0)
38
+ return "";
39
+ const contextPct = contextUsage?.percent ?? 0;
40
+ const contextColor = contextUsageColor(contextPct);
41
+ const pctText = theme.fg(contextColor, `${contextPct.toFixed(1)}%`);
42
+ const contextTokens = contextUsage?.tokens ?? 0;
43
+ const ctxText = `${theme.fg(contextColor, fmtTokens(contextTokens))}${theme.fg("dim", "/")}${theme.fg(contextColor, fmtTokens(contextWindow))}`;
44
+ const baseCore = `${pctText} ${theme.fg("dim", "·")} ${ctxText}`;
45
+ const base = showIconBar
46
+ ? `${theme.fg(contextColor, glyphs.context)} ${renderBar(theme, contextPct, barWidth, isAscii)} ${baseCore}`
47
+ : baseCore;
48
+ const rate = cacheHitRate !== undefined && Number.isFinite(cacheHitRate)
49
+ ? cacheHitRate
50
+ : 0;
51
+ const cacheText = `${glyphs.cacheHit} ${rate.toFixed(1)}%`;
52
+ return `${base} ${theme.fg("dim", "|")} ${theme.fg(cacheHitColor(rate), cacheText)}`;
53
+ }
54
+ /** Border adapter helper — one call from snapshot + theme/glyphs. */
55
+ export function formatTopContextFromSnapshot(snapshot, theme, glyphs, isAscii, showIconBar = false) {
56
+ return formatContextBar(snapshot.contextUsage, theme, glyphs, isAscii, 10, snapshot.totals.latestCacheHitRate, showIconBar);
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // Snapshot derivation — pure, owns cwd/sessionName/context/totals consolidation.
60
+ // ---------------------------------------------------------------------------
61
+ /**
62
+ * Create a chrome snapshot from extension context + footer state.
63
+ * Pure derivation: hides getUsageTotals caching, getContextUsage optional,
64
+ * and cwd/sessionName fallbacks behind one seam.
65
+ */
66
+ export function createChromeSnapshot(ctx, footerState) {
67
+ const cwd = ctx?.sessionManager?.getCwd?.() ??
68
+ // SAFETY: pi seam
69
+ ctx?.cwd ?? // SAFETY: pi seam — intentional unsafe cast, validated at runtime
70
+ process.cwd();
71
+ const sessionName = ctx?.sessionManager?.getSessionName?.();
72
+ const contextUsage = ctx?.getContextUsage?.();
73
+ const totals = getUsageTotals(
74
+ // SAFETY: pi seam
75
+ (ctx ?? {}));
76
+ // footerState may be absent when called from LiveBorder (context-only); use empty defaults
77
+ // SAFETY: pi seam
78
+ const git = footerState?.git ??
79
+ {
80
+ branch: undefined,
81
+ ahead: 0,
82
+ behind: 0,
83
+ modified: 0,
84
+ untracked: 0,
85
+ staged: 0,
86
+ stashed: 0,
87
+ conflicted: 0,
88
+ renamed: 0,
89
+ deleted: 0,
90
+ commit: null,
91
+ }; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
92
+ const runtime = footerState?.runtime ?? null;
93
+ return {
94
+ cwd,
95
+ sessionName,
96
+ git,
97
+ runtime,
98
+ contextUsage,
99
+ totals,
100
+ };
101
+ }
102
+ /**
103
+ * Deep module variant — owns snapshot + refresh lifecycle behind one seam.
104
+ * Thin wrapper over createChromeSnapshot for callers that prefer an instance.
105
+ */
106
+ export class ChromeState {
107
+ getFooterState;
108
+ getCtx;
109
+ constructor(getFooterState, getCtx) {
110
+ this.getFooterState = getFooterState;
111
+ this.getCtx = getCtx;
112
+ }
113
+ snapshot() {
114
+ return createChromeSnapshot(this.getCtx(), this.getFooterState());
115
+ }
116
+ /** Convenience: format top context bar from current snapshot + theme. */
117
+ formatTopContext(theme, glyphs, isAscii, showIconBar = false) {
118
+ return formatTopContextFromSnapshot(this.snapshot(), theme, glyphs, isAscii, showIconBar);
119
+ }
120
+ }
@@ -5,6 +5,17 @@ export function stressColor(value, warn = 70, danger = 90) {
5
5
  return "warning";
6
6
  return "accent";
7
7
  }
8
+ export function contextUsageColor(pct) {
9
+ // 25% 50% 75% thresholds — transit from dimmed (low) to highlight (high)
10
+ // for intuitive quota status: dim (<25) → accent (25-50) → warning (50-75) → error (≥75)
11
+ if (pct >= 75)
12
+ return "error";
13
+ if (pct >= 50)
14
+ return "warning";
15
+ if (pct >= 25)
16
+ return "accent";
17
+ return "dim";
18
+ }
8
19
  export function cacheHitColor(value) {
9
20
  if (value < 30)
10
21
  return "error";
package/dist/config.js CHANGED
@@ -91,24 +91,33 @@ function validate(config) {
91
91
  if (!isBoolean(config.enabled)) {
92
92
  config.enabled = DEFAULT_CONFIG.enabled;
93
93
  }
94
+ // SAFETY: intentional unsafe cast — validated at runtime
94
95
  if (!isBoolean(config.contextIconBar)) {
96
+ // SAFETY: intentional unsafe cast — validated at runtime
95
97
  config.contextIconBar = DEFAULT_CONFIG.contextIconBar;
96
98
  }
99
+ // SAFETY: intentional unsafe cast — validated at runtime
97
100
  const fs = config.footerSegments;
101
+ // SAFETY: intentional unsafe cast — validated at runtime
98
102
  const dfs = DEFAULT_CONFIG.footerSegments;
99
103
  for (const k of Object.keys(dfs)) {
100
104
  if (!isBoolean(fs[k]))
101
105
  fs[k] = dfs[k];
102
106
  }
107
+ // SAFETY: intentional unsafe cast — validated at runtime
103
108
  const t = config.telemetry;
109
+ // SAFETY: intentional unsafe cast — validated at runtime
104
110
  const dt = DEFAULT_CONFIG.telemetry;
105
111
  for (const k of Object.keys(dt)) {
106
112
  if (!isBoolean(t[k]))
107
113
  t[k] = dt[k];
108
114
  }
115
+ // SAFETY: intentional unsafe cast — validated at runtime
109
116
  const tl = config.timeline;
117
+ // SAFETY: intentional unsafe cast — validated at runtime
110
118
  const dtl = DEFAULT_CONFIG.timeline;
111
119
  if (!tl || typeof tl !== "object") {
120
+ // SAFETY: intentional unsafe cast — validated at runtime
112
121
  config.timeline = structuredClone(DEFAULT_CONFIG.timeline);
113
122
  }
114
123
  else {
@@ -129,7 +138,9 @@ export class ConfigStore {
129
138
  constructor(deps = {}) {
130
139
  this.readFile =
131
140
  deps.readFile ??
132
- ((p, enc) => readFileSync(p, enc));
141
+ ((p, enc) =>
142
+ // SAFETY: intentional unsafe cast — validated at runtime
143
+ readFileSync(p, enc));
133
144
  this.writeFile =
134
145
  deps.writeFile ??
135
146
  ((p, d, enc) => writeFileSync(p, d, enc));
@@ -150,7 +161,7 @@ export class ConfigStore {
150
161
  this.writeFile(p, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n", "utf8");
151
162
  }
152
163
  catch {
153
- // best-effort
164
+ // SAFETY: best-effort, ignore recoverable error
154
165
  }
155
166
  return structuredClone(DEFAULT_CONFIG);
156
167
  }
@@ -161,6 +172,7 @@ export class ConfigStore {
161
172
  return validate(merged);
162
173
  }
163
174
  catch (err) {
175
+ // SAFETY: best-effort, ignore recoverable error
164
176
  console.warn(`[pi-skill-desc] config parse error (${p}): ${err instanceof Error ? err.message : String(err)} — using defaults`);
165
177
  return structuredClone(DEFAULT_CONFIG);
166
178
  }
@@ -176,13 +188,14 @@ export class ConfigStore {
176
188
  this.writeFile(p, JSON.stringify(merged, null, 2) + "\n", "utf8");
177
189
  }
178
190
  catch {
179
- // best-effort
191
+ // SAFETY: best-effort, ignore recoverable error
180
192
  }
181
193
  for (const fn of this.listeners) {
182
194
  try {
183
195
  fn(merged, prev);
184
196
  }
185
197
  catch {
198
+ // SAFETY: best-effort, ignore recoverable error
186
199
  // subscriber error should not break store
187
200
  }
188
201
  }
@@ -17,10 +17,13 @@ function removeSoftwareCursor(line, cursorMarker = "") {
17
17
  function configureCursor(tui, cursorStyle) {
18
18
  if (cursorStyle === "block")
19
19
  return;
20
- const setShow = tui.setShowHardwareCursor;
20
+ const setShow =
21
+ // SAFETY: intentional unsafe cast — validated at runtime
22
+ tui.setShowHardwareCursor;
21
23
  if (typeof setShow === "function")
22
24
  setShow.call(tui, true);
23
25
  const seq = CURSOR_STYLE_SEQUENCES[cursorStyle];
26
+ // SAFETY: intentional unsafe cast — validated at runtime
24
27
  const term = tui
25
28
  .terminal;
26
29
  if (seq && term && typeof term.write === "function")
@@ -46,6 +49,7 @@ export class CursorPolicy {
46
49
  this.previewHardwareCursor = style !== "block";
47
50
  this.style = style;
48
51
  if (changed) {
52
+ // SAFETY: intentional unsafe cast — validated at runtime
49
53
  const tuiAny = this.tui;
50
54
  if (style === "block") {
51
55
  if (tuiAny.terminal)
@@ -37,7 +37,7 @@ export class FooterController {
37
37
  this.state = { ...this.state, git };
38
38
  }
39
39
  catch {
40
- // best-effort
40
+ // SAFETY: best-effort, ignore recoverable error
41
41
  }
42
42
  }
43
43
  async refreshRuntime() {
@@ -47,7 +47,7 @@ export class FooterController {
47
47
  this.state = { ...this.state, runtime };
48
48
  }
49
49
  catch {
50
- // best-effort
50
+ // SAFETY: best-effort, ignore recoverable error
51
51
  }
52
52
  }
53
53
  async refreshAll() {
package/dist/footer.js CHANGED
@@ -3,35 +3,9 @@ import { getUsageTotals } from "./state.js";
3
3
  import { resolveGlyphs, runtimeSymbol } from "./icons.js";
4
4
  import { alignRight, fitSegmentsByPriority, } from "./layout.js";
5
5
  import { basenamePath, formatCwd, truncatePath, } from "./path-format.js";
6
- import { cacheHitColor, stressColor, } from "./color-policy.js";
7
6
  import { fmtTokens, formatDuration, sanitizeStatus } from "./format.js";
8
- function renderBar(theme, pct, barWidth, ascii) {
9
- const filled = Math.max(0, Math.min(barWidth, Math.round((pct / 100) * barWidth)));
10
- const empty = barWidth - filled;
11
- const color = stressColor(pct);
12
- const filledCell = ascii ? "#" : "█";
13
- const emptyCell = ascii ? "-" : "░";
14
- return (theme.fg("dim", "[") +
15
- theme.fg(color, filledCell.repeat(filled)) +
16
- theme.fg("dim", emptyCell.repeat(empty)) +
17
- theme.fg("dim", "]"));
18
- }
19
- export function formatContextBar(contextUsage, theme, glyphs, isAscii, barWidth = 10, cacheHitRate, showIconBar = false) {
20
- const contextWindow = contextUsage?.contextWindow ?? 0;
21
- if (contextWindow <= 0)
22
- return "";
23
- const contextPct = contextUsage?.percent ?? 0;
24
- const pctText = theme.fg(stressColor(contextPct), `${contextPct.toFixed(1)}%`);
25
- const contextTokens = contextUsage?.tokens ?? 0;
26
- const ctxText = `${theme.fg("text", fmtTokens(contextTokens))}${theme.fg("dim", "/")}${theme.fg("text", fmtTokens(contextWindow))}`;
27
- const baseCore = `${pctText} ${theme.fg("dim", "·")} ${ctxText}`;
28
- const base = showIconBar
29
- ? `${theme.fg(stressColor(contextPct), glyphs.context)} ${renderBar(theme, contextPct, barWidth, isAscii)} ${baseCore}`
30
- : baseCore;
31
- const rate = cacheHitRate !== undefined && Number.isFinite(cacheHitRate) ? cacheHitRate : 0;
32
- const cacheText = `${glyphs.cacheHit} ${rate.toFixed(1)}%`;
33
- return `${base} ${theme.fg("dim", "|")} ${theme.fg(cacheHitColor(rate), cacheText)}`;
34
- }
7
+ // ChromeState owns context bar formatting — re-export for backward compat.
8
+ export { formatContextBar } from "./chrome-state.js";
35
9
  function renderGitSegment(theme, git, glyphs, segments) {
36
10
  const parts = [];
37
11
  if (segments.gitBranch) {
@@ -211,7 +185,9 @@ export function installFooter(ctx, getState, getConfig, getModelMeta, hooks) {
211
185
  const state = getState();
212
186
  const config = getConfig();
213
187
  const cwd = ctx.sessionManager?.getCwd() ?? process.cwd();
214
- const totals = getUsageTotals(ctx);
188
+ const totals = getUsageTotals(
189
+ // SAFETY: pi seam — intentional unsafe cast, validated at runtime
190
+ ctx);
215
191
  return renderFooter(width, state, config, themeStub, {
216
192
  cwd,
217
193
  sessionName: ctx.sessionManager?.getSessionName?.(),
@@ -222,8 +198,11 @@ export function installFooter(ctx, getState, getConfig, getModelMeta, hooks) {
222
198
  });
223
199
  };
224
200
  // Prefer native footer if available
225
- if (typeof ctx.ui.setFooter ===
201
+ if (
202
+ // SAFETY: pi seam — intentional unsafe cast, validated at runtime
203
+ typeof ctx.ui.setFooter ===
226
204
  "function") {
205
+ // SAFETY: pi seam — intentional unsafe cast, validated at runtime
227
206
  const ui = ctx.ui;
228
207
  ui.setFooter((tui, _theme, footerData) => {
229
208
  hooks.setRequestRender(() => tui.requestRender());
@@ -239,11 +218,14 @@ export function installFooter(ctx, getState, getConfig, getModelMeta, hooks) {
239
218
  invalidate() { },
240
219
  render(width) {
241
220
  // Use real theme when rendering
221
+ // SAFETY: pi seam — intentional unsafe cast, validated at runtime
242
222
  const theme = _theme;
243
223
  const state = getState();
244
224
  const config = getConfig();
245
225
  const cwd = ctx.sessionManager?.getCwd() ?? process.cwd();
246
- const totals = getUsageTotals(ctx);
226
+ const totals = getUsageTotals(
227
+ // SAFETY: pi seam — intentional unsafe cast, validated at runtime
228
+ ctx);
247
229
  return renderFooter(width, state, config, theme, {
248
230
  cwd,
249
231
  sessionName: ctx.sessionManager?.getSessionName?.(),
@@ -257,6 +239,7 @@ export function installFooter(ctx, getState, getConfig, getModelMeta, hooks) {
257
239
  };
258
240
  });
259
241
  return () => {
242
+ // SAFETY: pi seam — intentional unsafe cast, validated at runtime
260
243
  ctx.ui.setFooter(undefined);
261
244
  };
262
245
  }
package/dist/git.js CHANGED
@@ -29,6 +29,7 @@ async function gitExec(args, cwd) {
29
29
  return stdout;
30
30
  }
31
31
  catch {
32
+ // SAFETY: best-effort, ignore recoverable error
32
33
  return null;
33
34
  }
34
35
  }
package/dist/header.js CHANGED
@@ -109,7 +109,9 @@ export function installHeader(ctx, getConfig, getCwd, getTips) {
109
109
  };
110
110
  // Adapter: ExtensionUIContextLike.setWidget expects (key, content, options)
111
111
  // where content can be string[] or factory (tui, theme) => Component.
112
+ // SAFETY: pi seam — intentional unsafe cast, validated at runtime
112
113
  const ui = ctx.ui;
114
+ // SAFETY: intentional unsafe cast — validated at runtime
113
115
  ui.setWidget(key, factory, { placement: "aboveEditor" });
114
116
  return () => {
115
117
  ui.setWidget(key, undefined);