pi-editor-footer 0.2.0 → 0.3.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 +29 -0
- package/dist/border-renderer.js +73 -18
- package/dist/config.js +21 -0
- package/dist/footer.js +27 -19
- package/dist/index.js +259 -9
- package/dist/model-info.js +2 -5
- package/dist/telemetry.js +7 -17
- package/dist/theme-settings.js +38 -4
- package/dist/tracking-editor.js +9 -0
- package/package.json +2 -2
- package/src/border-renderer.ts +76 -26
- package/src/config.ts +28 -0
- package/src/footer.ts +27 -22
- package/src/index.ts +247 -9
- package/src/model-info.ts +3 -7
- package/src/telemetry.ts +7 -25
- package/src/theme-settings.ts +43 -4
- package/src/tracking-editor.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.3.0] - 2026-08-21
|
|
8
|
+
### Changed
|
|
9
|
+
- 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
|
+
- 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
|
+
- 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)
|
|
12
|
+
- Wall time dim line left aligned and permanent between `agent_end` and next `agent_start` (`aboveEditor` `· 8s wall · ↑` dim left, not centered, stays in gap, never exposed)
|
|
13
|
+
- Dim line after `agent_end` now with specified timeline metrics (`timeline.enabled`/`wallTime`/`tokens`/`cost` in `ThemeConfig`, `Timeline` tab in `/pi-editor-footer` settings) — `· 8s wall · ↑ 1.2k · ↓ 800 · $0.12` dim aboveEditor, never exposed to model
|
|
14
|
+
- Wall time dim line after `agent_end` now positioned after last agent message in transcript (`aboveEditor` widget `· 8s wall · ↑ 1.2k · ↓ 800 · $0.12` dim, not footer second line) — visible right after `agent_end` message, hidden on next `agent_start`, never exposed to model
|
|
15
|
+
- Wall time dim line after `agent_end` now a separate footer row (was inline `cwd · wall`) — now `line1: cwd · runtime | ↑·↓·$` and `line2: · 8s wall · ↑ 1.2k · ↓ 800 · $0.12` dim, so timeline is visible
|
|
16
|
+
- Tokens separator now `·` not `|` (`↑ 1.2k · ↓ 800 · $0.12` in footer stats and `· 8s wall · ↑ · ↓ · $` dim line)
|
|
17
|
+
- Settings window title aligned to repo name `pi-editor-footer Settings` (was `pi-lsz-theme Settings`) and slash command renamed `pi-footer` → `pi-editor-footer`
|
|
18
|
+
- Tokens/cost relocated from telemetry bottom to wall time dim line — bottom now `TPS 4.0 tok/s · TTFT 4.0s · 5.0s` (no `↑`/`↓`/`$`), dim line after `agent_end` is `· 8s wall · ↑ 1.2k · ↓ 800 · $0.12` (all `dim`, TUI-only, never exposed to model, via `FooterState.lastDoneIn` + `getUsageTotals` gated by `telemetry.tokens`/`cost`)
|
|
19
|
+
- Git branch never folded — `renderGitSegment` no longer `truncateBranch(…,20)`, full `feature/very-long-…` shown; priority `6` (> cwd `5`) so `fitSegmentsByPriority` never omits branch before cwd/runtime/stats
|
|
20
|
+
- Telemetry TPS display throttled to 1 s (data vs display separated) — `message_update` now only updates `liveDeltaChars`/`liveEstimatedTokens` in `TurnTelemetryTracker`, display `peekLive()` → `formatTurnTelemetry` only via `liveTickTimer` every `REFRESH_MS` and `message_start`/`end`/`turn_end`/`agent_settled` for approximately accurate TPS without per-delta TUI jank
|
|
21
|
+
- Telemetry cost toggling `telemetry.cost` off/on no longer resets cost to `$0.00` — `refreshLiveTelemetry` now falls back to `getLastTelemetry()` when `peekLive()` is null (idle) and `onConfigChanged` calls `refreshLiveTelemetry()` immediately
|
|
22
|
+
- Refresh rate set to one second (`REFRESH_MS = 1000` for `liveTickTimer` telemetry/top/context and `watchTimer` editor ownership watchdog, was hardcoded `1000`)
|
|
23
|
+
- 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
|
+
- 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
|
|
26
|
+
- Telemetry `>`, `~`, `+` glyphs removed (`TPS 60.6 tok/s · TTFT 2.5s · 8.3s · ↑395 · ↓505 · $0.16` not `> TPS … · ~ TTFT … · +8.3s`)
|
|
27
|
+
- 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
|
|
31
|
+
- 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
|
+
- 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 `|`)
|
|
34
|
+
- 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
|
+
|
|
7
36
|
## [0.2.0] - 2026-08-21
|
|
8
37
|
|
|
9
38
|
### Added
|
package/dist/border-renderer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
import {
|
|
2
|
+
import { buildLabel, } from "./model-info.js";
|
|
3
3
|
function stripAnsi(s) {
|
|
4
4
|
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
5
5
|
}
|
|
@@ -23,25 +23,51 @@ export class BorderRenderer {
|
|
|
23
23
|
let out = [...lines];
|
|
24
24
|
if (out.length === 0)
|
|
25
25
|
return out;
|
|
26
|
-
// Top
|
|
27
|
-
|
|
26
|
+
// Top: model info left, context bar right of it, run-activity far right
|
|
27
|
+
const hasTop = opts.glowEnabled || opts.topRightText || opts.topContextText;
|
|
28
|
+
if (hasTop) {
|
|
29
|
+
const theme = this.getLiveTheme();
|
|
30
|
+
const info = this.getModelInfo();
|
|
31
|
+
const glow = (s) => {
|
|
32
|
+
try {
|
|
33
|
+
const maybeGlow = theme.getThinkingBorderColor;
|
|
34
|
+
if (typeof maybeGlow === "function")
|
|
35
|
+
return maybeGlow.call(theme, info.level)(s);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// ignore
|
|
39
|
+
}
|
|
40
|
+
return s;
|
|
41
|
+
};
|
|
28
42
|
if (opts.glowEnabled) {
|
|
29
|
-
|
|
43
|
+
// Build left label; if context bar present, append it to the right of model info with pipe separator
|
|
44
|
+
let leftLabel = buildLabel(theme, info.provider, info.modelId, info.level, info.contextWindow);
|
|
45
|
+
if (opts.topContextText) {
|
|
46
|
+
leftLabel = `${leftLabel} ${theme.fg("dim", "|")} ${opts.topContextText}`;
|
|
47
|
+
}
|
|
48
|
+
// 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
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Only left (model+context), no right — use applyModelInfo replacement but with combined left
|
|
54
|
+
// Reuse embedTopWithLeftAndRight with empty right
|
|
55
|
+
out = embedTopWithLeftAndRight(out, width, leftLabel, "", glow);
|
|
56
|
+
}
|
|
30
57
|
}
|
|
31
|
-
if (opts.topRightText) {
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
out = embedTopRightBorder(out, width, opts.topRightText, glow);
|
|
58
|
+
else if (opts.topContextText || opts.topRightText) {
|
|
59
|
+
// No glow, but still need top borders for context/run activity
|
|
60
|
+
const left = opts.topContextText ?? "";
|
|
61
|
+
const right = opts.topRightText ?? "";
|
|
62
|
+
if (left && right) {
|
|
63
|
+
out = embedTopWithLeftAndRight(out, width, left, right, glow);
|
|
64
|
+
}
|
|
65
|
+
else if (left) {
|
|
66
|
+
out = embedTopRightBorder(out, width, left, glow);
|
|
67
|
+
}
|
|
68
|
+
else if (right) {
|
|
69
|
+
out = embedTopRightBorder(out, width, right, glow);
|
|
70
|
+
}
|
|
45
71
|
}
|
|
46
72
|
}
|
|
47
73
|
// Bottom embedding (telemetry left/right)
|
|
@@ -63,6 +89,35 @@ export class BorderRenderer {
|
|
|
63
89
|
return out;
|
|
64
90
|
}
|
|
65
91
|
}
|
|
92
|
+
function embedTopWithLeftAndRight(lines, width, leftText, rightText, getGlow) {
|
|
93
|
+
if (lines.length === 0)
|
|
94
|
+
return lines;
|
|
95
|
+
const top = lines[0] ?? "";
|
|
96
|
+
const plainTop = stripAnsi(top);
|
|
97
|
+
if (/^─── [↑↓] \d+ more/.test(plainTop))
|
|
98
|
+
return lines;
|
|
99
|
+
const leftW = leftText ? visibleWidth(leftText) : 0;
|
|
100
|
+
const rightW = rightText ? visibleWidth(rightText) : 0;
|
|
101
|
+
const maxLeft = rightText ? Math.max(0, width - rightW - 6) : width - 3;
|
|
102
|
+
const maxRight = leftText ? Math.max(0, width - leftW - 6) : width - 3;
|
|
103
|
+
let displayLeft = leftText;
|
|
104
|
+
let displayRight = rightText;
|
|
105
|
+
if (leftW > maxLeft)
|
|
106
|
+
displayLeft = truncateToWidth(leftText, maxLeft, "");
|
|
107
|
+
if (rightW > maxRight)
|
|
108
|
+
displayRight = truncateToWidth(rightText, maxRight, "");
|
|
109
|
+
const leftSegment = displayLeft ? `${getGlow("─")} ${displayLeft} ` : getGlow("─");
|
|
110
|
+
const rightSegment = displayRight ? ` ${displayRight} ${getGlow("─")}` : getGlow("─");
|
|
111
|
+
const used = visibleWidth(leftSegment) + visibleWidth(rightSegment);
|
|
112
|
+
const middleWidth = Math.max(0, width - used);
|
|
113
|
+
const middle = getGlow("─".repeat(middleWidth));
|
|
114
|
+
const embedded = `${leftSegment}${middle}${rightSegment}`;
|
|
115
|
+
const result = [...lines];
|
|
116
|
+
result[0] = truncateToWidth(embedded, width, "");
|
|
117
|
+
if (visibleWidth(embedded) !== width)
|
|
118
|
+
result[0] = embedded;
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
66
121
|
function embedBottomBorder(lines, width, leftText, rightText, getGlow) {
|
|
67
122
|
if ((!leftText && !rightText) || lines.length === 0)
|
|
68
123
|
return lines;
|
package/dist/config.js
CHANGED
|
@@ -9,6 +9,7 @@ export const DEFAULT_CONFIG = {
|
|
|
9
9
|
icons: {
|
|
10
10
|
mode: "auto",
|
|
11
11
|
},
|
|
12
|
+
contextIconBar: false,
|
|
12
13
|
footerSegments: {
|
|
13
14
|
cwd: true,
|
|
14
15
|
sessionName: false,
|
|
@@ -30,6 +31,12 @@ export const DEFAULT_CONFIG = {
|
|
|
30
31
|
stalls: true,
|
|
31
32
|
cost: true,
|
|
32
33
|
},
|
|
34
|
+
timeline: {
|
|
35
|
+
enabled: true,
|
|
36
|
+
wallTime: true,
|
|
37
|
+
tokens: true,
|
|
38
|
+
cost: true,
|
|
39
|
+
},
|
|
33
40
|
};
|
|
34
41
|
export function getConfigPath() {
|
|
35
42
|
const home = homedir();
|
|
@@ -84,6 +91,9 @@ function validate(config) {
|
|
|
84
91
|
if (!isBoolean(config.enabled)) {
|
|
85
92
|
config.enabled = DEFAULT_CONFIG.enabled;
|
|
86
93
|
}
|
|
94
|
+
if (!isBoolean(config.contextIconBar)) {
|
|
95
|
+
config.contextIconBar = DEFAULT_CONFIG.contextIconBar;
|
|
96
|
+
}
|
|
87
97
|
const fs = config.footerSegments;
|
|
88
98
|
const dfs = DEFAULT_CONFIG.footerSegments;
|
|
89
99
|
for (const k of Object.keys(dfs)) {
|
|
@@ -96,6 +106,17 @@ function validate(config) {
|
|
|
96
106
|
if (!isBoolean(t[k]))
|
|
97
107
|
t[k] = dt[k];
|
|
98
108
|
}
|
|
109
|
+
const tl = config.timeline;
|
|
110
|
+
const dtl = DEFAULT_CONFIG.timeline;
|
|
111
|
+
if (!tl || typeof tl !== "object") {
|
|
112
|
+
config.timeline = structuredClone(DEFAULT_CONFIG.timeline);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
for (const k of Object.keys(dtl)) {
|
|
116
|
+
if (!isBoolean(tl[k]))
|
|
117
|
+
tl[k] = dtl[k];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
99
120
|
return config;
|
|
100
121
|
}
|
|
101
122
|
export class ConfigStore {
|
package/dist/footer.js
CHANGED
|
@@ -2,7 +2,7 @@ import { truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-work
|
|
|
2
2
|
import { getUsageTotals } from "./state.js";
|
|
3
3
|
import { resolveGlyphs, runtimeSymbol } from "./icons.js";
|
|
4
4
|
import { alignRight, fitSegmentsByPriority, } from "./layout.js";
|
|
5
|
-
import { basenamePath, formatCwd,
|
|
5
|
+
import { basenamePath, formatCwd, truncatePath, } from "./path-format.js";
|
|
6
6
|
import { cacheHitColor, stressColor, } from "./color-policy.js";
|
|
7
7
|
import { fmtTokens, formatDuration, sanitizeStatus } from "./format.js";
|
|
8
8
|
function renderBar(theme, pct, barWidth, ascii) {
|
|
@@ -16,7 +16,7 @@ function renderBar(theme, pct, barWidth, ascii) {
|
|
|
16
16
|
theme.fg("dim", emptyCell.repeat(empty)) +
|
|
17
17
|
theme.fg("dim", "]"));
|
|
18
18
|
}
|
|
19
|
-
export function formatContextBar(contextUsage, theme, glyphs, isAscii, barWidth = 10) {
|
|
19
|
+
export function formatContextBar(contextUsage, theme, glyphs, isAscii, barWidth = 10, cacheHitRate, showIconBar = false) {
|
|
20
20
|
const contextWindow = contextUsage?.contextWindow ?? 0;
|
|
21
21
|
if (contextWindow <= 0)
|
|
22
22
|
return "";
|
|
@@ -24,15 +24,20 @@ export function formatContextBar(contextUsage, theme, glyphs, isAscii, barWidth
|
|
|
24
24
|
const pctText = theme.fg(stressColor(contextPct), `${contextPct.toFixed(1)}%`);
|
|
25
25
|
const contextTokens = contextUsage?.tokens ?? 0;
|
|
26
26
|
const ctxText = `${theme.fg("text", fmtTokens(contextTokens))}${theme.fg("dim", "/")}${theme.fg("text", fmtTokens(contextWindow))}`;
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
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)}`;
|
|
30
34
|
}
|
|
31
|
-
function renderGitSegment(theme, git, glyphs, segments
|
|
35
|
+
function renderGitSegment(theme, git, glyphs, segments) {
|
|
32
36
|
const parts = [];
|
|
33
37
|
if (segments.gitBranch) {
|
|
34
38
|
if (git.branch) {
|
|
35
|
-
|
|
39
|
+
// Never truncate branch name — full name always shown per user request
|
|
40
|
+
parts.push(theme.fg("mdLink", git.branch));
|
|
36
41
|
}
|
|
37
42
|
else if (git.commit?.detached) {
|
|
38
43
|
parts.push(theme.fg("warning", "HEAD"));
|
|
@@ -80,12 +85,17 @@ function renderRuntimeSegment(theme, runtime, iconMode) {
|
|
|
80
85
|
const label = [symbol, version].filter(Boolean).join(" ");
|
|
81
86
|
return label;
|
|
82
87
|
}
|
|
83
|
-
function renderTimerSegment(theme, state, glyphs) {
|
|
88
|
+
function renderTimerSegment(theme, state, glyphs, totals, config) {
|
|
89
|
+
// Wall time dim line — never exposed to model, TUI-only
|
|
90
|
+
// After agent_end: · 8.3s wall · ↑ 1.2k · ↓ 800 · $0.12 (all dim)
|
|
91
|
+
// During run: working 8.3s (accent)
|
|
84
92
|
if (state.workingSince !== undefined) {
|
|
85
93
|
return `${theme.fg("accent", glyphs.working)} ${theme.fg("dim", "working")} ${theme.fg("accent", formatDuration(Date.now() - state.workingSince))}`;
|
|
86
94
|
}
|
|
87
95
|
if (state.lastDoneIn !== undefined) {
|
|
88
|
-
|
|
96
|
+
// Wall time after agent_end is now a dim row after last message (aboveEditor widget), not inline footer
|
|
97
|
+
// Keep footer clean — wall time lives in transcript, not here
|
|
98
|
+
return "";
|
|
89
99
|
}
|
|
90
100
|
return "";
|
|
91
101
|
}
|
|
@@ -135,7 +145,8 @@ export function renderFooter(width, state, config, theme, ctx) {
|
|
|
135
145
|
const gitSeg = renderGitSegment(theme, state.git, glyphs, segments);
|
|
136
146
|
if (gitSeg) {
|
|
137
147
|
const sep = leftParts.length > 0 ? `${theme.fg("dim", " · ")}` : "";
|
|
138
|
-
|
|
148
|
+
// Branch name must never be folded/omitted — keep priority higher than cwd (5)
|
|
149
|
+
leftParts.push({ text: `${sep}${gitSeg}`, priority: 6 });
|
|
139
150
|
}
|
|
140
151
|
if (segments.runtime) {
|
|
141
152
|
const runtimeSeg = renderRuntimeSegment(theme, state.runtime, config.icons.mode);
|
|
@@ -144,7 +155,7 @@ export function renderFooter(width, state, config, theme, ctx) {
|
|
|
144
155
|
leftParts.push({ text: `${sep}${runtimeSeg}`, priority: 4 });
|
|
145
156
|
}
|
|
146
157
|
}
|
|
147
|
-
const timerSeg = renderTimerSegment(theme, state, glyphs);
|
|
158
|
+
const timerSeg = renderTimerSegment(theme, state, glyphs, totals, config);
|
|
148
159
|
if (timerSeg) {
|
|
149
160
|
const sep = leftParts.length > 0 ? `${theme.fg("dim", " • ")}` : "";
|
|
150
161
|
leftParts.push({ text: `${sep}${timerSeg}`, priority: 1 });
|
|
@@ -153,18 +164,15 @@ export function renderFooter(width, state, config, theme, ctx) {
|
|
|
153
164
|
if (segments.tokens) {
|
|
154
165
|
stats.push(theme.fg("accent", `${glyphs.input} ${fmtTokens(totals.input)}`));
|
|
155
166
|
stats.push(theme.fg("success", `${glyphs.output} ${fmtTokens(totals.output)}`));
|
|
156
|
-
const hasCacheTokens = totals.cacheRead > 0 || totals.cacheWrite > 0;
|
|
157
|
-
if (hasCacheTokens && totals.latestCacheHitRate !== undefined) {
|
|
158
|
-
stats.push(theme.fg(cacheHitColor(totals.latestCacheHitRate), `${glyphs.cacheHit} ${totals.latestCacheHitRate.toFixed(1)}%`));
|
|
159
|
-
}
|
|
160
167
|
}
|
|
161
168
|
if (segments.cost) {
|
|
162
|
-
const costValue = totals.cost.toFixed(
|
|
163
|
-
// Avoid "$ $0.
|
|
164
|
-
const costText =
|
|
169
|
+
const costValue = totals.cost.toFixed(2);
|
|
170
|
+
// Avoid "$ $0.00" when the cost glyph itself is "$" (ascii mode) — glyph already is the currency symbol
|
|
171
|
+
const costText = `$${costValue}`;
|
|
165
172
|
stats.push(theme.fg("warning", costText));
|
|
166
173
|
}
|
|
167
|
-
|
|
174
|
+
// cache to the right of input/output omitted per user request — cache stays in top context bar only
|
|
175
|
+
const statsBlock = stats.join(` ${theme.fg("dim", "·")} `);
|
|
168
176
|
const rightBlock = statsBlock;
|
|
169
177
|
const rightCompact = statsBlock;
|
|
170
178
|
const allParts = [...leftParts];
|