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 +26 -5
- package/dist/border-renderer.js +39 -9
- package/dist/chrome-state.js +120 -0
- package/dist/color-policy.js +11 -0
- package/dist/config.js +16 -3
- package/dist/cursor-policy.js +5 -1
- package/dist/footer-controller.js +2 -2
- package/dist/footer.js +14 -31
- package/dist/git.js +1 -0
- package/dist/header.js +2 -0
- package/dist/index.js +184 -284
- package/dist/live-border.js +161 -0
- package/dist/runtime.js +7 -3
- package/dist/session-kernel.js +25 -1
- package/dist/state.js +6 -2
- package/dist/telemetry.js +91 -54
- package/dist/theme-settings.js +4 -0
- package/dist/tracking-editor.js +90 -41
- package/dist/transcript-timeline.js +362 -0
- package/dist/utils.js +1 -1
- package/docs/reference/pi-tui-internals.md +3 -1
- package/package.json +10 -4
- package/src/border-renderer.ts +63 -11
- package/src/chrome-state.ts +211 -0
- package/src/color-policy.ts +9 -0
- package/src/config.ts +14 -2
- package/src/cursor-policy.ts +3 -0
- package/src/footer-controller.ts +2 -2
- package/src/footer.ts +8 -56
- package/src/git.ts +1 -0
- package/src/header.ts +2 -0
- package/src/index.ts +204 -307
- package/src/live-border.ts +200 -0
- package/src/runtime.ts +7 -3
- package/src/session-kernel.ts +34 -1
- package/src/state.ts +2 -0
- package/src/telemetry.ts +108 -61
- package/src/theme-settings.ts +4 -0
- package/src/tracking-editor.ts +103 -40
- package/src/transcript-timeline.ts +405 -0
- package/src/utils.ts +1 -1
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`
|
|
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
|
|
29
|
-
- Nerd mode cost now `$0.00` not ` $0.00` (always single `$` in both footer `renderFooter` and telemetry `formatTurnTelemetry`, was `glyph === "$"
|
|
30
|
-
- Cache session now always shown with zero value (`c 0.0%` in footer `↑ | ↓ | $ | c` and
|
|
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 (
|
|
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
|
package/dist/border-renderer.js
CHANGED
|
@@ -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.
|
|
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 (
|
|
50
|
-
out = embedTopWithLeftAndRight(out, width, leftLabel,
|
|
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 ||
|
|
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 =
|
|
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
|
|
110
|
-
|
|
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
|
+
}
|
package/dist/color-policy.js
CHANGED
|
@@ -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) =>
|
|
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
|
}
|
package/dist/cursor-policy.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
9
|
-
|
|
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(
|
|
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 (
|
|
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(
|
|
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
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);
|