pi-zentui 0.24.0 → 0.25.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/README.md +4 -2
- package/docs/configuration.md +10 -3
- package/extensions/zentui/accent-rail-editor.ts +2 -2
- package/extensions/zentui/config.ts +11 -3
- package/extensions/zentui/footer.ts +8 -3
- package/extensions/zentui/format.ts +2 -1
- package/extensions/zentui/icons.ts +53 -13
- package/extensions/zentui/index.ts +4 -2
- package/extensions/zentui/minimalist-editor.ts +16 -1
- package/extensions/zentui/settings-command.ts +13 -4
- package/extensions/zentui/settings-previews.ts +1 -0
- package/extensions/zentui/working-line.ts +81 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,7 +114,7 @@ The displayed preset is derived from your current selections: individual changes
|
|
|
114
114
|
|
|
115
115
|
### Minimal overrides
|
|
116
116
|
|
|
117
|
-
Installation enables Opencode Editor, Framed User messages, Zentui selector borders, and Starship Footer. Working line and Thinking (Experimental) remain disabled. Missing fields retain those defaults; there is no automatic migration. Auto icons
|
|
117
|
+
Installation enables Opencode Editor, Framed User messages, Zentui selector borders, and Starship Footer. Working line and Thinking (Experimental) remain disabled. Missing fields retain those defaults; there is no automatic migration. Auto icons use Nerd glyphs for iTerm2, WezTerm, Ghostty, Kitty, and Alacritty environment signals, and otherwise fall back to ASCII. Set `ZENTUI_NERD_FONTS=1` or `0` to override Auto. These environment heuristics cannot detect whether a Nerd Font is installed or configured; explicit `nerd` and `ascii` modes always win. ASCII changes icons, not the entire UI.
|
|
118
118
|
|
|
119
119
|
Change only what you need. For example, this changes only the Editor's accent:
|
|
120
120
|
|
|
@@ -172,10 +172,12 @@ Pi 0.84 also provides a native fullscreen TUI with a sticky editor and Footer. P
|
|
|
172
172
|
|
|
173
173
|
**Codex quota (opt-in):** Show remaining 5-hour/weekly quota for `openai-codex` through independent Editor and Starship Footer settings, both off by default. See [configuration and private-endpoint limitations](./docs/configuration.md#codex-account-quota), including `$codex_quota` for custom templates.
|
|
174
174
|
|
|
175
|
+
Minimalist can also show the latest assistant prompt's cache hit rate in its top-right metadata. Enable **Editor → Minimalist → Cache hit rate** or set `components.editor.styles.minimalist.showCacheHit` to `true`; it is off by default and does not depend on Footer.
|
|
176
|
+
|
|
175
177
|
## Requirements
|
|
176
178
|
|
|
177
179
|
- [Pi](https://pi.dev) coding agent 0.80.5 or newer
|
|
178
|
-
- A [Nerd Font](https://www.nerdfonts.com/) for icons
|
|
180
|
+
- A [Nerd Font](https://www.nerdfonts.com/) for Nerd icons; Auto falls back to ASCII when no supported terminal signal is present
|
|
179
181
|
|
|
180
182
|
## Development
|
|
181
183
|
|
package/docs/configuration.md
CHANGED
|
@@ -171,6 +171,7 @@ Reference only—not a starter file. Prefer the minimal overrides above. Optiona
|
|
|
171
171
|
"showSessionName": true,
|
|
172
172
|
"showTimer": true,
|
|
173
173
|
"showCost": true,
|
|
174
|
+
"showCacheHit": false,
|
|
174
175
|
"showGit": true,
|
|
175
176
|
"contextThresholds": {
|
|
176
177
|
"warning": 70,
|
|
@@ -475,9 +476,9 @@ Set `ZENTUI_DEBUG=1` when launching Pi to log the workaround diagnostic without
|
|
|
475
476
|
|
|
476
477
|
### Minimalist
|
|
477
478
|
|
|
478
|
-
Set `components.editor.style` to `minimalist` or select it in `/zentui`. The rounded frame places viewport counts, Bash state, current/completed turn duration, and explicit session name at top left; cost, model, thinking, and
|
|
479
|
+
Set `components.editor.style` to `minimalist` or select it in `/zentui`. The rounded frame places viewport counts, Bash state, current/completed turn duration, and explicit session name at top left; cost, model, thinking, context, and optional latest-prompt cache hit rate at top right; viewport count plus Git at bottom left; and configured path at bottom right. Unnamed sessions add no placeholder.
|
|
479
480
|
|
|
480
|
-
Path examples are `src` (`compact`), `zentui/src` (`project`), and `~/Projects/zentui/src` (`full`). Context can render as `11%`, `11%/372k`, or, with the gauge enabled and enough room, `[█░░░░] 11%/372k`. The gauge shortens or disappears before the context text at narrow widths. Session name, timer, cost, and Git can be hidden independently; model, thinking, and context remain structurally stable.
|
|
481
|
+
Path examples are `src` (`compact`), `zentui/src` (`project`), and `~/Projects/zentui/src` (`full`). Context can render as `11%`, `11%/372k`, or, with the gauge enabled and enough room, `[█░░░░] 11%/372k`. Enable `showCacheHit` to append values such as `Cache 98.2%`; it defaults to `false`, omits missing data, yields before context at narrow widths, and remains independent of Footer. The gauge shortens or disappears before the context text at narrow widths. Session name, timer, cost, cache hit rate, and Git can be hidden independently; model, thinking, and context remain structurally stable.
|
|
481
482
|
|
|
482
483
|
Autocomplete stays inside the frame when Pi output can be split safely. Unknown third-party layouts fail open. Footer visibility remains independently controlled by `components.footer.style`; Minimalist does not remove Pi's header.
|
|
483
484
|
|
|
@@ -637,9 +638,15 @@ Zentui accepts at most 16 unique keys, keys up to 64 code units, and values up t
|
|
|
637
638
|
| `↓` | Behind |
|
|
638
639
|
| `⇕` | Diverged |
|
|
639
640
|
|
|
641
|
+
## Icon Auto detection
|
|
642
|
+
|
|
643
|
+
`icons.mode: "auto"` preserves Auto in memory and on disk while deriving an effective mode for the current process. Exact `ZENTUI_NERD_FONTS=1` or `0` overrides Auto. Otherwise Auto selects Nerd glyphs when `TERM_PROGRAM` is `iTerm.app`, `WezTerm`, or `ghostty` (case-insensitive), or when `KITTY_WINDOW_ID` or `ALACRITTY_SOCKET` is nonempty. Unknown terminals, VS Code, and Windows Terminal default to ASCII-safe glyphs. Explicit `nerd` and `ascii` modes ignore the override and environment signals.
|
|
644
|
+
|
|
645
|
+
This is conservative terminal-environment detection, not font probing: terminal identity cannot prove that a Nerd Font is installed or configured. Use explicit mode or `ZENTUI_NERD_FONTS` when Auto chooses incorrectly. Custom icon overrides still win over either effective mode.
|
|
646
|
+
|
|
640
647
|
## Runtime detection
|
|
641
648
|
|
|
642
|
-
Runtime/language modules use Starship Nerd Font symbols and defaults such as `bold green` for Node.js. Theme mode maps those styles through Pi; Footer terminal mode uses the terminal colorscheme's ANSI colors.
|
|
649
|
+
Runtime/language modules use Starship Nerd Font symbols and defaults such as `bold green` for Node.js. Theme mode maps those styles through Pi; Footer terminal mode uses the terminal colorscheme's ANSI colors. In Auto mode, runtime, OS, package, rail, and gauge symbols all use the same derived effective icon mode.
|
|
643
650
|
|
|
644
651
|
| Runtime/language | Detection examples |
|
|
645
652
|
| --- | --- |
|
|
@@ -41,8 +41,8 @@ function clampLines(lines: string[], width: number): string[] {
|
|
|
41
41
|
|
|
42
42
|
function selectedRail(config: ZentuiConfig): string {
|
|
43
43
|
const style = config.components.editor.styles["accent-rail"];
|
|
44
|
-
const fallback = config.icons.
|
|
45
|
-
const configured = config.icons.
|
|
44
|
+
const fallback = config.icons.effectiveMode === "ascii" ? "|" : "▎";
|
|
45
|
+
const configured = config.icons.effectiveMode === "ascii" ? style.asciiRail : style.rail;
|
|
46
46
|
const sanitized = sanitizeEditorMetadataText(configured);
|
|
47
47
|
const glyph = truncateToWidth(sanitized, 1, "");
|
|
48
48
|
return visibleWidth(glyph) === 1 ? glyph : fallback;
|
|
@@ -24,9 +24,9 @@ import {
|
|
|
24
24
|
} from "./component-colors";
|
|
25
25
|
import {
|
|
26
26
|
ICON_GLYPH_KEYS,
|
|
27
|
+
type IconEnvironment,
|
|
27
28
|
type IconGlyphs,
|
|
28
29
|
type IconMode,
|
|
29
|
-
NERD_DEFAULT_ICONS,
|
|
30
30
|
normalizeIconMode,
|
|
31
31
|
type ResolvedIcons,
|
|
32
32
|
resolveConfiguredIcons,
|
|
@@ -139,6 +139,7 @@ export type MinimalistEditorStyleConfig = {
|
|
|
139
139
|
showSessionName: boolean;
|
|
140
140
|
showTimer: boolean;
|
|
141
141
|
showCost: boolean;
|
|
142
|
+
showCacheHit: boolean;
|
|
142
143
|
showGit: boolean;
|
|
143
144
|
contextThresholds: ContextThresholds;
|
|
144
145
|
};
|
|
@@ -477,6 +478,7 @@ const defaultMinimalistStyle: MinimalistEditorStyleConfig = {
|
|
|
477
478
|
showSessionName: true,
|
|
478
479
|
showTimer: true,
|
|
479
480
|
showCost: true,
|
|
481
|
+
showCacheHit: false,
|
|
480
482
|
showGit: true,
|
|
481
483
|
contextThresholds: { warning: 70, error: 90 },
|
|
482
484
|
};
|
|
@@ -550,7 +552,7 @@ const defaultComponents: ComponentsConfig = {
|
|
|
550
552
|
|
|
551
553
|
export const defaultConfig: PolishedTuiConfig = {
|
|
552
554
|
projectRefreshIntervalMs: DEFAULT_PROJECT_REFRESH_INTERVAL_MS,
|
|
553
|
-
icons:
|
|
555
|
+
icons: resolveConfiguredIcons("auto"),
|
|
554
556
|
colors: {
|
|
555
557
|
cwd: "bold cyan",
|
|
556
558
|
sessionName: "bold green",
|
|
@@ -1312,6 +1314,7 @@ function resolveComponents(config: ConfigRecord): ComponentsConfig {
|
|
|
1312
1314
|
),
|
|
1313
1315
|
showTimer: parseBoolean(minimalist.showTimer, defaultMinimalistStyle.showTimer),
|
|
1314
1316
|
showCost: parseBoolean(minimalist.showCost, defaultMinimalistStyle.showCost),
|
|
1317
|
+
showCacheHit: parseBoolean(minimalist.showCacheHit, defaultMinimalistStyle.showCacheHit),
|
|
1315
1318
|
showGit: parseBoolean(minimalist.showGit, defaultMinimalistStyle.showGit),
|
|
1316
1319
|
contextThresholds: minimalistThresholds,
|
|
1317
1320
|
},
|
|
@@ -1529,7 +1532,10 @@ export function hasUnsupportedComponentStyle(
|
|
|
1529
1532
|
return unsupportedComponentStyles.get(config)?.has(owner) ?? false;
|
|
1530
1533
|
}
|
|
1531
1534
|
|
|
1532
|
-
export function mergeConfig(
|
|
1535
|
+
export function mergeConfig(
|
|
1536
|
+
parsed: unknown,
|
|
1537
|
+
iconEnvironment: IconEnvironment = process.env,
|
|
1538
|
+
): PolishedTuiConfig {
|
|
1533
1539
|
const config = isRecord(parsed) ? parsed : {};
|
|
1534
1540
|
const iconsRecord = recordValue(config.icons);
|
|
1535
1541
|
const colorsRecord = recordValue(config.colors);
|
|
@@ -1539,6 +1545,7 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
1539
1545
|
icons: resolveConfiguredIcons(
|
|
1540
1546
|
normalizeIconMode(iconsRecord.mode),
|
|
1541
1547
|
normalizeIconOverrides(iconsRecord),
|
|
1548
|
+
iconEnvironment,
|
|
1542
1549
|
),
|
|
1543
1550
|
colors: {
|
|
1544
1551
|
...defaultConfig.colors,
|
|
@@ -1806,6 +1813,7 @@ function applyMinimalistStylePatch(
|
|
|
1806
1813
|
if (patch.showSessionName !== undefined) style.showSessionName = patch.showSessionName;
|
|
1807
1814
|
if (patch.showTimer !== undefined) style.showTimer = patch.showTimer;
|
|
1808
1815
|
if (patch.showCost !== undefined) style.showCost = patch.showCost;
|
|
1816
|
+
if (patch.showCacheHit !== undefined) style.showCacheHit = patch.showCacheHit;
|
|
1809
1817
|
if (patch.showGit !== undefined) style.showGit = patch.showGit;
|
|
1810
1818
|
if (patch.contextThresholds !== undefined) {
|
|
1811
1819
|
style.contextThresholds = { ...style.contextThresholds, ...patch.contextThresholds };
|
|
@@ -240,7 +240,7 @@ export function installFooter(
|
|
|
240
240
|
FOOTER_FORMAT_ALIASES,
|
|
241
241
|
);
|
|
242
242
|
const colorSource = config.components.footer.colorSource;
|
|
243
|
-
const iconMode = config.icons.
|
|
243
|
+
const iconMode = config.icons.effectiveMode;
|
|
244
244
|
const pathDisplay = config.components.footer.styles.starship.pathDisplay;
|
|
245
245
|
const formattedCwd = sanitizeEditorMetadataText(
|
|
246
246
|
formatCwdLabel(ctx.cwd, config.icons.cwd, {
|
|
@@ -451,7 +451,12 @@ export function installFooter(
|
|
|
451
451
|
theme,
|
|
452
452
|
colorSource,
|
|
453
453
|
componentColor(config, "footer", "os"),
|
|
454
|
-
formatOsLabel(
|
|
454
|
+
formatOsLabel(
|
|
455
|
+
config.icons.os,
|
|
456
|
+
iconMode,
|
|
457
|
+
process.platform,
|
|
458
|
+
config.icons.osOverridden,
|
|
459
|
+
),
|
|
455
460
|
);
|
|
456
461
|
case "time":
|
|
457
462
|
return renderStyleForSource(
|
|
@@ -672,7 +677,7 @@ export function installFooter(
|
|
|
672
677
|
theme,
|
|
673
678
|
colorSource,
|
|
674
679
|
componentColor(config, "footer", "os"),
|
|
675
|
-
formatOsLabel(config.icons.os, iconMode),
|
|
680
|
+
formatOsLabel(config.icons.os, iconMode, process.platform, config.icons.osOverridden),
|
|
676
681
|
)
|
|
677
682
|
: "";
|
|
678
683
|
const left = [
|
|
@@ -572,6 +572,7 @@ export function formatOsLabel(
|
|
|
572
572
|
configuredIcon: string,
|
|
573
573
|
mode: IconMode = "auto",
|
|
574
574
|
platform: string = process.platform,
|
|
575
|
+
osOverridden = false,
|
|
575
576
|
): string {
|
|
576
|
-
return resolveOsIcon(configuredIcon, mode, platform);
|
|
577
|
+
return resolveOsIcon(configuredIcon, mode, platform, process.env, osOverridden);
|
|
577
578
|
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export type IconMode = "auto" | "nerd" | "ascii";
|
|
10
|
+
export type EffectiveIconMode = Exclude<IconMode, "auto">;
|
|
11
|
+
export type IconEnvironment = Readonly<Record<string, string | undefined>>;
|
|
10
12
|
|
|
11
13
|
export type IconGlyphs = {
|
|
12
14
|
cwd: string;
|
|
@@ -31,7 +33,14 @@ export type IconGlyphs = {
|
|
|
31
33
|
package: string;
|
|
32
34
|
};
|
|
33
35
|
|
|
34
|
-
export type ResolvedIcons = IconGlyphs & {
|
|
36
|
+
export type ResolvedIcons = IconGlyphs & {
|
|
37
|
+
/** Canonical user choice; Auto is never replaced with an environment-specific value. */
|
|
38
|
+
mode: IconMode;
|
|
39
|
+
/** Runtime-only mode derived from the canonical choice and current environment. */
|
|
40
|
+
effectiveMode: EffectiveIconMode;
|
|
41
|
+
/** Runtime-only provenance needed when an override equals the active mode default. */
|
|
42
|
+
osOverridden: boolean;
|
|
43
|
+
};
|
|
35
44
|
|
|
36
45
|
export const ICON_GLYPH_KEYS = [
|
|
37
46
|
"cwd",
|
|
@@ -192,39 +201,65 @@ export function normalizeIconMode(value: unknown): IconMode {
|
|
|
192
201
|
return isIconMode(value) ? value : "auto";
|
|
193
202
|
}
|
|
194
203
|
|
|
195
|
-
export function
|
|
196
|
-
|
|
204
|
+
export function detectAutoIconMode(env: IconEnvironment): EffectiveIconMode {
|
|
205
|
+
if (env.ZENTUI_NERD_FONTS === "1") return "nerd";
|
|
206
|
+
if (env.ZENTUI_NERD_FONTS === "0") return "ascii";
|
|
207
|
+
|
|
208
|
+
const termProgram = env.TERM_PROGRAM?.toLowerCase();
|
|
209
|
+
if (termProgram === "iterm.app" || termProgram === "wezterm" || termProgram === "ghostty") {
|
|
210
|
+
return "nerd";
|
|
211
|
+
}
|
|
212
|
+
if (env.KITTY_WINDOW_ID?.trim() || env.ALACRITTY_SOCKET?.trim()) return "nerd";
|
|
213
|
+
return "ascii";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function resolveEffectiveIconMode(
|
|
217
|
+
mode: IconMode,
|
|
218
|
+
env: IconEnvironment = process.env,
|
|
219
|
+
): EffectiveIconMode {
|
|
220
|
+
return mode === "auto" ? detectAutoIconMode(env) : mode;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function modeDefaultIcons(mode: IconMode, env: IconEnvironment = process.env): IconGlyphs {
|
|
224
|
+
return resolveEffectiveIconMode(mode, env) === "ascii"
|
|
225
|
+
? { ...ASCII_DEFAULT_ICONS }
|
|
226
|
+
: { ...NERD_DEFAULT_ICONS };
|
|
197
227
|
}
|
|
198
228
|
|
|
199
229
|
export function resolveConfiguredIcons(
|
|
200
230
|
mode: IconMode,
|
|
201
231
|
overrides: Partial<IconGlyphs> = {},
|
|
232
|
+
env: IconEnvironment = process.env,
|
|
202
233
|
): ResolvedIcons {
|
|
203
|
-
const
|
|
234
|
+
const effectiveMode = resolveEffectiveIconMode(mode, env);
|
|
235
|
+
const base = modeDefaultIcons(effectiveMode);
|
|
204
236
|
const rail =
|
|
205
237
|
typeof overrides.rail === "string" && overrides.rail.trim().length > 0
|
|
206
238
|
? overrides.rail
|
|
207
239
|
: base.rail;
|
|
208
240
|
return {
|
|
209
241
|
mode,
|
|
242
|
+
effectiveMode,
|
|
210
243
|
...base,
|
|
211
244
|
...overrides,
|
|
212
245
|
rail,
|
|
246
|
+
osOverridden: typeof overrides.os === "string",
|
|
213
247
|
};
|
|
214
248
|
}
|
|
215
249
|
|
|
216
|
-
/**
|
|
217
|
-
* Honor a custom `icons.os` when it differs from the mode default.
|
|
218
|
-
* Otherwise map by platform for the active mode.
|
|
219
|
-
*/
|
|
250
|
+
/** Map the active mode default by platform while preserving explicit `icons.os` overrides. */
|
|
220
251
|
export function resolveOsIcon(
|
|
221
252
|
configuredOsIcon: string,
|
|
222
253
|
mode: IconMode = "auto",
|
|
223
254
|
platform: string = process.platform,
|
|
255
|
+
env: IconEnvironment = process.env,
|
|
256
|
+
osOverridden = false,
|
|
224
257
|
): string {
|
|
225
|
-
|
|
258
|
+
if (osOverridden) return configuredOsIcon;
|
|
259
|
+
const effectiveMode = resolveEffectiveIconMode(mode, env);
|
|
260
|
+
const modeDefault = modeDefaultIcons(effectiveMode).os;
|
|
226
261
|
if (configuredOsIcon !== modeDefault) return configuredOsIcon;
|
|
227
|
-
const platformMap =
|
|
262
|
+
const platformMap = effectiveMode === "ascii" ? OS_PLATFORM_ICONS_ASCII : OS_PLATFORM_ICONS_NERD;
|
|
228
263
|
return platformMap[platform] ?? configuredOsIcon;
|
|
229
264
|
}
|
|
230
265
|
|
|
@@ -232,8 +267,9 @@ export function resolveRuntimeSymbol(
|
|
|
232
267
|
name: string,
|
|
233
268
|
nerdSymbol: string,
|
|
234
269
|
mode: IconMode = "auto",
|
|
270
|
+
env: IconEnvironment = process.env,
|
|
235
271
|
): string {
|
|
236
|
-
if (mode !== "ascii") return nerdSymbol;
|
|
272
|
+
if (resolveEffectiveIconMode(mode, env) !== "ascii") return nerdSymbol;
|
|
237
273
|
return RUNTIME_ASCII_SYMBOLS[name] ?? (name.slice(0, 3) || "*");
|
|
238
274
|
}
|
|
239
275
|
|
|
@@ -243,8 +279,12 @@ export function resolveRuntimeSymbol(
|
|
|
243
279
|
* Honors a configured `icons.package` override; otherwise falls back to the
|
|
244
280
|
* mode default (Nerd Font preset / ASCII label).
|
|
245
281
|
*/
|
|
246
|
-
export function resolvePackageIcon(
|
|
247
|
-
|
|
282
|
+
export function resolvePackageIcon(
|
|
283
|
+
configuredPackageIcon: string,
|
|
284
|
+
mode: IconMode = "auto",
|
|
285
|
+
env: IconEnvironment = process.env,
|
|
286
|
+
): string {
|
|
287
|
+
const modeDefault = modeDefaultIcons(mode, env).package;
|
|
248
288
|
if (typeof configuredPackageIcon === "string" && configuredPackageIcon.length > 0) {
|
|
249
289
|
return configuredPackageIcon;
|
|
250
290
|
}
|
|
@@ -796,6 +796,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
796
796
|
thinkingLevel: getThinkingLevel(),
|
|
797
797
|
contextPercent: getContextPercent(ctx),
|
|
798
798
|
contextWindow: getContextWindow(ctx),
|
|
799
|
+
cacheHitRate: state.usageTotals.latestCacheHitRate,
|
|
799
800
|
sessionName: ctx.sessionManager.getSessionName() ?? "",
|
|
800
801
|
agentDurationMs: getAgentDurationMs(),
|
|
801
802
|
agentActive: agentRunActive,
|
|
@@ -836,6 +837,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
836
837
|
thinkingLevel: getThinkingLevel(),
|
|
837
838
|
contextPercent: getContextPercent(ctx),
|
|
838
839
|
contextWindow: getContextWindow(ctx),
|
|
840
|
+
cacheHitRate: state.usageTotals.latestCacheHitRate,
|
|
839
841
|
sessionName: ctx.sessionManager.getSessionName() ?? "",
|
|
840
842
|
agentDurationMs: getAgentDurationMs(),
|
|
841
843
|
agentActive: agentRunActive,
|
|
@@ -1500,7 +1502,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1500
1502
|
interactionMetrics.agentEnd();
|
|
1501
1503
|
pauseAgentRun();
|
|
1502
1504
|
workingLine.finishAgent(ctx);
|
|
1503
|
-
workingLine.
|
|
1505
|
+
workingLine.flushMetrics(displayTokens, interactionMetrics.currentThought(), ctx);
|
|
1504
1506
|
// Reconcile once more after Pi has persisted the assistant message.
|
|
1505
1507
|
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
1506
1508
|
});
|
|
@@ -1525,7 +1527,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1525
1527
|
thinkingExperimental.endMessage(event);
|
|
1526
1528
|
const result = interactionMetrics.messageEnd(event.message);
|
|
1527
1529
|
if (result.status === "accepted") {
|
|
1528
|
-
workingLine.
|
|
1530
|
+
workingLine.flushMetrics(result.displayTokens, interactionMetrics.currentThought(), ctx);
|
|
1529
1531
|
}
|
|
1530
1532
|
// Pi notifies extensions before persisting a successful message, so retain its live
|
|
1531
1533
|
// context until agent_end; accepted failed messages clear immediately instead of showing
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
bashModeLabel,
|
|
11
11
|
buildContextGauge,
|
|
12
12
|
contextColorTier,
|
|
13
|
+
formatCacheHitRate,
|
|
13
14
|
formatCount,
|
|
14
15
|
formatCwdLabel,
|
|
15
16
|
formatElapsedDuration,
|
|
@@ -53,6 +54,7 @@ export type MinimalistEditorMetadata = {
|
|
|
53
54
|
thinkingLevel?: string;
|
|
54
55
|
contextPercent?: number;
|
|
55
56
|
contextWindow?: number;
|
|
57
|
+
cacheHitRate?: number;
|
|
56
58
|
sessionName?: string;
|
|
57
59
|
agentDurationMs?: number;
|
|
58
60
|
agentActive?: boolean;
|
|
@@ -256,7 +258,11 @@ function renderTopRight(
|
|
|
256
258
|
let context = renderStyleForSource(uiTheme, source, style, text);
|
|
257
259
|
if (config.components.editor.styles.minimalist.contextGauge) {
|
|
258
260
|
for (const gaugeWidth of [5, 3]) {
|
|
259
|
-
const gauge = `[${buildContextGauge(
|
|
261
|
+
const gauge = `[${buildContextGauge(
|
|
262
|
+
percent,
|
|
263
|
+
gaugeWidth,
|
|
264
|
+
config.icons.effectiveMode === "ascii",
|
|
265
|
+
)}] ${text}`;
|
|
260
266
|
const styledGauge = renderStyleForSource(uiTheme, source, style, gauge);
|
|
261
267
|
const candidate = joinParts([...parts, styledGauge]);
|
|
262
268
|
if (visibleWidth(candidate) <= availableWidth) {
|
|
@@ -277,6 +283,15 @@ function renderTopRight(
|
|
|
277
283
|
}
|
|
278
284
|
parts.push(context);
|
|
279
285
|
}
|
|
286
|
+
const cacheHit =
|
|
287
|
+
config.components.editor.styles.minimalist.showCacheHit &&
|
|
288
|
+
metadata.cacheHitRate !== undefined &&
|
|
289
|
+
Number.isFinite(metadata.cacheHitRate)
|
|
290
|
+
? safeThemeFg(uiTheme, "muted", `Cache ${formatCacheHitRate(metadata.cacheHitRate)}`)
|
|
291
|
+
: "";
|
|
292
|
+
if (cacheHit && (!fit || visibleWidth(joinParts([...parts, cacheHit])) <= availableWidth)) {
|
|
293
|
+
parts.push(cacheHit);
|
|
294
|
+
}
|
|
280
295
|
const quota = renderCodexQuota(metadata.codexQuota, uiTheme, config, "editor");
|
|
281
296
|
if (quota && visibleWidth(joinParts([...parts, quota])) <= availableWidth) parts.push(quota);
|
|
282
297
|
const joined = joinParts(parts);
|
|
@@ -570,7 +570,7 @@ function buildAppearanceItems(config: PolishedTuiConfig): SettingItem[] {
|
|
|
570
570
|
id: "iconMode",
|
|
571
571
|
label: "Icon mode",
|
|
572
572
|
description:
|
|
573
|
-
"Auto
|
|
573
|
+
"Auto uses conservative signals: TERM_PROGRAM=iTerm.app|WezTerm|ghostty (case-insensitive), nonempty KITTY_WINDOW_ID or ALACRITTY_SOCKET, and exact ZENTUI_NERD_FONTS=1|0; otherwise ASCII. Environment heuristics cannot detect whether a Nerd Font is installed/configured.",
|
|
574
574
|
currentValue: config.icons.mode,
|
|
575
575
|
values: iconModeValues,
|
|
576
576
|
},
|
|
@@ -711,6 +711,13 @@ function buildMinimalistEditorStyleItems(config: PolishedTuiConfig): SettingItem
|
|
|
711
711
|
currentValue: featureValue(minimalist.showCost),
|
|
712
712
|
values: featureStateValues,
|
|
713
713
|
},
|
|
714
|
+
{
|
|
715
|
+
id: "minimalistShowCacheHit",
|
|
716
|
+
label: "Cache hit rate",
|
|
717
|
+
description: "Show the latest prompt cache hit rate.",
|
|
718
|
+
currentValue: featureValue(minimalist.showCacheHit),
|
|
719
|
+
values: featureStateValues,
|
|
720
|
+
},
|
|
714
721
|
{
|
|
715
722
|
id: "minimalistShowGit",
|
|
716
723
|
label: "Git",
|
|
@@ -1723,9 +1730,11 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
|
|
|
1723
1730
|
? "showTimer"
|
|
1724
1731
|
: id === "minimalistShowCost"
|
|
1725
1732
|
? "showCost"
|
|
1726
|
-
: id === "
|
|
1727
|
-
? "
|
|
1728
|
-
:
|
|
1733
|
+
: id === "minimalistShowCacheHit"
|
|
1734
|
+
? "showCacheHit"
|
|
1735
|
+
: id === "minimalistShowGit"
|
|
1736
|
+
? "showGit"
|
|
1737
|
+
: undefined;
|
|
1729
1738
|
if (!key) return;
|
|
1730
1739
|
deps.setMinimalist({ [key]: enabled }, ctx);
|
|
1731
1740
|
} else return;
|
|
@@ -40,6 +40,8 @@ export const MAX_WORKING_LINE_NORMALIZED_CODE_UNITS = 256;
|
|
|
40
40
|
export const MAX_WORKING_LINE_STYLE_TOKENS = 4;
|
|
41
41
|
export const MAX_WORKING_LINE_STYLE_CODE_UNITS = 48;
|
|
42
42
|
export const MAX_WORKING_LINE_ENTRIES_EXAMINED = 256;
|
|
43
|
+
/** Minimum interval between metric-only Loader replacements while an assistant streams. */
|
|
44
|
+
export const WORKING_LINE_METRIC_UPDATE_INTERVAL_MS = 50;
|
|
43
45
|
|
|
44
46
|
const WORKING_LINE_FALLBACKS: Record<"low" | "mid" | "high", SourceStyleFallback> = {
|
|
45
47
|
low: { theme: "dim", terminal: "bright-black" },
|
|
@@ -67,6 +69,16 @@ type WorkingLineContext = {
|
|
|
67
69
|
|
|
68
70
|
type AgentDurationListener = (durationMs: number) => void;
|
|
69
71
|
|
|
72
|
+
export type WorkingLineMetricScheduler = {
|
|
73
|
+
setTimeout(callback: () => void, delayMs: number): unknown;
|
|
74
|
+
clearTimeout(handle: unknown): void;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const defaultMetricScheduler: WorkingLineMetricScheduler = {
|
|
78
|
+
setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
79
|
+
clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
80
|
+
};
|
|
81
|
+
|
|
70
82
|
/** One agent-duration clock shared by minimalist Editor and Working-line consumers. */
|
|
71
83
|
export class AgentDurationClock {
|
|
72
84
|
private startedAt: number | undefined;
|
|
@@ -1051,6 +1063,11 @@ export class WorkingLineController {
|
|
|
1051
1063
|
private elapsedUpdatesContext: WorkingLineContext | undefined;
|
|
1052
1064
|
private elapsedUpdatesGeneration = 0;
|
|
1053
1065
|
private stopElapsedUpdates: (() => void) | undefined;
|
|
1066
|
+
private lastMetricWriteAt: number | undefined;
|
|
1067
|
+
private metricUpdateScheduled = false;
|
|
1068
|
+
private metricUpdateHandle: unknown;
|
|
1069
|
+
private metricUpdateContext: WorkingLineContext | undefined;
|
|
1070
|
+
private metricUpdateGeneration = 0;
|
|
1054
1071
|
|
|
1055
1072
|
constructor(
|
|
1056
1073
|
private readonly getConfig: () => ZentuiConfig,
|
|
@@ -1060,6 +1077,7 @@ export class WorkingLineController {
|
|
|
1060
1077
|
private readonly now: () => number = Date.now,
|
|
1061
1078
|
private readonly getThought: () => WorkingLineRuntimeSegments["thought"] = () => this.thought,
|
|
1062
1079
|
private readonly onUnavailable: () => void = () => {},
|
|
1080
|
+
private readonly metricScheduler: WorkingLineMetricScheduler = defaultMetricScheduler,
|
|
1063
1081
|
) {}
|
|
1064
1082
|
|
|
1065
1083
|
startSession(ctx: WorkingLineContext): WorkingLineReconcileResult {
|
|
@@ -1082,6 +1100,7 @@ export class WorkingLineController {
|
|
|
1082
1100
|
}
|
|
1083
1101
|
|
|
1084
1102
|
startTurn(ctx: WorkingLineContext): WorkingLineReconcileResult {
|
|
1103
|
+
this.lastMetricWriteAt = undefined;
|
|
1085
1104
|
const config = this.getConfig().components.workingLine;
|
|
1086
1105
|
this.activeTools.clear();
|
|
1087
1106
|
const selectedMessage = selectWorkingLineMessage(config, this.random);
|
|
@@ -1099,12 +1118,15 @@ export class WorkingLineController {
|
|
|
1099
1118
|
): void {
|
|
1100
1119
|
this.tokens = tokens;
|
|
1101
1120
|
this.thought = thought;
|
|
1102
|
-
this.
|
|
1121
|
+
this.scheduleMetricIndicator(ctx);
|
|
1103
1122
|
this.reconcileElapsedUpdates(ctx);
|
|
1104
1123
|
}
|
|
1105
1124
|
|
|
1106
1125
|
updateTokens(tokens: WorkingLineRuntimeSegments["tokens"], ctx: WorkingLineContext): void {
|
|
1107
|
-
this.
|
|
1126
|
+
this.tokens = tokens;
|
|
1127
|
+
this.thought = this.getThought();
|
|
1128
|
+
this.updateIndicator(ctx);
|
|
1129
|
+
this.reconcileElapsedUpdates(ctx);
|
|
1108
1130
|
}
|
|
1109
1131
|
|
|
1110
1132
|
updateExtensionSegments(segments: readonly string[], ctx: WorkingLineContext): boolean {
|
|
@@ -1143,7 +1165,7 @@ export class WorkingLineController {
|
|
|
1143
1165
|
this.updateIndicator(ctx);
|
|
1144
1166
|
}
|
|
1145
1167
|
|
|
1146
|
-
|
|
1168
|
+
flushMetrics(
|
|
1147
1169
|
tokens: WorkingLineRuntimeSegments["tokens"],
|
|
1148
1170
|
thought: WorkingLineRuntimeSegments["thought"],
|
|
1149
1171
|
ctx: WorkingLineContext,
|
|
@@ -1154,6 +1176,14 @@ export class WorkingLineController {
|
|
|
1154
1176
|
this.reconcileElapsedUpdates(ctx);
|
|
1155
1177
|
}
|
|
1156
1178
|
|
|
1179
|
+
settle(
|
|
1180
|
+
tokens: WorkingLineRuntimeSegments["tokens"],
|
|
1181
|
+
thought: WorkingLineRuntimeSegments["thought"],
|
|
1182
|
+
ctx: WorkingLineContext,
|
|
1183
|
+
): void {
|
|
1184
|
+
this.flushMetrics(tokens, thought, ctx);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1157
1187
|
reconcile(ctx: WorkingLineContext): WorkingLineReconcileResult {
|
|
1158
1188
|
const config = this.getConfig().components.workingLine;
|
|
1159
1189
|
if (!config.enabled) {
|
|
@@ -1195,6 +1225,7 @@ export class WorkingLineController {
|
|
|
1195
1225
|
rebasePhase = false,
|
|
1196
1226
|
selectedMessage?: string,
|
|
1197
1227
|
): WorkingLineReconcileResult {
|
|
1228
|
+
this.cancelMetricIndicator();
|
|
1198
1229
|
const ui = workingLineUi(ctx);
|
|
1199
1230
|
if (!ui) {
|
|
1200
1231
|
this.installed = false;
|
|
@@ -1350,6 +1381,7 @@ export class WorkingLineController {
|
|
|
1350
1381
|
}
|
|
1351
1382
|
|
|
1352
1383
|
private updateIndicator(ctx: WorkingLineContext): boolean {
|
|
1384
|
+
this.cancelMetricIndicator();
|
|
1353
1385
|
const rootConfig = this.getConfig();
|
|
1354
1386
|
if (!rootConfig.components.workingLine.enabled || !this.installed) return false;
|
|
1355
1387
|
const ui = workingLineUi(ctx);
|
|
@@ -1365,6 +1397,49 @@ export class WorkingLineController {
|
|
|
1365
1397
|
}
|
|
1366
1398
|
}
|
|
1367
1399
|
|
|
1400
|
+
private scheduleMetricIndicator(ctx: WorkingLineContext): void {
|
|
1401
|
+
const config = this.getConfig().components.workingLine;
|
|
1402
|
+
if (!config.enabled || !this.installed) return;
|
|
1403
|
+
this.metricUpdateContext = ctx;
|
|
1404
|
+
const sampledAt = this.now();
|
|
1405
|
+
const elapsed =
|
|
1406
|
+
this.lastMetricWriteAt === undefined
|
|
1407
|
+
? Number.POSITIVE_INFINITY
|
|
1408
|
+
: Math.max(0, sampledAt - this.lastMetricWriteAt);
|
|
1409
|
+
if (elapsed >= WORKING_LINE_METRIC_UPDATE_INTERVAL_MS) {
|
|
1410
|
+
this.updateIndicator(ctx);
|
|
1411
|
+
this.lastMetricWriteAt = sampledAt;
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
if (this.metricUpdateScheduled) return;
|
|
1415
|
+
this.metricUpdateScheduled = true;
|
|
1416
|
+
const generation = ++this.metricUpdateGeneration;
|
|
1417
|
+
this.metricUpdateHandle = this.metricScheduler.setTimeout(
|
|
1418
|
+
() => {
|
|
1419
|
+
if (!this.metricUpdateScheduled || generation !== this.metricUpdateGeneration) return;
|
|
1420
|
+
this.metricUpdateScheduled = false;
|
|
1421
|
+
this.metricUpdateHandle = undefined;
|
|
1422
|
+
const latest = this.metricUpdateContext;
|
|
1423
|
+
this.metricUpdateContext = undefined;
|
|
1424
|
+
if (latest) {
|
|
1425
|
+
this.updateIndicator(latest);
|
|
1426
|
+
this.lastMetricWriteAt = this.now();
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1429
|
+
Math.max(0, WORKING_LINE_METRIC_UPDATE_INTERVAL_MS - elapsed),
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
private cancelMetricIndicator(): void {
|
|
1434
|
+
this.metricUpdateContext = undefined;
|
|
1435
|
+
this.metricUpdateGeneration++;
|
|
1436
|
+
if (this.metricUpdateScheduled) {
|
|
1437
|
+
this.metricScheduler.clearTimeout(this.metricUpdateHandle);
|
|
1438
|
+
}
|
|
1439
|
+
this.metricUpdateScheduled = false;
|
|
1440
|
+
this.metricUpdateHandle = undefined;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1368
1443
|
private reconcileElapsedUpdates(ctx: WorkingLineContext): void {
|
|
1369
1444
|
this.elapsedUpdatesContext = ctx;
|
|
1370
1445
|
const config = this.getConfig().components.workingLine;
|
|
@@ -1473,6 +1548,7 @@ export class WorkingLineController {
|
|
|
1473
1548
|
}
|
|
1474
1549
|
|
|
1475
1550
|
private reset(ctx: WorkingLineContext): void {
|
|
1551
|
+
this.cancelMetricIndicator();
|
|
1476
1552
|
this.deactivateElapsedUpdates();
|
|
1477
1553
|
if (!this.ownsIndicator && !this.ownsMessage) return;
|
|
1478
1554
|
const ui = workingLineUi(ctx);
|
|
@@ -1499,6 +1575,8 @@ export class WorkingLineController {
|
|
|
1499
1575
|
}
|
|
1500
1576
|
|
|
1501
1577
|
private clearRuntime(): void {
|
|
1578
|
+
this.cancelMetricIndicator();
|
|
1579
|
+
this.lastMetricWriteAt = undefined;
|
|
1502
1580
|
this.deactivateElapsedUpdates();
|
|
1503
1581
|
this.agentActive = false;
|
|
1504
1582
|
this.activeTools.clear();
|