pi-zentui 0.10.1 → 0.12.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 +9 -5
- package/extensions/zentui/config.ts +8 -0
- package/extensions/zentui/footer.ts +28 -3
- package/extensions/zentui/index.ts +44 -6
- package/extensions/zentui/live-context.ts +75 -0
- package/extensions/zentui/session-lifecycle.ts +11 -4
- package/extensions/zentui/settings-command.ts +3 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Zentui brings two popular aesthetics to Pi:
|
|
|
21
21
|
- `on branch` — git branch with icon
|
|
22
22
|
- `[!?↑]` — git status indicators (modified, untracked, ahead/behind, stashed, etc.)
|
|
23
23
|
- `via v5.5.0` — runtime detection with version and Starship-style Nerd Font runtime/language modules
|
|
24
|
-
- Optional segments (off by default): `user@host`, current time, OS icon, session duration, and the **project package version** (e.g. `package.json` → `0.6.0`) — distinct from the runtime segment, which shows the installed toolchain
|
|
24
|
+
- Optional segments (off by default): session name, `user@host`, current time, OS icon, session duration, and the **project package version** (e.g. `package.json` → `0.6.0`) — distinct from the runtime segment, which shows the installed toolchain
|
|
25
25
|
- Right side shows context usage, token counts, and cost
|
|
26
26
|
- Built-in footer segments can be shown or hidden individually from `/zentui`
|
|
27
27
|
- Fully custom Starship-style layout via a `footerFormat` template string — see [Footer Format Template](#footer-format-template)
|
|
@@ -194,6 +194,7 @@ Default config values — copy this and change any value you want:
|
|
|
194
194
|
},
|
|
195
195
|
"colors": {
|
|
196
196
|
"cwd": "bold cyan",
|
|
197
|
+
"sessionName": "bold green",
|
|
197
198
|
"gitBranch": "bold purple",
|
|
198
199
|
"gitStatus": "bold red",
|
|
199
200
|
"contextNormal": "bright-black",
|
|
@@ -236,6 +237,7 @@ Default config values — copy this and change any value you want:
|
|
|
236
237
|
},
|
|
237
238
|
"footerSegments": {
|
|
238
239
|
"cwd": true,
|
|
240
|
+
"sessionName": true,
|
|
239
241
|
"gitBranch": true,
|
|
240
242
|
"gitStatus": true,
|
|
241
243
|
"gitCounts": false,
|
|
@@ -275,7 +277,7 @@ Default config values — copy this and change any value you want:
|
|
|
275
277
|
|
|
276
278
|
- Style values can be Starship/terminal strings (`bold purple`, `fg:202`, `#89b` / `#89b4fa`, `bg:blue fg:bright-green`) or Pi theme tokens (`accent`, `borderMuted`, `thinkingHigh`). Short `#rgb` hex values expand to `#rrggbb`.
|
|
277
279
|
- `projectRefreshIntervalMs`: project status polling interval; `0` disables polling. Values `1..4999` clamp up to `5000` (minimum 5s); invalid/non-finite values fall back to `30000`.
|
|
278
|
-
- `contextStyle`: `text` (default), `gauge`, or `text+gauge` for the context segment.
|
|
280
|
+
- `contextStyle`: `text` (default), `gauge`, or `text+gauge` for the context segment. Context usage refreshes during assistant streaming; token and cost totals remain canonical and finalize at turn boundaries.
|
|
279
281
|
- `separator`: controls the default footer layout and extension-status connectors: `pipe` (default, ` | `), `dot` (` · `), `chevron` (` › `), or `none` (one space). Cycle it from the `/zentui` **Layout** tab. This selects the separator glyph; `colors.separator` controls its color. Custom `footerFormat` literals and `$sep` keep their existing behavior.
|
|
280
282
|
- `contextThresholds`: `{ warning, error }` percentages (default `70` / `90`) that select contextNormal / contextWarning / contextError colors.
|
|
281
283
|
- `pathDisplay`: controls how the cwd/`$cwd` path is shown. `mode` is `basename` (default, last segment only) or `full` (path with home contracted to `~`). In `full` mode, `depth` keeps only the last N trailing directories (`0` = entire path after `~`, max `5`); when parents are dropped the path is prefixed with `…/` (Starship-style). The `/zentui` **Layout** tab cycles path mode and path depth (`0`–`5`; depth is ignored for basename). Example: `~/Projects/foo/bar` with `depth: 2` → `…/foo/bar`.
|
|
@@ -283,7 +285,7 @@ Default config values — copy this and change any value you want:
|
|
|
283
285
|
- `icons`: every shown icon key is configurable; omit any key to use the Zentui default. `icons.mode` is `auto` | `nerd` | `ascii` (default `auto`, same glyphs as nerd). ASCII mode swaps in plain fallbacks for statusline icons and runtime symbols — useful without a Nerd Font. Custom per-icon strings always win over mode defaults. Custom `icons.os` always wins; when left at the mode default, Zentui maps the OS icon by platform. `rail` sets the vertical glyph drawn as the left rail of the active editor frame and previous user messages when `copyFriendly` is disabled (default `│`; any single Unicode vertical or block glyph). `editorPrompt` controls an optional copy-friendly editor prompt glyph; the default is `""` so copy-friendly mode stays rail-free.
|
|
284
286
|
- `colorSources`: `theme` maps styles through Pi theme tokens; `terminal` emits terminal colors. `/zentui` switches these sources; manual JSON controls specific style values.
|
|
285
287
|
- `features`: `editor` enables Zentui's custom editor, selector borders, and previous-message chrome. `statusLine` enables Zentui's custom footer/status line. `copyFriendly` hides editor and previous-message rail glyphs so native terminal selection copies less chrome. All three can be changed from `/zentui` or direct slash-command arguments.
|
|
286
|
-
- `footerSegments`: show or hide individual built-in footer segments (`cwd`, `gitBranch`, `gitStatus`, `gitCounts`, `gitCommit`, `gitMetrics`, `runtime`, `packageVersion`, `sessionDuration`, `username`, `time`, `os`, `context`, `tokens`, `cost`). Toggle them from the `Built-in segments` tab in `/zentui`.
|
|
288
|
+
- `footerSegments`: show or hide individual built-in footer segments (`cwd`, `sessionName`, `gitBranch`, `gitStatus`, `gitCounts`, `gitCommit`, `gitMetrics`, `runtime`, `packageVersion`, `sessionDuration`, `username`, `time`, `os`, `context`, `tokens`, `cost`). Toggle them from the `Built-in segments` tab in `/zentui`.
|
|
287
289
|
- `footerFormat`: optional Starship-style template string that fully controls the footer layout. When set, it overrides `footerSegments`. See [Footer Format Template](#footer-format-template) below. The `/zentui` **Layout** tab configures context style, separator, path display mode/depth, branch length, and icon mode; set or clear custom formats with `/zentui format`.
|
|
288
290
|
- `gitCommit`: Starship [`git_commit`](https://starship.rs/config/#git-commit)-style options for the `gitCommit` footer segment. `hashLength` (default `7`, clamped to `4`–`40`) controls the short-hash display length. `onlyDetached` (default `true`) shows the hash mainly on detached HEAD. `showTag` (default `true`) appends an exact-match tag (`git describe --tags --exact-match HEAD`). The tag probe piggybacks on the existing git refresh — it only runs when both the segment and `showTag` are on, and misses/failures degrade silently.
|
|
289
291
|
- `gitMetrics`: Starship [`git_metrics`](https://starship.rs/config/#git-metrics)-style options for the `gitMetrics` footer segment. Uses `git diff HEAD --numstat` (staged + unstaged combined — the Starship “total dirty” view) to show aggregate `+added −deleted` line counts. `onlyNonzero` (default `true`) omits each zero component independently and hides the segment entirely at `0/0`. `ignoreSubmodules` (default `false`) adds `--ignore-submodules=all`. The numstat diff piggybacks on the existing git refresh and uses a hard 2s timeout; a metrics-only failure degrades silently without discarding fresh branch/status data. On very large monorepos the diff may lag or be omitted on timeout.
|
|
@@ -304,7 +306,7 @@ A second `$fill` creates a **centered middle zone** — content between the two
|
|
|
304
306
|
|
|
305
307
|
```json
|
|
306
308
|
{
|
|
307
|
-
"footerFormat": "$os $username $cwd( on $git_branch)( $git_status)( via $runtime)$fill($context)($sep$tokens)($sep$cost)($sep$time)"
|
|
309
|
+
"footerFormat": "$os $username $cwd($sep$session_name)( on $git_branch)( $git_status)( via $runtime)$fill($context)($sep$tokens)($sep$cost)($sep$time)"
|
|
308
310
|
}
|
|
309
311
|
```
|
|
310
312
|
|
|
@@ -321,6 +323,7 @@ Center the branch between directory and cost:
|
|
|
321
323
|
| Token | Aliases | Renders |
|
|
322
324
|
| ------------------- | ------------ | ------------------------------------------------------------------- |
|
|
323
325
|
| `$cwd` | `$directory` | current directory |
|
|
326
|
+
| `$session_name` | | current Pi session name |
|
|
324
327
|
| `$git_branch` | `$branch` | git branch with icon |
|
|
325
328
|
| `$git_status` | `$status` | `[!?↑]` status block |
|
|
326
329
|
| `$git_state` | `$state` | `REBASING` / `MERGING` / … (optional `n/m`) |
|
|
@@ -354,6 +357,7 @@ Center the branch between directory and cost:
|
|
|
354
357
|
- Literal text (`on branch`, `using`, `\|`, spaces) is rendered verbatim — you control all spacing.
|
|
355
358
|
- Each variable renders its core value only (no `on`/`via` prefixes); add those words as literal text.
|
|
356
359
|
- Conditional groups: wrap optional pieces in parentheses, e.g. `$cwd( on $git_branch)($git_status)$fill($context)`. If every `$var` inside a group is empty, the whole group (including its literals) is dropped.
|
|
360
|
+
- `$session_name` is available whenever `footerFormat` is set, independently of `footerSegments.sessionName`; use a conditional group such as `($sep$session_name)` so unnamed sessions leave no separator.
|
|
357
361
|
- Unknown `$variables` render empty.
|
|
358
362
|
- Set or clear at runtime: `/zentui format "<template>"` and `/zentui format clear`.
|
|
359
363
|
|
|
@@ -409,7 +413,7 @@ Mouse wheel scrolling is enabled by default when the fixed editor is on. Disable
|
|
|
409
413
|
|
|
410
414
|
## Requirements
|
|
411
415
|
|
|
412
|
-
- [Pi](https://pi.dev) coding agent 0.80 or newer
|
|
416
|
+
- [Pi](https://pi.dev) coding agent 0.80.3 or newer
|
|
413
417
|
- A [Nerd Font](https://www.nerdfonts.com/) for icons (or set `icons.mode` to `"ascii"`)
|
|
414
418
|
|
|
415
419
|
## Development
|
|
@@ -66,6 +66,7 @@ export type UiFeaturesConfig = {
|
|
|
66
66
|
|
|
67
67
|
export type FooterSegmentsConfig = {
|
|
68
68
|
cwd: boolean;
|
|
69
|
+
sessionName: boolean;
|
|
69
70
|
gitBranch: boolean;
|
|
70
71
|
gitStatus: boolean;
|
|
71
72
|
gitCounts: boolean;
|
|
@@ -132,6 +133,7 @@ export type PolishedTuiConfig = {
|
|
|
132
133
|
icons: ResolvedIcons;
|
|
133
134
|
colors: {
|
|
134
135
|
cwd: ColorSpec;
|
|
136
|
+
sessionName: ColorSpec;
|
|
135
137
|
gitBranch: ColorSpec;
|
|
136
138
|
gitStatus: ColorSpec;
|
|
137
139
|
contextNormal: ColorSpec;
|
|
@@ -177,6 +179,7 @@ export type PolishedTuiConfig = {
|
|
|
177
179
|
*/
|
|
178
180
|
export const FOOTER_FORMAT_VARIABLES = [
|
|
179
181
|
"cwd",
|
|
182
|
+
"session_name",
|
|
180
183
|
"git_branch",
|
|
181
184
|
"git_status",
|
|
182
185
|
"git_state",
|
|
@@ -229,6 +232,7 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
229
232
|
},
|
|
230
233
|
colors: {
|
|
231
234
|
cwd: "bold cyan",
|
|
235
|
+
sessionName: "bold green",
|
|
232
236
|
gitBranch: "bold purple",
|
|
233
237
|
gitStatus: "bold red",
|
|
234
238
|
contextNormal: "bright-black",
|
|
@@ -260,6 +264,7 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
260
264
|
},
|
|
261
265
|
footerSegments: {
|
|
262
266
|
cwd: true,
|
|
267
|
+
sessionName: true,
|
|
263
268
|
gitBranch: true,
|
|
264
269
|
gitStatus: true,
|
|
265
270
|
gitCounts: false,
|
|
@@ -431,6 +436,7 @@ function normalizeIconOverrides(record: Record<string, unknown>): Partial<IconGl
|
|
|
431
436
|
function normalizeColors(record: Record<string, unknown>): Partial<PolishedTuiConfig["colors"]> {
|
|
432
437
|
return definedColors({
|
|
433
438
|
cwd: colorValue(record, "cwd") ?? colorValue(record, "cwdText"),
|
|
439
|
+
sessionName: colorValue(record, "sessionName"),
|
|
434
440
|
gitBranch: colorValue(record, "gitBranch") ?? colorValue(record, "git"),
|
|
435
441
|
gitStatus: colorValue(record, "gitStatus"),
|
|
436
442
|
contextNormal: colorValue(record, "contextNormal"),
|
|
@@ -482,6 +488,7 @@ function normalizeUiFeatures(record: Record<string, unknown>): UiFeaturesConfig
|
|
|
482
488
|
function normalizeFooterSegments(record: Record<string, unknown>): FooterSegmentsConfig {
|
|
483
489
|
return {
|
|
484
490
|
cwd: footerSegmentValue(record, "cwd"),
|
|
491
|
+
sessionName: footerSegmentValue(record, "sessionName"),
|
|
485
492
|
gitBranch: footerSegmentValue(record, "gitBranch"),
|
|
486
493
|
gitStatus: footerSegmentValue(record, "gitStatus"),
|
|
487
494
|
gitCounts: footerSegmentValue(record, "gitCounts"),
|
|
@@ -593,6 +600,7 @@ function isUiFeatureKey(value: string): value is keyof UiFeaturesConfig {
|
|
|
593
600
|
function isFooterSegmentKey(value: string): value is keyof FooterSegmentsConfig {
|
|
594
601
|
return (
|
|
595
602
|
value === "cwd" ||
|
|
603
|
+
value === "sessionName" ||
|
|
596
604
|
value === "gitBranch" ||
|
|
597
605
|
value === "gitStatus" ||
|
|
598
606
|
value === "gitCounts" ||
|
|
@@ -2,7 +2,11 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { PolishedTuiConfig, SeparatorStyle } from "./config";
|
|
4
4
|
import { FOOTER_FORMAT_ALIASES } from "./config";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
collectExtensionStatusSegments,
|
|
7
|
+
type ExtensionStatusSegment,
|
|
8
|
+
sanitizeExtensionStatusText,
|
|
9
|
+
} from "./extension-status";
|
|
6
10
|
import { parseFooterFormat, renderFormatSplit, stripOrphanSeparators } from "./footer-format";
|
|
7
11
|
import {
|
|
8
12
|
buildContextDisplayLabel,
|
|
@@ -19,6 +23,7 @@ import {
|
|
|
19
23
|
formatUsernameHostLabel,
|
|
20
24
|
} from "./format";
|
|
21
25
|
import { resolveRuntimeSymbol } from "./icons";
|
|
26
|
+
import type { LiveContextOverride } from "./live-context";
|
|
22
27
|
import type { FooterState } from "./state";
|
|
23
28
|
import { renderStyleForSource } from "./style";
|
|
24
29
|
|
|
@@ -155,6 +160,7 @@ export function installFooter(
|
|
|
155
160
|
setRequestRender: (fn: (() => void) | undefined) => void;
|
|
156
161
|
scheduleProjectRefresh: (ctx: ExtensionContext) => void;
|
|
157
162
|
setExtensionStatusesGetter?: (fn: (() => ReadonlyMap<string, string>) | undefined) => void;
|
|
163
|
+
getLiveContext?: () => LiveContextOverride | undefined;
|
|
158
164
|
},
|
|
159
165
|
): void {
|
|
160
166
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
@@ -193,19 +199,35 @@ export function installFooter(
|
|
|
193
199
|
depth: config.pathDisplay.depth,
|
|
194
200
|
}),
|
|
195
201
|
);
|
|
202
|
+
const needsSessionName = config.footerFormat
|
|
203
|
+
? /(?:\$session_name\b|\$\{session_name\})/.test(config.footerFormat)
|
|
204
|
+
: config.footerSegments.sessionName;
|
|
205
|
+
const sessionName = needsSessionName
|
|
206
|
+
? sanitizeExtensionStatusText(ctx.sessionManager.getSessionName() ?? "")
|
|
207
|
+
: "";
|
|
208
|
+
const sessionNameLabel = sessionName
|
|
209
|
+
? renderStyleForSource(theme, colorSource, config.colors.sessionName, sessionName)
|
|
210
|
+
: "";
|
|
211
|
+
const builtInSessionNameLabel = sessionNameLabel ? `in ${sessionNameLabel}` : "";
|
|
196
212
|
const branch = state.branch;
|
|
197
213
|
const branchText = branch
|
|
198
214
|
? formatGitBranchText(branch, config.gitBranch.maxLength)
|
|
199
215
|
: undefined;
|
|
200
216
|
const contextUsage = ctx.getContextUsage();
|
|
217
|
+
const liveContext = hooks.getLiveContext?.();
|
|
201
218
|
const contextWindow = ctx.model?.contextWindow ?? contextUsage?.contextWindow;
|
|
219
|
+
const useLiveContext =
|
|
220
|
+
liveContext !== undefined && contextWindow !== undefined && contextWindow > 0;
|
|
221
|
+
const contextPercent = useLiveContext
|
|
222
|
+
? (liveContext.tokens / contextWindow) * 100
|
|
223
|
+
: contextUsage?.percent;
|
|
202
224
|
const contextLabel = buildContextDisplayLabel({
|
|
203
|
-
percent:
|
|
225
|
+
percent: contextPercent,
|
|
204
226
|
contextWindow,
|
|
205
227
|
style: config.contextStyle,
|
|
206
228
|
asciiGauge: iconMode === "ascii",
|
|
207
229
|
});
|
|
208
|
-
const tier = contextColorTier(
|
|
230
|
+
const tier = contextColorTier(contextPercent, config.contextThresholds);
|
|
209
231
|
const contextColor =
|
|
210
232
|
tier === "error"
|
|
211
233
|
? config.colors.contextError
|
|
@@ -255,6 +277,8 @@ export function installFooter(
|
|
|
255
277
|
switch (canonical) {
|
|
256
278
|
case "cwd":
|
|
257
279
|
return cwdLabel;
|
|
280
|
+
case "session_name":
|
|
281
|
+
return sessionNameLabel;
|
|
258
282
|
case "git_branch":
|
|
259
283
|
return branchText
|
|
260
284
|
? gitIcon
|
|
@@ -483,6 +507,7 @@ export function installFooter(
|
|
|
483
507
|
osSegment,
|
|
484
508
|
usernameSegment,
|
|
485
509
|
config.footerSegments.cwd ? cwdLabel : "",
|
|
510
|
+
config.footerSegments.sessionName ? builtInSessionNameLabel : "",
|
|
486
511
|
branchLabel,
|
|
487
512
|
gitCommitLabel,
|
|
488
513
|
gitMetricsLabel,
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
import { installFooter } from "./footer";
|
|
42
42
|
import { buildSessionDurationLabel, invalidateUsageTotalsCache } from "./format";
|
|
43
43
|
import { emptyGitStatus, readGitStatus } from "./git";
|
|
44
|
+
import { LiveContextController } from "./live-context";
|
|
44
45
|
import { readPackageVersionResult } from "./package-version";
|
|
45
46
|
import {
|
|
46
47
|
createProjectRefreshScheduler,
|
|
@@ -113,6 +114,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
113
114
|
const refresh = () => {
|
|
114
115
|
if (sessionLifecycle.isCurrent()) requestFooterRender?.();
|
|
115
116
|
};
|
|
117
|
+
const liveContext = new LiveContextController(sessionLifecycle, refresh);
|
|
116
118
|
const getActiveTheme = () => activeTheme;
|
|
117
119
|
const getCurrentConfig = () => currentConfig;
|
|
118
120
|
const getThinkingLevel = () =>
|
|
@@ -330,6 +332,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
330
332
|
setExtensionStatusesGetter(fn) {
|
|
331
333
|
getActiveExtensionStatuses = fn ?? (() => new Map());
|
|
332
334
|
},
|
|
335
|
+
getLiveContext: () => liveContext.get(),
|
|
333
336
|
});
|
|
334
337
|
footerInstalled = true;
|
|
335
338
|
stopProjectRefresh();
|
|
@@ -442,6 +445,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
442
445
|
|
|
443
446
|
pi.on("session_start", async (_event, ctx) => {
|
|
444
447
|
sessionLifecycle.start();
|
|
448
|
+
liveContext.clear();
|
|
445
449
|
state.sessionStartEpoch = Date.now();
|
|
446
450
|
invalidateUsageTotalsCache();
|
|
447
451
|
lastProjectCwd = undefined;
|
|
@@ -510,6 +514,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
510
514
|
});
|
|
511
515
|
|
|
512
516
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
517
|
+
liveContext.clear();
|
|
513
518
|
cleanupUi(ctx);
|
|
514
519
|
});
|
|
515
520
|
|
|
@@ -518,12 +523,45 @@ export default function (pi: ExtensionAPI) {
|
|
|
518
523
|
refreshInteractiveState(ctx, true);
|
|
519
524
|
};
|
|
520
525
|
|
|
521
|
-
pi.on("agent_start",
|
|
522
|
-
|
|
523
|
-
|
|
526
|
+
pi.on("agent_start", (event, ctx) => {
|
|
527
|
+
liveContext.clear();
|
|
528
|
+
syncInteractiveState(event, ctx);
|
|
529
|
+
});
|
|
530
|
+
pi.on("agent_end", (event, ctx) => {
|
|
531
|
+
liveContext.clear();
|
|
532
|
+
syncInteractiveAndProjectState(event, ctx);
|
|
533
|
+
});
|
|
534
|
+
pi.on("model_select", (event, ctx) => {
|
|
535
|
+
liveContext.clear();
|
|
536
|
+
syncInteractiveState(event, ctx);
|
|
537
|
+
});
|
|
524
538
|
pi.on("thinking_level_select", syncInteractiveState);
|
|
525
|
-
pi.on("
|
|
539
|
+
pi.on("session_info_changed", syncInteractiveState);
|
|
540
|
+
pi.on("message_update", (event) => {
|
|
541
|
+
liveContext.update(event.message);
|
|
542
|
+
});
|
|
543
|
+
pi.on("message_end", (event, ctx) => {
|
|
544
|
+
// Pi notifies extensions before persisting a successful message, so retain its live
|
|
545
|
+
// context until agent_end; failed messages clear immediately instead of showing stale usage.
|
|
546
|
+
if (
|
|
547
|
+
event.message.role === "assistant" &&
|
|
548
|
+
(event.message.stopReason === "error" || event.message.stopReason === "aborted")
|
|
549
|
+
) {
|
|
550
|
+
liveContext.clear();
|
|
551
|
+
}
|
|
552
|
+
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
553
|
+
});
|
|
554
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
555
|
+
liveContext.clear();
|
|
556
|
+
syncInteractiveState(event, ctx);
|
|
557
|
+
});
|
|
526
558
|
pi.on("tool_execution_end", syncInteractiveAndProjectState);
|
|
527
|
-
pi.on("session_compact",
|
|
528
|
-
|
|
559
|
+
pi.on("session_compact", (event, ctx) => {
|
|
560
|
+
liveContext.clear();
|
|
561
|
+
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
562
|
+
});
|
|
563
|
+
pi.on("session_tree", (event, ctx) => {
|
|
564
|
+
liveContext.clear();
|
|
565
|
+
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
566
|
+
});
|
|
529
567
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { AssistantMessage, Usage } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { SessionLifecycle } from "./session-lifecycle";
|
|
3
|
+
|
|
4
|
+
export type LiveContextOverride = {
|
|
5
|
+
tokens: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function usageComponent(value: unknown): number {
|
|
9
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function calculateLiveContextTokens(usage: Usage | undefined): number | undefined {
|
|
13
|
+
if (!usage) return undefined;
|
|
14
|
+
const totalTokens = usageComponent(usage.totalTokens);
|
|
15
|
+
if (totalTokens > 0) return totalTokens;
|
|
16
|
+
const calculated =
|
|
17
|
+
usageComponent(usage.input) +
|
|
18
|
+
usageComponent(usage.output) +
|
|
19
|
+
usageComponent(usage.cacheRead) +
|
|
20
|
+
usageComponent(usage.cacheWrite);
|
|
21
|
+
return calculated > 0 ? calculated : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function liveContextFromMessage(message: unknown): LiveContextOverride | undefined {
|
|
25
|
+
if (!message || typeof message !== "object") return undefined;
|
|
26
|
+
const assistant = message as Partial<AssistantMessage>;
|
|
27
|
+
if (assistant.role !== "assistant") return undefined;
|
|
28
|
+
if (assistant.stopReason === "error" || assistant.stopReason === "aborted") return undefined;
|
|
29
|
+
const tokens = calculateLiveContextTokens(assistant.usage);
|
|
30
|
+
return tokens === undefined ? undefined : { tokens };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class LiveContextController {
|
|
34
|
+
private readonly lifecycle: SessionLifecycle;
|
|
35
|
+
private readonly requestRender: () => void;
|
|
36
|
+
private override: LiveContextOverride | undefined;
|
|
37
|
+
private cancelScheduledRender: (() => void) | undefined;
|
|
38
|
+
private scheduledGeneration: number | undefined;
|
|
39
|
+
|
|
40
|
+
constructor(lifecycle: SessionLifecycle, requestRender: () => void) {
|
|
41
|
+
this.lifecycle = lifecycle;
|
|
42
|
+
this.requestRender = requestRender;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get(): LiveContextOverride | undefined {
|
|
46
|
+
return this.override;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
update(message: unknown): boolean {
|
|
50
|
+
const next = liveContextFromMessage(message);
|
|
51
|
+
if (!next || !this.lifecycle.isCurrent()) return false;
|
|
52
|
+
this.override = next;
|
|
53
|
+
const generation = this.lifecycle.currentGeneration();
|
|
54
|
+
if (this.cancelScheduledRender && this.scheduledGeneration !== generation) {
|
|
55
|
+
this.cancelScheduledRender = undefined;
|
|
56
|
+
this.scheduledGeneration = undefined;
|
|
57
|
+
}
|
|
58
|
+
if (!this.cancelScheduledRender) {
|
|
59
|
+
this.scheduledGeneration = generation;
|
|
60
|
+
this.cancelScheduledRender = this.lifecycle.defer(() => {
|
|
61
|
+
this.cancelScheduledRender = undefined;
|
|
62
|
+
this.scheduledGeneration = undefined;
|
|
63
|
+
if (this.override) this.requestRender();
|
|
64
|
+
}, 250);
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
clear(): void {
|
|
70
|
+
this.override = undefined;
|
|
71
|
+
this.cancelScheduledRender?.();
|
|
72
|
+
this.cancelScheduledRender = undefined;
|
|
73
|
+
this.scheduledGeneration = undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -18,14 +18,21 @@ export class SessionLifecycle {
|
|
|
18
18
|
return this.active && generation === this.generation;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
defer(callback: () => void): void {
|
|
22
|
-
if (!this.active) return;
|
|
21
|
+
defer(callback: () => void, delayMs = 0): () => void {
|
|
22
|
+
if (!this.active) return () => {};
|
|
23
23
|
const generation = this.generation;
|
|
24
|
+
let canceled = false;
|
|
24
25
|
const timeout = setTimeout(() => {
|
|
25
26
|
this.timeouts.delete(timeout);
|
|
26
|
-
if (this.isCurrent(generation)) callback();
|
|
27
|
-
},
|
|
27
|
+
if (!canceled && this.isCurrent(generation)) callback();
|
|
28
|
+
}, delayMs);
|
|
28
29
|
this.timeouts.add(timeout);
|
|
30
|
+
return () => {
|
|
31
|
+
if (canceled) return;
|
|
32
|
+
canceled = true;
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
this.timeouts.delete(timeout);
|
|
35
|
+
};
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
queueMicrotask(callback: () => void): () => void {
|
|
@@ -124,6 +124,7 @@ const featureSettingDescriptions: Record<FeatureSettingId, string> = {
|
|
|
124
124
|
|
|
125
125
|
const footerSegmentSettingLabels: Record<FooterSegmentSettingId, string> = {
|
|
126
126
|
cwd: "Current directory",
|
|
127
|
+
sessionName: "Session name",
|
|
127
128
|
gitBranch: "Git branch",
|
|
128
129
|
gitStatus: "Git status",
|
|
129
130
|
gitCounts: "Git counts",
|
|
@@ -142,6 +143,7 @@ const footerSegmentSettingLabels: Record<FooterSegmentSettingId, string> = {
|
|
|
142
143
|
|
|
143
144
|
const footerSegmentSettingDescriptions: Record<FooterSegmentSettingId, string> = {
|
|
144
145
|
cwd: "Show or hide the current working directory segment on the left.",
|
|
146
|
+
sessionName: "Show or hide the current Pi session name on the left, after the current directory.",
|
|
145
147
|
gitBranch: "Show or hide the git branch name on the left.",
|
|
146
148
|
gitStatus: "Show or hide git status icons and ahead/behind markers.",
|
|
147
149
|
gitCounts:
|
|
@@ -207,6 +209,7 @@ function isFeatureSettingId(value: string): value is FeatureSettingId {
|
|
|
207
209
|
function isFooterSegmentSettingId(value: string): value is FooterSegmentSettingId {
|
|
208
210
|
return (
|
|
209
211
|
value === "cwd" ||
|
|
212
|
+
value === "sessionName" ||
|
|
210
213
|
value === "gitBranch" ||
|
|
211
214
|
value === "gitStatus" ||
|
|
212
215
|
value === "gitCounts" ||
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-zentui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"prepublishOnly": "npm run verify && npm run pack:check"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@earendil-works/pi-ai": ">=0.80",
|
|
39
|
-
"@earendil-works/pi-coding-agent": ">=0.80",
|
|
40
|
-
"@earendil-works/pi-tui": ">=0.80"
|
|
38
|
+
"@earendil-works/pi-ai": ">=0.80.3",
|
|
39
|
+
"@earendil-works/pi-coding-agent": ">=0.80.3",
|
|
40
|
+
"@earendil-works/pi-tui": ">=0.80.3"
|
|
41
41
|
},
|
|
42
42
|
"pi": {
|
|
43
43
|
"extensions": [
|