pi-zentui 0.21.0 → 0.22.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 +17 -3
- package/extensions/zentui/config.ts +35 -3
- package/extensions/zentui/editor-metadata-format.ts +58 -2
- package/extensions/zentui/footer.ts +12 -22
- package/extensions/zentui/format.ts +56 -12
- package/extensions/zentui/index.ts +81 -50
- package/extensions/zentui/prototype-patch-registry.ts +55 -11
- package/extensions/zentui/repository-root.ts +98 -0
- package/extensions/zentui/settings-command.ts +123 -5
- package/extensions/zentui/settings-previews.ts +102 -2
- package/extensions/zentui/state.ts +11 -0
- package/extensions/zentui/thinking-experimental.ts +1557 -0
- package/extensions/zentui/thinking-steps.ts +156 -0
- package/extensions/zentui/ui.ts +69 -12
- package/extensions/zentui/user-message-styles.ts +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -14,14 +14,15 @@
|
|
|
14
14
|
|
|
15
15
|
## What is this?
|
|
16
16
|
|
|
17
|
-
Zentui gives
|
|
17
|
+
Zentui gives Pi surfaces independent, opt-in treatments:
|
|
18
18
|
|
|
19
19
|
- **Editor** — Opencode, Opencode copy-friendly, Accent Rail, and Minimalist input treatments
|
|
20
20
|
- **User messages** — framed, framed copy-friendly, compact, and labeled transcript messages
|
|
21
|
+
- **Thinking (Experimental)** — optional Rail, Tree, or Streaming private thinking renderers, without owning the Working line
|
|
21
22
|
- **Working line** — optional ownership of Pi's complete in-progress row and settled turn summary
|
|
22
23
|
- **Footer** — Pi's native Footer, a Starship-style statusline, or a hidden zero-row Footer
|
|
23
24
|
|
|
24
|
-
Editor, User messages, Working line, and selector borders have independent `enabled` fields. Footer uses one `style`: `native`, `starship`, or `hidden`. Use `/zentui` to configure each component without coupling it to the others.
|
|
25
|
+
Editor, User messages, Thinking (Experimental), Working line, and selector borders have independent `enabled` fields. Footer uses one `style`: `native`, `starship`, or `hidden`. Use `/zentui` to configure each component without coupling it to the others.
|
|
25
26
|
|
|
26
27
|
## Highlights
|
|
27
28
|
|
|
@@ -29,6 +30,7 @@ Editor, User messages, Working line, and selector borders have independent `enab
|
|
|
29
30
|
| --- | --- | --- |
|
|
30
31
|
| Editor | `opencode` | Opencode, copy-friendly, Accent Rail, Minimalist |
|
|
31
32
|
| User messages | `framed` | Framed, copy-friendly, Compact, Labeled |
|
|
33
|
+
| Thinking (Experimental) | disabled (`tree`) | Rail, Tree, Streaming |
|
|
32
34
|
| Working line | disabled | Five spinner presets, live tool/time/thinking/token segments, turn summary |
|
|
33
35
|
| Footer | `starship` | Native, Starship, Hidden |
|
|
34
36
|
| Selector borders | `zentui` | Independent enablement and color source |
|
|
@@ -87,7 +89,7 @@ pi install git:github.com/lmilojevicc/pi-zentui
|
|
|
87
89
|
|
|
88
90
|
## Configure
|
|
89
91
|
|
|
90
|
-
Run `/zentui` inside Pi to configure Appearance, Editor, User messages, Working line, Footer, Segments, Git, and Extensions. Use `Tab` and `Shift+Tab` to switch sections.
|
|
92
|
+
Run `/zentui` inside Pi to configure Appearance, Editor, User messages, Thinking (Experimental), Working line, Footer, Segments, Git, and Extensions. Use `Tab` and `Shift+Tab` to switch sections. Most changes apply live; Thinking (Experimental) changes are saved and require restarting Pi. Configuration is saved to:
|
|
91
93
|
|
|
92
94
|
```text
|
|
93
95
|
~/.pi/agent/zentui.json
|
|
@@ -106,6 +108,10 @@ A small starter config:
|
|
|
106
108
|
"enabled": true,
|
|
107
109
|
"style": "framed"
|
|
108
110
|
},
|
|
111
|
+
"thinkingSteps": {
|
|
112
|
+
"enabled": false,
|
|
113
|
+
"mode": "tree"
|
|
114
|
+
},
|
|
109
115
|
"workingLine": {
|
|
110
116
|
"enabled": false
|
|
111
117
|
},
|
|
@@ -124,6 +130,8 @@ Detailed reference:
|
|
|
124
130
|
- [Configuration, component styles, defaults, runtime detection, and compatibility](https://github.com/lmilojevicc/pi-zentui/blob/main/docs/configuration.md)
|
|
125
131
|
- [Footer format template and variables](https://github.com/lmilojevicc/pi-zentui/blob/main/docs/footer-format.md)
|
|
126
132
|
|
|
133
|
+
The Starship Footer path defaults to `basename`. Opt into `components.footer.styles.starship.pathDisplay.mode: "repository"` to omit the repository directory itself: the repository root renders `.`, while `/repo/extensions/zentui` renders `extensions/zentui`. `depth` keeps the final N components in `full` and `repository` modes; `0` is unlimited. Until a current, safely contained repository root is available, repository mode silently uses the unlimited `full` path with `~` home abbreviation.
|
|
134
|
+
|
|
127
135
|
Useful shortcuts:
|
|
128
136
|
|
|
129
137
|
```text
|
|
@@ -136,6 +144,12 @@ Useful shortcuts:
|
|
|
136
144
|
/zentui format clear
|
|
137
145
|
```
|
|
138
146
|
|
|
147
|
+
**Thinking (Experimental)** uses one restart-gated private `AssistantMessageComponent` renderer for Rail, Tree, and Streaming, tested against exact Pi versions 0.80.5, 0.83.0, 0.84.0, and 0.84.4. It is disabled by default and may break after Pi updates. Every enable, disable, or mode change is saved but does not alter the active startup snapshot; restart Pi to apply it. Zentui installs an enabled startup snapshot before transcript restoration. Missing constructors, incompatible child layouts, parser limits, theme/render/width errors, or displaced patch ownership fail open to complete native thinking rather than switching modes.
|
|
148
|
+
|
|
149
|
+
Rail shows every parsed label in each native contiguous thinking run (`│ Label`, with only the open final phase shown as `│ • Label`). Tree independently shows the latest five labels in each run (`├─ · Label`, settled `└─ · Label`, open `└─ • Label`); it never aggregates across intervening text or tool blocks. Labels are rendered by fresh host-shaped Pi Markdown instances before cropping, so emphasis, code, links, HTML, LaTeX, custom transforms, and native `thinkingText` styling remain host-controlled. Every label occupies one terminal row: ANSI/OSC/grapheme-aware cropping adds `…` only when needed. Native horizontal padding stays external. Connectors are styled directly with the current theme's `accent` callback on every render, so custom themes control them independently. Hidden native thinking remains hidden and keeps Pi's native hidden label.
|
|
150
|
+
|
|
151
|
+
Streaming retains Pi's host-rendered final five rows under `Thinking 7.1s`, folds completed reasoning under `Thought` or current-session `Thought for 12.3s`, and owns the configured thinking-toggle binding (Ctrl+T by default) only for an active Streaming startup. Restored completions cannot recover a duration because Pi does not persist the thinking-end timestamp. Expand/refold and lifecycle tracking are bounded to 256 retained assistant components; evicted entries are first restored natively. All modes restore/dispose on shutdown. Thinking (Experimental) never writes the Working line and does not change its existing **Thinking time** option, working text, Footer, Editor, statuses, or model behavior.
|
|
152
|
+
|
|
139
153
|
Pi 0.84 also provides a native fullscreen TUI with a sticky editor and Footer. Zentui does not enable it automatically; select fullscreen from Pi's `/settings`, set `"tuiMode": "fullscreen"` in Pi settings, or launch Pi with `--tui-mode fullscreen`.
|
|
140
154
|
|
|
141
155
|
## Requirements
|
|
@@ -46,6 +46,7 @@ export type WorkingLineSpinner =
|
|
|
46
46
|
| "claude-inspired"
|
|
47
47
|
| "pulse";
|
|
48
48
|
export type WorkingLineTextAnimation = "classic" | "kitt" | "disabled";
|
|
49
|
+
export type ThinkingStepsMode = "rail" | "tree" | "streaming";
|
|
49
50
|
export type ComponentStyleOwner = "editor" | "userMessages" | "selectorBorders" | "footer";
|
|
50
51
|
export type MinimalistPathDisplayMode = "compact" | "project" | "full";
|
|
51
52
|
export type MinimalistContextFormat = "percent" | "percent-total";
|
|
@@ -61,11 +62,11 @@ export type ContextThresholds = {
|
|
|
61
62
|
error: number;
|
|
62
63
|
};
|
|
63
64
|
|
|
64
|
-
export type PathDisplayMode = "basename" | "full";
|
|
65
|
+
export type PathDisplayMode = "basename" | "full" | "repository";
|
|
65
66
|
|
|
66
67
|
export type PathDisplayConfig = {
|
|
67
68
|
mode: PathDisplayMode;
|
|
68
|
-
/**
|
|
69
|
+
/** Final components to show in full/repository mode. 0 = unlimited; clamped to 0..5. */
|
|
69
70
|
depth: number;
|
|
70
71
|
};
|
|
71
72
|
|
|
@@ -250,9 +251,15 @@ export type WorkingLineComponentPatch = Partial<
|
|
|
250
251
|
segments?: Partial<WorkingLineSegmentsConfig>;
|
|
251
252
|
};
|
|
252
253
|
|
|
254
|
+
export type ThinkingStepsComponentConfig = {
|
|
255
|
+
enabled: boolean;
|
|
256
|
+
mode: ThinkingStepsMode;
|
|
257
|
+
};
|
|
258
|
+
|
|
253
259
|
export type ComponentsConfig = {
|
|
254
260
|
editor: EditorComponentConfig;
|
|
255
261
|
userMessages: UserMessagesComponentConfig;
|
|
262
|
+
thinkingSteps: ThinkingStepsComponentConfig;
|
|
256
263
|
workingLine: WorkingLineComponentConfig;
|
|
257
264
|
selectorBorders: SelectorBordersComponentConfig;
|
|
258
265
|
footer: FooterComponentConfig;
|
|
@@ -500,6 +507,7 @@ const defaultComponents: ComponentsConfig = {
|
|
|
500
507
|
colorSource: "theme",
|
|
501
508
|
styles: { framed: {}, "framed-copy-friendly": {}, compact: {}, labeled: {} },
|
|
502
509
|
},
|
|
510
|
+
thinkingSteps: { enabled: false, mode: "tree" },
|
|
503
511
|
workingLine: {
|
|
504
512
|
enabled: false,
|
|
505
513
|
turnSummary: true,
|
|
@@ -665,7 +673,10 @@ function parseContextThresholds(
|
|
|
665
673
|
function parsePathDisplay(value: unknown): PathDisplayConfig {
|
|
666
674
|
const defaults = defaultConfig.pathDisplay;
|
|
667
675
|
if (!isRecord(value)) return { ...defaults };
|
|
668
|
-
const mode =
|
|
676
|
+
const mode =
|
|
677
|
+
value.mode === "full" || value.mode === "basename" || value.mode === "repository"
|
|
678
|
+
? value.mode
|
|
679
|
+
: defaults.mode;
|
|
669
680
|
const rawDepth = value.depth;
|
|
670
681
|
const depth =
|
|
671
682
|
typeof rawDepth === "number" && Number.isFinite(rawDepth) && rawDepth >= 0
|
|
@@ -1188,6 +1199,7 @@ function resolveComponents(config: ConfigRecord): ComponentsConfig {
|
|
|
1188
1199
|
const userMessages = recordValue(components.userMessages);
|
|
1189
1200
|
const userMessageStyles = recordValue(userMessages.styles);
|
|
1190
1201
|
const framed = recordValue(userMessageStyles.framed);
|
|
1202
|
+
const thinkingSteps = recordValue(components.thinkingSteps);
|
|
1191
1203
|
const workingLine = recordValue(components.workingLine);
|
|
1192
1204
|
const workingLineMessages = recordValue(workingLine.messages);
|
|
1193
1205
|
const workingLineSegments = recordValue(workingLine.segments);
|
|
@@ -1295,6 +1307,15 @@ function resolveComponents(config: ConfigRecord): ComponentsConfig {
|
|
|
1295
1307
|
labeled: {},
|
|
1296
1308
|
},
|
|
1297
1309
|
},
|
|
1310
|
+
thinkingSteps: {
|
|
1311
|
+
enabled: parseBoolean(thinkingSteps.enabled, defaultComponents.thinkingSteps.enabled),
|
|
1312
|
+
mode:
|
|
1313
|
+
thinkingSteps.mode === "rail" || thinkingSteps.mode === "tree"
|
|
1314
|
+
? thinkingSteps.mode
|
|
1315
|
+
: thinkingSteps.mode === "streaming" || thinkingSteps.mode === "streaming-experimental"
|
|
1316
|
+
? "streaming"
|
|
1317
|
+
: defaultComponents.thinkingSteps.mode,
|
|
1318
|
+
},
|
|
1298
1319
|
workingLine: {
|
|
1299
1320
|
enabled: parseBoolean(workingLine.enabled, defaultComponents.workingLine.enabled),
|
|
1300
1321
|
turnSummary: parseBoolean(workingLine.turnSummary, defaultComponents.workingLine.turnSummary),
|
|
@@ -1734,6 +1755,17 @@ export function saveUserMessagesComponentPatch(
|
|
|
1734
1755
|
);
|
|
1735
1756
|
}
|
|
1736
1757
|
|
|
1758
|
+
export function saveThinkingStepsComponentPatch(
|
|
1759
|
+
patch: Partial<ThinkingStepsComponentConfig>,
|
|
1760
|
+
path = configPath,
|
|
1761
|
+
): PolishedTuiConfig {
|
|
1762
|
+
return saveComponentsMutation((components) => {
|
|
1763
|
+
const component = components.thinkingSteps;
|
|
1764
|
+
if (patch.enabled !== undefined) component.enabled = patch.enabled;
|
|
1765
|
+
if (patch.mode !== undefined) component.mode = patch.mode;
|
|
1766
|
+
}, path);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1737
1769
|
export function saveWorkingLineComponentPatch(
|
|
1738
1770
|
patch: WorkingLineComponentPatch,
|
|
1739
1771
|
path = configPath,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { ZentuiConfig } from "./config";
|
|
3
3
|
import { type FormatToken, parseFooterFormat } from "./footer-format";
|
|
4
|
+
import { buildSessionTokenLabel, formatCacheHitRate, formatContextPercentLabel } from "./format";
|
|
4
5
|
import { EDITOR_ACCENT_FALLBACK, renderStyleForSourceOrFallback, safeThemeFg } from "./style";
|
|
5
6
|
|
|
6
7
|
export type EditorMetadataValues = {
|
|
@@ -10,6 +11,11 @@ export type EditorMetadataValues = {
|
|
|
10
11
|
provider: string;
|
|
11
12
|
thinking: string;
|
|
12
13
|
sessionName: string;
|
|
14
|
+
contextPercent?: number;
|
|
15
|
+
contextWindow?: number;
|
|
16
|
+
inputTokens?: number;
|
|
17
|
+
outputTokens?: number;
|
|
18
|
+
cacheHitRate?: number;
|
|
13
19
|
};
|
|
14
20
|
|
|
15
21
|
type RenderedTokens = {
|
|
@@ -18,6 +24,12 @@ type RenderedTokens = {
|
|
|
18
24
|
hasNonEmptyDynamic: boolean;
|
|
19
25
|
};
|
|
20
26
|
|
|
27
|
+
export type EditorMetadataZones = {
|
|
28
|
+
left: string;
|
|
29
|
+
middle: string;
|
|
30
|
+
right: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
21
33
|
const ESC = 0x1b;
|
|
22
34
|
const BEL = 0x07;
|
|
23
35
|
const CAN = 0x18;
|
|
@@ -164,7 +176,16 @@ function renderVariable(
|
|
|
164
176
|
? thinking
|
|
165
177
|
: name === "session_name"
|
|
166
178
|
? values.sessionName
|
|
167
|
-
: ""
|
|
179
|
+
: name === "context"
|
|
180
|
+
? formatContextPercentLabel(values.contextPercent, values.contextWindow)
|
|
181
|
+
: name === "tokens"
|
|
182
|
+
? buildSessionTokenLabel({
|
|
183
|
+
input: values.inputTokens ?? 0,
|
|
184
|
+
output: values.outputTokens ?? 0,
|
|
185
|
+
})
|
|
186
|
+
: name === "cache_hit"
|
|
187
|
+
? formatCacheHitRate(values.cacheHitRate)
|
|
188
|
+
: "";
|
|
168
189
|
const plain = sanitizeEditorMetadataText(raw);
|
|
169
190
|
if (!plain) return { plain: "", styled: "" };
|
|
170
191
|
|
|
@@ -204,7 +225,7 @@ function renderVariable(
|
|
|
204
225
|
),
|
|
205
226
|
};
|
|
206
227
|
}
|
|
207
|
-
if (name === "session_name") {
|
|
228
|
+
if (name === "session_name" || name === "context" || name === "tokens" || name === "cache_hit") {
|
|
208
229
|
return { plain, styled: safeThemeFg(uiTheme, "border", plain) };
|
|
209
230
|
}
|
|
210
231
|
return { plain: "", styled: "" };
|
|
@@ -250,6 +271,41 @@ function renderTokens(
|
|
|
250
271
|
return { styled, hasDynamic, hasNonEmptyDynamic };
|
|
251
272
|
}
|
|
252
273
|
|
|
274
|
+
export function renderEditorMetadataFormatSplit(
|
|
275
|
+
format: string,
|
|
276
|
+
values: EditorMetadataValues,
|
|
277
|
+
uiTheme: Theme,
|
|
278
|
+
config: ZentuiConfig,
|
|
279
|
+
): EditorMetadataZones {
|
|
280
|
+
const tokens = parseFooterFormat(sanitizeEditorMetadataText(format));
|
|
281
|
+
const fillIndices: number[] = [];
|
|
282
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
283
|
+
if (tokens[index]?.kind === "fill") fillIndices.push(index);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const first = fillIndices[0];
|
|
287
|
+
const second = fillIndices[1];
|
|
288
|
+
if (first === undefined) {
|
|
289
|
+
return {
|
|
290
|
+
left: renderTokens(tokens, values, uiTheme, config).styled,
|
|
291
|
+
middle: "",
|
|
292
|
+
right: "",
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (second === undefined) {
|
|
296
|
+
return {
|
|
297
|
+
left: renderTokens(tokens.slice(0, first), values, uiTheme, config).styled,
|
|
298
|
+
middle: "",
|
|
299
|
+
right: renderTokens(tokens.slice(first + 1), values, uiTheme, config).styled,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
left: renderTokens(tokens.slice(0, first), values, uiTheme, config).styled,
|
|
304
|
+
middle: renderTokens(tokens.slice(first + 1, second), values, uiTheme, config).styled,
|
|
305
|
+
right: renderTokens(tokens.slice(second + 1), values, uiTheme, config).styled,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
253
309
|
export function renderEditorMetadataFormat(
|
|
254
310
|
format: string,
|
|
255
311
|
values: EditorMetadataValues,
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
formatRuntimeSegment,
|
|
36
36
|
formatTimeLabel,
|
|
37
37
|
formatUsernameHostLabel,
|
|
38
|
+
resolveContextUsage,
|
|
38
39
|
} from "./format";
|
|
39
40
|
import { resolveRuntimeSymbol } from "./icons";
|
|
40
41
|
import type { LiveContextOverride } from "./live-context";
|
|
@@ -187,6 +188,7 @@ export function installFooter(
|
|
|
187
188
|
scheduleProjectRefresh: (ctx: ExtensionContext) => void;
|
|
188
189
|
setExtensionStatusesGetter?: (fn: (() => ReadonlyMap<string, string>) | undefined) => void;
|
|
189
190
|
getLiveContext?: () => LiveContextOverride | undefined;
|
|
191
|
+
getRepositoryRoot?: (cwd: string) => string | undefined;
|
|
190
192
|
onDispose?: () => void;
|
|
191
193
|
},
|
|
192
194
|
): void {
|
|
@@ -227,10 +229,13 @@ export function installFooter(
|
|
|
227
229
|
);
|
|
228
230
|
const colorSource = config.components.footer.colorSource;
|
|
229
231
|
const iconMode = config.icons.mode;
|
|
232
|
+
const pathDisplay = config.components.footer.styles.starship.pathDisplay;
|
|
230
233
|
const formattedCwd = sanitizeEditorMetadataText(
|
|
231
234
|
formatCwdLabel(ctx.cwd, config.icons.cwd, {
|
|
232
|
-
mode:
|
|
233
|
-
depth:
|
|
235
|
+
mode: pathDisplay.mode,
|
|
236
|
+
depth: pathDisplay.depth,
|
|
237
|
+
repositoryRoot:
|
|
238
|
+
pathDisplay.mode === "repository" ? hooks.getRepositoryRoot?.(ctx.cwd) : undefined,
|
|
234
239
|
}),
|
|
235
240
|
);
|
|
236
241
|
const branch = sanitizeEditorMetadataText(state.branch ?? "") || undefined;
|
|
@@ -282,14 +287,10 @@ export function installFooter(
|
|
|
282
287
|
config.components.footer.styles.starship.gitBranch.maxLength,
|
|
283
288
|
)
|
|
284
289
|
: undefined;
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
liveContext !== undefined && contextWindow !== undefined && contextWindow > 0;
|
|
290
|
-
const contextPercent = useLiveContext
|
|
291
|
-
? (liveContext.tokens / contextWindow) * 100
|
|
292
|
-
: contextUsage?.percent;
|
|
290
|
+
const { percent: contextPercent, contextWindow } = resolveContextUsage(
|
|
291
|
+
ctx,
|
|
292
|
+
hooks.getLiveContext?.(),
|
|
293
|
+
);
|
|
293
294
|
const contextLabel = buildContextDisplayLabel({
|
|
294
295
|
percent: contextPercent,
|
|
295
296
|
contextWindow,
|
|
@@ -731,18 +732,7 @@ export function installFooter(
|
|
|
731
732
|
if (reflowed) return frameRows(reflowed);
|
|
732
733
|
|
|
733
734
|
const chunkBudget = compactChunkBudget(innerWidth);
|
|
734
|
-
const compactCwdLabel = truncateToWidth(
|
|
735
|
-
renderStyleForSource(
|
|
736
|
-
theme,
|
|
737
|
-
colorSource,
|
|
738
|
-
config.colors.cwd,
|
|
739
|
-
sanitizeEditorMetadataText(
|
|
740
|
-
formatCwdLabel(ctx.cwd, config.icons.cwd, { mode: "basename", depth: 0 }),
|
|
741
|
-
),
|
|
742
|
-
),
|
|
743
|
-
chunkBudget,
|
|
744
|
-
"…",
|
|
745
|
-
);
|
|
735
|
+
const compactCwdLabel = truncateToWidth(cwdLabel, chunkBudget, "…");
|
|
746
736
|
const compactSessionNameLabel = truncateToWidth(
|
|
747
737
|
sessionNameLabel,
|
|
748
738
|
Math.max(1, chunkBudget - visibleWidth("in ")),
|
|
@@ -294,17 +294,24 @@ export function buildCacheWriteLabel(cacheWrite: number): string {
|
|
|
294
294
|
return cacheWrite > 0 ? `W${formatCount(cacheWrite)}` : "";
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
export function
|
|
297
|
+
export function buildSessionTokenLabel(totals: Pick<UsageTotals, "input" | "output">): string {
|
|
298
298
|
const parts: string[] = [];
|
|
299
299
|
if (totals.input) parts.push(`↑${formatCount(totals.input)}`);
|
|
300
300
|
if (totals.output) parts.push(`↓${formatCount(totals.output)}`);
|
|
301
|
+
return parts.length > 0 ? parts.join(" ") : "↑0 ↓0";
|
|
302
|
+
}
|
|
301
303
|
|
|
304
|
+
export function formatCacheHitRate(rate: number | undefined): string {
|
|
305
|
+
return `${rate !== undefined && Number.isFinite(rate) ? rate.toFixed(1) : "0.0"}%`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function buildTokenLabel(totals: UsageTotals, cacheHitIcon = ""): string {
|
|
309
|
+
const tokenLabel = buildSessionTokenLabel(totals);
|
|
302
310
|
const hasCacheTokens = totals.cacheRead > 0 || totals.cacheWrite > 0;
|
|
303
|
-
if (hasCacheTokens
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
return parts.length > 0 ? parts.join(" ") : "↑0 ↓0";
|
|
311
|
+
if (!hasCacheTokens || totals.latestCacheHitRate === undefined) return tokenLabel;
|
|
312
|
+
|
|
313
|
+
const cacheHitRate = formatCacheHitRate(totals.latestCacheHitRate);
|
|
314
|
+
return `${tokenLabel} ${cacheHitIcon ? `${cacheHitIcon} ` : ""}${cacheHitRate}`;
|
|
308
315
|
}
|
|
309
316
|
|
|
310
317
|
export function buildCostLabel(totals: UsageTotals): string {
|
|
@@ -372,10 +379,29 @@ export function buildContextDisplayLabel(options: {
|
|
|
372
379
|
return text;
|
|
373
380
|
}
|
|
374
381
|
|
|
375
|
-
export
|
|
382
|
+
export type ContextUsageSnapshot = {
|
|
383
|
+
percent: number | undefined;
|
|
384
|
+
contextWindow: number | undefined;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
export function resolveContextUsage(
|
|
388
|
+
ctx: Pick<ExtensionContext, "model" | "getContextUsage">,
|
|
389
|
+
live?: { tokens: number },
|
|
390
|
+
): ContextUsageSnapshot {
|
|
376
391
|
const usage = ctx.getContextUsage();
|
|
377
392
|
const contextWindow = ctx.model?.contextWindow ?? usage?.contextWindow;
|
|
378
|
-
return
|
|
393
|
+
return {
|
|
394
|
+
percent:
|
|
395
|
+
live && contextWindow && contextWindow > 0
|
|
396
|
+
? (live.tokens / contextWindow) * 100
|
|
397
|
+
: (usage?.percent ?? undefined),
|
|
398
|
+
contextWindow,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function buildContextLabel(ctx: ExtensionContext): string {
|
|
403
|
+
const { percent, contextWindow } = resolveContextUsage(ctx);
|
|
404
|
+
return formatContextPercentLabel(percent, contextWindow);
|
|
379
405
|
}
|
|
380
406
|
|
|
381
407
|
export function formatRuntimeSegment(
|
|
@@ -417,9 +443,10 @@ export function formatPackageVersionSegment(
|
|
|
417
443
|
|
|
418
444
|
export type FormatCwdOptions = {
|
|
419
445
|
mode?: PathDisplayMode;
|
|
420
|
-
/**
|
|
446
|
+
/** Final path components to keep in full/repository mode. 0 = unlimited. */
|
|
421
447
|
depth?: number;
|
|
422
448
|
home?: string;
|
|
449
|
+
repositoryRoot?: string;
|
|
423
450
|
};
|
|
424
451
|
|
|
425
452
|
function normalizeDisplayPath(cwd: string): string {
|
|
@@ -462,8 +489,7 @@ function applyPathDepth(path: string, depth: number): string {
|
|
|
462
489
|
export function formatCwdLabel(cwd: string, cwdIcon: string, options?: FormatCwdOptions): string {
|
|
463
490
|
const mode = options?.mode ?? "basename";
|
|
464
491
|
const normalized = normalizeDisplayPath(cwd);
|
|
465
|
-
|
|
466
|
-
if (mode === "full") {
|
|
492
|
+
const fullPath = (depth = options?.depth ?? 0) => {
|
|
467
493
|
const home =
|
|
468
494
|
options?.home ??
|
|
469
495
|
(() => {
|
|
@@ -473,7 +499,25 @@ export function formatCwdLabel(cwd: string, cwdIcon: string, options?: FormatCwd
|
|
|
473
499
|
return "";
|
|
474
500
|
}
|
|
475
501
|
})();
|
|
476
|
-
|
|
502
|
+
return applyPathDepth(toHomePath(normalized, home), depth);
|
|
503
|
+
};
|
|
504
|
+
let pathText: string;
|
|
505
|
+
if (mode === "full") {
|
|
506
|
+
pathText = fullPath();
|
|
507
|
+
} else if (mode === "repository") {
|
|
508
|
+
const root = options?.repositoryRoot ? normalizeDisplayPath(options.repositoryRoot) : undefined;
|
|
509
|
+
if (!root) {
|
|
510
|
+
pathText = fullPath(0);
|
|
511
|
+
} else {
|
|
512
|
+
const rootPrefix = root === "/" ? "/" : `${root}/`;
|
|
513
|
+
if (normalized !== root && !normalized.startsWith(rootPrefix)) {
|
|
514
|
+
// Unsafe roots fail open to the existing unlimited full path.
|
|
515
|
+
pathText = fullPath(0);
|
|
516
|
+
} else {
|
|
517
|
+
const relative = normalized === root ? "." : normalized.slice(rootPrefix.length);
|
|
518
|
+
pathText = relative === "." ? relative : applyPathDepth(relative, options?.depth ?? 0);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
477
521
|
} else if (normalized === "/") {
|
|
478
522
|
pathText = "/";
|
|
479
523
|
} else {
|