pi-zentui 0.10.1 → 0.11.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
CHANGED
|
@@ -275,7 +275,7 @@ Default config values — copy this and change any value you want:
|
|
|
275
275
|
|
|
276
276
|
- 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
277
|
- `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.
|
|
278
|
+
- `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
279
|
- `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
280
|
- `contextThresholds`: `{ warning, error }` percentages (default `70` / `90`) that select contextNormal / contextWarning / contextError colors.
|
|
281
281
|
- `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`.
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
formatUsernameHostLabel,
|
|
20
20
|
} from "./format";
|
|
21
21
|
import { resolveRuntimeSymbol } from "./icons";
|
|
22
|
+
import type { LiveContextOverride } from "./live-context";
|
|
22
23
|
import type { FooterState } from "./state";
|
|
23
24
|
import { renderStyleForSource } from "./style";
|
|
24
25
|
|
|
@@ -155,6 +156,7 @@ export function installFooter(
|
|
|
155
156
|
setRequestRender: (fn: (() => void) | undefined) => void;
|
|
156
157
|
scheduleProjectRefresh: (ctx: ExtensionContext) => void;
|
|
157
158
|
setExtensionStatusesGetter?: (fn: (() => ReadonlyMap<string, string>) | undefined) => void;
|
|
159
|
+
getLiveContext?: () => LiveContextOverride | undefined;
|
|
158
160
|
},
|
|
159
161
|
): void {
|
|
160
162
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
@@ -198,14 +200,20 @@ export function installFooter(
|
|
|
198
200
|
? formatGitBranchText(branch, config.gitBranch.maxLength)
|
|
199
201
|
: undefined;
|
|
200
202
|
const contextUsage = ctx.getContextUsage();
|
|
203
|
+
const liveContext = hooks.getLiveContext?.();
|
|
201
204
|
const contextWindow = ctx.model?.contextWindow ?? contextUsage?.contextWindow;
|
|
205
|
+
const useLiveContext =
|
|
206
|
+
liveContext !== undefined && contextWindow !== undefined && contextWindow > 0;
|
|
207
|
+
const contextPercent = useLiveContext
|
|
208
|
+
? (liveContext.tokens / contextWindow) * 100
|
|
209
|
+
: contextUsage?.percent;
|
|
202
210
|
const contextLabel = buildContextDisplayLabel({
|
|
203
|
-
percent:
|
|
211
|
+
percent: contextPercent,
|
|
204
212
|
contextWindow,
|
|
205
213
|
style: config.contextStyle,
|
|
206
214
|
asciiGauge: iconMode === "ascii",
|
|
207
215
|
});
|
|
208
|
-
const tier = contextColorTier(
|
|
216
|
+
const tier = contextColorTier(contextPercent, config.contextThresholds);
|
|
209
217
|
const contextColor =
|
|
210
218
|
tier === "error"
|
|
211
219
|
? config.colors.contextError
|
|
@@ -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,44 @@ 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("message_update", (event) => {
|
|
540
|
+
liveContext.update(event.message);
|
|
541
|
+
});
|
|
542
|
+
pi.on("message_end", (event, ctx) => {
|
|
543
|
+
// Pi notifies extensions before persisting a successful message, so retain its live
|
|
544
|
+
// context until agent_end; failed messages clear immediately instead of showing stale usage.
|
|
545
|
+
if (
|
|
546
|
+
event.message.role === "assistant" &&
|
|
547
|
+
(event.message.stopReason === "error" || event.message.stopReason === "aborted")
|
|
548
|
+
) {
|
|
549
|
+
liveContext.clear();
|
|
550
|
+
}
|
|
551
|
+
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
552
|
+
});
|
|
553
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
554
|
+
liveContext.clear();
|
|
555
|
+
syncInteractiveState(event, ctx);
|
|
556
|
+
});
|
|
526
557
|
pi.on("tool_execution_end", syncInteractiveAndProjectState);
|
|
527
|
-
pi.on("session_compact",
|
|
528
|
-
|
|
558
|
+
pi.on("session_compact", (event, ctx) => {
|
|
559
|
+
liveContext.clear();
|
|
560
|
+
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
561
|
+
});
|
|
562
|
+
pi.on("session_tree", (event, ctx) => {
|
|
563
|
+
liveContext.clear();
|
|
564
|
+
syncInteractiveAndProjectStateWithUsage(event, ctx);
|
|
565
|
+
});
|
|
529
566
|
}
|
|
@@ -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 {
|