@rosetears/aili-pi 0.1.0 → 0.1.3

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.
Files changed (58) hide show
  1. package/README.md +15 -6
  2. package/THIRD_PARTY_NOTICES.md +14 -3
  3. package/extensions/header/index.ts +92 -0
  4. package/extensions/matrix/index.ts +375 -0
  5. package/extensions/zentui/config.ts +1014 -0
  6. package/extensions/zentui/extension-status.ts +96 -0
  7. package/extensions/zentui/fixed-editor/cluster.ts +98 -0
  8. package/extensions/zentui/fixed-editor/compositor.ts +719 -0
  9. package/extensions/zentui/fixed-editor/index.ts +223 -0
  10. package/extensions/zentui/fixed-editor/input.ts +85 -0
  11. package/extensions/zentui/fixed-editor/pi-compat.ts +296 -0
  12. package/extensions/zentui/fixed-editor/selection.ts +217 -0
  13. package/extensions/zentui/fixed-editor/terminal-modes.ts +75 -0
  14. package/extensions/zentui/fixed-editor/types.ts +37 -0
  15. package/extensions/zentui/footer-format.ts +279 -0
  16. package/extensions/zentui/footer.ts +595 -0
  17. package/extensions/zentui/format.ts +434 -0
  18. package/extensions/zentui/git.ts +384 -0
  19. package/extensions/zentui/gradient.ts +70 -0
  20. package/extensions/zentui/icons.ts +252 -0
  21. package/extensions/zentui/index.ts +580 -0
  22. package/extensions/zentui/live-context.ts +75 -0
  23. package/extensions/zentui/package-version.ts +650 -0
  24. package/extensions/zentui/project-refresh.ts +104 -0
  25. package/extensions/zentui/project-state.ts +59 -0
  26. package/extensions/zentui/prototype-patch-registry.ts +111 -0
  27. package/extensions/zentui/runtime.ts +841 -0
  28. package/extensions/zentui/selector-border.ts +77 -0
  29. package/extensions/zentui/session-lifecycle.ts +60 -0
  30. package/extensions/zentui/settings-command.ts +897 -0
  31. package/extensions/zentui/state.ts +55 -0
  32. package/extensions/zentui/style.ts +332 -0
  33. package/extensions/zentui/thinking-message.ts +159 -0
  34. package/extensions/zentui/tool-execution.ts +189 -0
  35. package/extensions/zentui/ui.ts +618 -0
  36. package/extensions/zentui/user-message.ts +252 -0
  37. package/licenses/pi-sakura-cyberdeck-MIT.txt +21 -0
  38. package/licenses/pi-zentui-MIT.txt +21 -0
  39. package/manifests/capabilities.json +1 -1
  40. package/manifests/live-verification.json +14 -8
  41. package/manifests/provenance.json +18 -5
  42. package/manifests/sbom.json +19 -3
  43. package/notices/pi-sakura-cyberdeck-NOTICE.txt +6 -0
  44. package/package.json +12 -3
  45. package/scripts/local-package-e2e.ts +1 -1
  46. package/scripts/sync-global-skills.d.mts +13 -0
  47. package/scripts/sync-global-skills.mjs +134 -0
  48. package/src/runtime/credential-guard.ts +58 -0
  49. package/src/runtime/doctor.ts +1 -1
  50. package/src/runtime/index.ts +2 -2
  51. package/src/runtime/path-boundaries.ts +1 -1
  52. package/src/runtime/registry.ts +6 -2
  53. package/src/runtime/rem-head.txt +38 -0
  54. package/src/runtime/rose-context.ts +1 -1
  55. package/src/runtime/subagents.ts +74 -403
  56. package/templates/APPEND_SYSTEM.md +10 -8
  57. package/themes/rem-cyberdeck.json +32 -0
  58. package/upstream/opencode-global-agents.lock.json +34 -0
@@ -0,0 +1,580 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ KeybindingsManager,
5
+ Theme,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
8
+ import {
9
+ type ColorSourcesConfig,
10
+ type ContextStyle,
11
+ type ExtensionStatusColorMode,
12
+ type ExtensionStatusPlacement,
13
+ ensureConfigExists,
14
+ type FixedEditorConfig,
15
+ type FooterSegmentsConfig,
16
+ type GitBranchConfig,
17
+ type IconMode,
18
+ loadConfig,
19
+ type PathDisplayConfig,
20
+ type PolishedTuiConfig,
21
+ type SeparatorStyle,
22
+ saveColorSourcesPatch,
23
+ saveContextStylePatch,
24
+ saveExtensionStatusColorMode,
25
+ saveExtensionStatusPlacement,
26
+ saveFixedEditorPatch,
27
+ saveFooterFormatPatch,
28
+ saveFooterSegmentsPatch,
29
+ saveGitBranchPatch,
30
+ saveIconsModePatch,
31
+ savePathDisplayPatch,
32
+ saveSeparatorPatch,
33
+ saveUiFeaturesPatch,
34
+ type UiFeaturesConfig,
35
+ } from "./config.js";
36
+ import {
37
+ disposeFixedEditor,
38
+ installFixedEditorProbe,
39
+ removeFixedEditorProbe,
40
+ } from "./fixed-editor/index.js";
41
+ import { installFooter } from "./footer.js";
42
+ import { buildSessionDurationLabel, invalidateUsageTotalsCache } from "./format.js";
43
+ import { emptyGitStatus, readGitStatus } from "./git.js";
44
+ import { LiveContextController } from "./live-context.js";
45
+ import { readPackageVersionResult } from "./package-version.js";
46
+ import {
47
+ createProjectRefreshScheduler,
48
+ type ScheduleProjectRefreshOptions,
49
+ type StopProjectRefreshInterval,
50
+ startProjectRefreshInterval,
51
+ } from "./project-refresh.js";
52
+ import { applyProjectRefreshToState } from "./project-state.js";
53
+ import { readRuntimeInfo } from "./runtime.js";
54
+ import { installSelectorBorderStyle } from "./selector-border.js";
55
+ import { SessionLifecycle } from "./session-lifecycle.js";
56
+ import { registerZentuiSettingsCommand } from "./settings-command.js";
57
+ import { createInitialState, type FooterState, syncState } from "./state.js";
58
+ import { installThinkingMessageStyle } from "./thinking-message.js";
59
+ import { installToolExecutionStyle } from "./tool-execution.js";
60
+ import { PolishedEditor, WrappedPolishedEditor } from "./ui.js";
61
+ import { installUserMessageStyle } from "./user-message.js";
62
+
63
+ const ZENTUI_EDITOR_FACTORY = Symbol.for("pi-zentui.editor-factory");
64
+ const ZENTUI_EDITOR_BASE_FACTORY = Symbol.for("pi-zentui.editor-base-factory");
65
+
66
+ type EditorFactory = NonNullable<Parameters<ExtensionContext["ui"]["setEditorComponent"]>[0]>;
67
+
68
+ type ZentuiEditorFactory = EditorFactory & {
69
+ [ZENTUI_EDITOR_FACTORY]?: true;
70
+ [ZENTUI_EDITOR_BASE_FACTORY]?: EditorFactory;
71
+ };
72
+
73
+ type ApplyUiResult = {
74
+ editorBlocked: boolean;
75
+ };
76
+
77
+ type EditorInstallMode = "none" | "standalone" | "wrapper";
78
+
79
+ function isZentuiEditorFactory(factory: EditorFactory | undefined): boolean {
80
+ return Boolean((factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_FACTORY]);
81
+ }
82
+
83
+ function getZentuiEditorBaseFactory(factory: EditorFactory | undefined): EditorFactory | undefined {
84
+ return (factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_BASE_FACTORY];
85
+ }
86
+
87
+ function isTuiContext(ctx: ExtensionContext): boolean {
88
+ try {
89
+ const mode = (ctx as ExtensionContext & { mode?: string }).mode;
90
+ return ctx.hasUI && (mode === undefined || mode === "tui");
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ export default function (pi: ExtensionAPI) {
97
+ const state: FooterState = createInitialState(emptyGitStatus());
98
+ const sessionLifecycle = new SessionLifecycle();
99
+
100
+ let currentConfig: PolishedTuiConfig = loadConfig();
101
+ let activeTheme: Theme | undefined;
102
+ let requestFooterRender: (() => void) | undefined;
103
+ let getActiveExtensionStatuses: () => ReadonlyMap<string, string> = () => new Map();
104
+ let stopRefreshInterval: StopProjectRefreshInterval = () => {};
105
+ let cleanupPrototypePatches: () => void = () => {};
106
+ let footerInstalled = false;
107
+ let editorInstalled = false;
108
+ let editorInstallMode: EditorInstallMode = "none";
109
+ let installedEditorFactory: EditorFactory | undefined;
110
+ let wrappedEditorFactory: EditorFactory | undefined;
111
+ let prototypePatchesInstalled = false;
112
+ let stopSessionTimer: () => void = () => {};
113
+ let lastDurationLabel = "";
114
+ let lastProjectCwd: string | undefined;
115
+
116
+ const refresh = () => {
117
+ if (sessionLifecycle.isCurrent()) requestFooterRender?.();
118
+ };
119
+ const liveContext = new LiveContextController(sessionLifecycle, refresh);
120
+ const getActiveTheme = () => activeTheme;
121
+ const getCurrentConfig = () => currentConfig;
122
+ const getThinkingLevel = () =>
123
+ sessionLifecycle.isCurrent() ? pi.getThinkingLevel() : ("off" as const);
124
+ const syncFooterState = (ctx: ExtensionContext) =>
125
+ syncState(state, ctx, currentConfig.icons.cacheHit);
126
+
127
+ type ProjectRefreshTarget = { cwd: string; generation: number };
128
+ const refreshProjectState = async ({ cwd, generation }: ProjectRefreshTarget) => {
129
+ if (!sessionLifecycle.isCurrent(generation)) return;
130
+ const gitCommitConfig = currentConfig.gitCommit;
131
+ const gitMetricsConfig = currentConfig.gitMetrics;
132
+ const segments = currentConfig.footerSegments;
133
+ const fmt = currentConfig.footerFormat;
134
+ // Enable optional probes when the segment is on OR a custom footerFormat
135
+ // references the relevant variable. Mirrors the session-duration timer
136
+ // pattern so format-only users still get data.
137
+ const formatNeedsTag = /\$\{?(?:git_tag|tag)\b/.test(fmt);
138
+ const formatNeedsCommit = /\$\{?(?:git_commit|commit)\b/.test(fmt);
139
+ const formatNeedsMetrics = /\$\{?(?:git_metrics|git_added|git_deleted)\b/.test(fmt);
140
+ const formatNeedsPackage = /\$\{?(?:package|package_version)\b/.test(fmt);
141
+ const wantExactTag =
142
+ ((segments.gitCommit || formatNeedsCommit) && gitCommitConfig.showTag) || formatNeedsTag;
143
+ const wantMetrics = segments.gitMetrics || formatNeedsMetrics;
144
+ const wantPackage = segments.packageVersion || formatNeedsPackage;
145
+ const [git, runtime, packageVersion] = await Promise.all([
146
+ readGitStatus(cwd, {
147
+ readExactTag: wantExactTag,
148
+ readMetrics: wantMetrics,
149
+ ignoreSubmodules: gitMetricsConfig.ignoreSubmodules,
150
+ }),
151
+ readRuntimeInfo(cwd),
152
+ wantPackage ? readPackageVersionResult(cwd) : Promise.resolve(undefined),
153
+ ]);
154
+ if (!sessionLifecycle.isCurrent(generation)) return;
155
+ lastProjectCwd = applyProjectRefreshToState(state, {
156
+ cwd,
157
+ previousCwd: lastProjectCwd,
158
+ git,
159
+ runtime,
160
+ packageVersion,
161
+ });
162
+ };
163
+
164
+ const projectRefreshScheduler = createProjectRefreshScheduler(refreshProjectState, refresh);
165
+ const scheduleProjectRefresh = (
166
+ ctx: ExtensionContext,
167
+ options?: ScheduleProjectRefreshOptions,
168
+ ) => {
169
+ const generation = sessionLifecycle.currentGeneration();
170
+ if (!sessionLifecycle.isCurrent(generation)) return;
171
+ const cwd = ctx.cwd;
172
+ projectRefreshScheduler.schedule({ cwd, generation }, options);
173
+ };
174
+
175
+ const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
176
+ if (!sessionLifecycle.isCurrent() || !ctx.hasUI) return;
177
+ syncFooterState(ctx);
178
+ if (project && currentConfig.features.statusLine) scheduleProjectRefresh(ctx);
179
+ refresh();
180
+ };
181
+
182
+ const stopProjectRefresh = () => {
183
+ stopRefreshInterval();
184
+ stopRefreshInterval = () => {};
185
+ projectRefreshScheduler.stop();
186
+ };
187
+
188
+ const startSessionTimer = () => {
189
+ stopSessionTimer();
190
+ lastDurationLabel = "";
191
+ const segments = currentConfig.footerSegments;
192
+ const format = currentConfig.footerFormat ?? "";
193
+ // The clock belongs to the editor's upper-right metadata, not the footer.
194
+ // Keep a lightweight refresh tick while that editor chrome is enabled.
195
+ const needsWallClock =
196
+ currentConfig.features.editor || segments.time || /\$\{?time\b/.test(format);
197
+ const needsDuration =
198
+ segments.sessionDuration || /\$\{?(?:session_duration|duration)\b/.test(format);
199
+ if (!(needsWallClock || (currentConfig.features.statusLine && needsDuration))) return;
200
+
201
+ const timer = setInterval(() => {
202
+ if (!sessionLifecycle.isCurrent()) return;
203
+ if (needsWallClock) {
204
+ refresh();
205
+ return;
206
+ }
207
+ const label = state.sessionStartEpoch
208
+ ? buildSessionDurationLabel(state.sessionStartEpoch)
209
+ : "";
210
+ if (label === lastDurationLabel) return;
211
+ lastDurationLabel = label;
212
+ refresh();
213
+ }, 1000);
214
+ timer.unref?.();
215
+ stopSessionTimer = () => {
216
+ clearInterval(timer);
217
+ stopSessionTimer = () => {};
218
+ };
219
+ };
220
+
221
+ const installPrototypePatches = () => {
222
+ if (prototypePatchesInstalled) return;
223
+ const cleanupSelectorBorderStyle = installSelectorBorderStyle(getActiveTheme, getCurrentConfig);
224
+ const cleanupUserMessageStyle = installUserMessageStyle(getActiveTheme, getCurrentConfig);
225
+ const cleanupToolExecutionStyle = installToolExecutionStyle(getActiveTheme);
226
+ const cleanupThinkingMessageStyle = installThinkingMessageStyle(getActiveTheme);
227
+ cleanupPrototypePatches = () => {
228
+ cleanupThinkingMessageStyle();
229
+ cleanupToolExecutionStyle();
230
+ cleanupSelectorBorderStyle();
231
+ cleanupUserMessageStyle();
232
+ };
233
+ prototypePatchesInstalled = true;
234
+ };
235
+
236
+ const uninstallPrototypePatches = () => {
237
+ cleanupPrototypePatches();
238
+ cleanupPrototypePatches = () => {};
239
+ prototypePatchesInstalled = false;
240
+ };
241
+
242
+ const makeEditorFactory = (ctx: ExtensionContext): ZentuiEditorFactory => {
243
+ const sessionTheme = ctx.ui.theme;
244
+ const factory = ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) =>
245
+ new PolishedEditor(
246
+ tui,
247
+ theme,
248
+ keybindings,
249
+ sessionTheme,
250
+ getCurrentConfig,
251
+ () => ({
252
+ modelLabel: state.modelLabel,
253
+ providerLabel: state.providerLabel,
254
+ contextLabel: state.contextLabel,
255
+ contextUsedLabel: state.contextUsedLabel,
256
+ tokenLabel: state.tokenLabel,
257
+ }),
258
+ getThinkingLevel,
259
+ )) as ZentuiEditorFactory;
260
+ factory[ZENTUI_EDITOR_FACTORY] = true;
261
+ return factory;
262
+ };
263
+
264
+ const makeWrappedEditorFactory = (
265
+ ctx: ExtensionContext,
266
+ baseFactory: EditorFactory,
267
+ ): ZentuiEditorFactory => {
268
+ const sessionTheme = ctx.ui.theme;
269
+ const factory = ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) =>
270
+ new WrappedPolishedEditor(
271
+ baseFactory(tui, theme, keybindings),
272
+ sessionTheme,
273
+ getCurrentConfig,
274
+ () => ({
275
+ modelLabel: state.modelLabel,
276
+ providerLabel: state.providerLabel,
277
+ contextLabel: state.contextLabel,
278
+ contextUsedLabel: state.contextUsedLabel,
279
+ tokenLabel: state.tokenLabel,
280
+ }),
281
+ getThinkingLevel,
282
+ )) as ZentuiEditorFactory;
283
+ factory[ZENTUI_EDITOR_FACTORY] = true;
284
+ factory[ZENTUI_EDITOR_BASE_FACTORY] = baseFactory;
285
+ return factory;
286
+ };
287
+
288
+ const installEditor = (ctx: ExtensionContext): boolean => {
289
+ const currentFactory = ctx.ui.getEditorComponent();
290
+ if (currentFactory && currentFactory === installedEditorFactory) {
291
+ editorInstalled = true;
292
+ return true;
293
+ }
294
+
295
+ installPrototypePatches();
296
+ const currentZentuiBaseFactory = getZentuiEditorBaseFactory(currentFactory);
297
+ if (currentFactory && isZentuiEditorFactory(currentFactory)) {
298
+ wrappedEditorFactory = currentZentuiBaseFactory;
299
+ const nextFactory = currentZentuiBaseFactory
300
+ ? makeWrappedEditorFactory(ctx, currentZentuiBaseFactory)
301
+ : makeEditorFactory(ctx);
302
+ ctx.ui.setEditorComponent(nextFactory);
303
+ installedEditorFactory = nextFactory;
304
+ editorInstallMode = currentZentuiBaseFactory ? "wrapper" : "standalone";
305
+ } else if (currentFactory) {
306
+ wrappedEditorFactory = currentFactory;
307
+ const nextFactory = makeWrappedEditorFactory(ctx, currentFactory);
308
+ ctx.ui.setEditorComponent(nextFactory);
309
+ installedEditorFactory = nextFactory;
310
+ editorInstallMode = "wrapper";
311
+ } else {
312
+ wrappedEditorFactory = undefined;
313
+ const nextFactory = makeEditorFactory(ctx);
314
+ ctx.ui.setEditorComponent(nextFactory);
315
+ installedEditorFactory = nextFactory;
316
+ editorInstallMode = "standalone";
317
+ }
318
+ editorInstalled = true;
319
+ return true;
320
+ };
321
+
322
+ const uninstallEditor = (ctx: ExtensionContext): boolean => {
323
+ const currentFactory = ctx.ui.getEditorComponent();
324
+ if (currentFactory && !isZentuiEditorFactory(currentFactory)) return false;
325
+
326
+ uninstallPrototypePatches();
327
+ ctx.ui.setEditorComponent(
328
+ editorInstallMode === "wrapper" && wrappedEditorFactory ? wrappedEditorFactory : undefined,
329
+ );
330
+ wrappedEditorFactory = undefined;
331
+ installedEditorFactory = undefined;
332
+ editorInstallMode = "none";
333
+ editorInstalled = false;
334
+ return true;
335
+ };
336
+
337
+ const installStatusLine = (ctx: ExtensionContext) => {
338
+ if (footerInstalled) return;
339
+ installFooter(ctx, state, getCurrentConfig, {
340
+ setRequestRender: (fn) => {
341
+ requestFooterRender = fn;
342
+ },
343
+ scheduleProjectRefresh,
344
+ setExtensionStatusesGetter(fn) {
345
+ getActiveExtensionStatuses = fn ?? (() => new Map());
346
+ },
347
+ getLiveContext: () => liveContext.get(),
348
+ });
349
+ footerInstalled = true;
350
+ stopProjectRefresh();
351
+ stopRefreshInterval = startProjectRefreshInterval(currentConfig.projectRefreshIntervalMs, () =>
352
+ scheduleProjectRefresh(ctx),
353
+ );
354
+ scheduleProjectRefresh(ctx, { force: true });
355
+ refresh();
356
+ startSessionTimer();
357
+ };
358
+
359
+ const uninstallStatusLine = (ctx: ExtensionContext) => {
360
+ stopSessionTimer();
361
+ stopProjectRefresh();
362
+ ctx.ui.setFooter(undefined);
363
+ footerInstalled = false;
364
+ requestFooterRender = undefined;
365
+ getActiveExtensionStatuses = () => new Map();
366
+ };
367
+
368
+ const applyConfiguredUi = (ctx: ExtensionContext): ApplyUiResult => {
369
+ const result: ApplyUiResult = { editorBlocked: false };
370
+ if (!isTuiContext(ctx)) return result;
371
+ activeTheme = ctx.ui.theme;
372
+ if (currentConfig.features.editor) {
373
+ const currentFactory = ctx.ui.getEditorComponent();
374
+ const editorMissingOrReplaced = !editorInstalled || !isZentuiEditorFactory(currentFactory);
375
+ if (editorMissingOrReplaced) result.editorBlocked = !installEditor(ctx);
376
+ } else if (editorInstalled || prototypePatchesInstalled) {
377
+ result.editorBlocked = !uninstallEditor(ctx);
378
+ }
379
+
380
+ if (currentConfig.features.statusLine) {
381
+ installStatusLine(ctx);
382
+ } else if (footerInstalled) {
383
+ uninstallStatusLine(ctx);
384
+ }
385
+ return result;
386
+ };
387
+
388
+ const installUi = (ctx: ExtensionContext) => {
389
+ if (!isTuiContext(ctx)) return;
390
+ activeTheme = ctx.ui.theme;
391
+ uninstallPrototypePatches();
392
+ footerInstalled = false;
393
+ editorInstalled = false;
394
+ installedEditorFactory = undefined;
395
+ ensureConfigExists();
396
+ currentConfig = loadConfig();
397
+ syncFooterState(ctx);
398
+ stopProjectRefresh();
399
+ applyConfiguredUi(ctx);
400
+ if (currentConfig.fixedEditor?.enabled) {
401
+ installFixedEditorProbe(ctx, getCurrentConfig, sessionLifecycle);
402
+ }
403
+ refresh();
404
+ };
405
+
406
+ const scheduleEditorReconciliation = (ctx: ExtensionContext) => {
407
+ sessionLifecycle.defer(() => {
408
+ if (!isTuiContext(ctx) || !currentConfig.features.editor) return;
409
+ const currentFactory = ctx.ui.getEditorComponent();
410
+ if (currentFactory && currentFactory !== installedEditorFactory) {
411
+ applyConfiguredUi(ctx);
412
+ refresh();
413
+ }
414
+ });
415
+ };
416
+
417
+ const cleanupUi = (ctx?: ExtensionContext) => {
418
+ if (!ctx || !sessionLifecycle.isCurrent()) return;
419
+ sessionLifecycle.shutdown();
420
+ try {
421
+ disposeFixedEditor(ctx);
422
+ if (isTuiContext(ctx)) removeFixedEditorProbe(ctx);
423
+ uninstallPrototypePatches();
424
+ stopSessionTimer();
425
+ stopProjectRefresh();
426
+ requestFooterRender = undefined;
427
+ getActiveExtensionStatuses = () => new Map();
428
+ if (isTuiContext(ctx)) {
429
+ ctx.ui.setFooter(undefined);
430
+ const currentFactory = ctx.ui.getEditorComponent();
431
+ if (!currentFactory || isZentuiEditorFactory(currentFactory)) {
432
+ ctx.ui.setEditorComponent(
433
+ getZentuiEditorBaseFactory(currentFactory) ??
434
+ (editorInstallMode === "wrapper" && wrappedEditorFactory
435
+ ? wrappedEditorFactory
436
+ : undefined),
437
+ );
438
+ }
439
+ }
440
+ wrappedEditorFactory = undefined;
441
+ installedEditorFactory = undefined;
442
+ editorInstallMode = "none";
443
+ footerInstalled = false;
444
+ editorInstalled = false;
445
+ activeTheme = undefined;
446
+ } finally {
447
+ requestFooterRender = undefined;
448
+ }
449
+ };
450
+
451
+ const syncInteractiveState = (_event: unknown, ctx: ExtensionContext) => {
452
+ refreshInteractiveState(ctx);
453
+ };
454
+ const syncInteractiveAndProjectState = (_event: unknown, ctx: ExtensionContext) => {
455
+ refreshInteractiveState(ctx, true);
456
+ };
457
+
458
+ pi.on("session_start", async (_event, ctx) => {
459
+ sessionLifecycle.start();
460
+ liveContext.clear();
461
+ state.sessionStartEpoch = Date.now();
462
+ invalidateUsageTotalsCache();
463
+ lastProjectCwd = undefined;
464
+ installUi(ctx);
465
+ scheduleEditorReconciliation(ctx);
466
+ });
467
+
468
+ registerZentuiSettingsCommand(pi, {
469
+ sessionLifecycle,
470
+ getConfig: getCurrentConfig,
471
+ setColorSources(patch: Partial<ColorSourcesConfig>) {
472
+ currentConfig = saveColorSourcesPatch(patch);
473
+ },
474
+ setUiFeatures(patch: Partial<UiFeaturesConfig>, ctx: ExtensionContext) {
475
+ currentConfig = saveUiFeaturesPatch(patch);
476
+ const result = applyConfiguredUi(ctx);
477
+ return {
478
+ applied: !(patch.editor !== undefined && result.editorBlocked),
479
+ reason: result.editorBlocked
480
+ ? "another extension is currently managing the editor; reload Pi to apply this change"
481
+ : undefined,
482
+ };
483
+ },
484
+ setFooterSegments(patch: Partial<FooterSegmentsConfig>) {
485
+ currentConfig = saveFooterSegmentsPatch(patch);
486
+ startSessionTimer();
487
+ },
488
+ setFooterFormat(value: string) {
489
+ currentConfig = saveFooterFormatPatch(value);
490
+ startSessionTimer();
491
+ },
492
+ setIconMode(mode: IconMode) {
493
+ currentConfig = saveIconsModePatch(mode);
494
+ },
495
+ setContextStyle(style: ContextStyle) {
496
+ currentConfig = saveContextStylePatch(style);
497
+ },
498
+ setSeparator(separator: SeparatorStyle) {
499
+ currentConfig = saveSeparatorPatch(separator);
500
+ },
501
+ setPathDisplay(patch: Partial<PathDisplayConfig>) {
502
+ currentConfig = savePathDisplayPatch(patch);
503
+ },
504
+ setGitBranch(patch: Partial<GitBranchConfig>) {
505
+ currentConfig = saveGitBranchPatch(patch);
506
+ },
507
+ getActiveExtensionStatuses() {
508
+ return getActiveExtensionStatuses();
509
+ },
510
+ setExtensionStatusPlacement(key: string, placement: ExtensionStatusPlacement) {
511
+ currentConfig = saveExtensionStatusPlacement(key, placement);
512
+ },
513
+ setExtensionStatusColorMode(key: string, colorMode: ExtensionStatusColorMode) {
514
+ currentConfig = saveExtensionStatusColorMode(key, colorMode);
515
+ },
516
+ setFixedEditor(patch: Partial<FixedEditorConfig>, ctx: ExtensionContext) {
517
+ currentConfig = saveFixedEditorPatch(patch);
518
+ if (patch.enabled === true) {
519
+ installFixedEditorProbe(ctx, getCurrentConfig, sessionLifecycle);
520
+ } else if (patch.enabled === false) {
521
+ disposeFixedEditor(ctx);
522
+ }
523
+ refresh();
524
+ },
525
+ requestRender() {
526
+ refresh();
527
+ },
528
+ });
529
+
530
+ pi.on("session_shutdown", async (_event, ctx) => {
531
+ liveContext.clear();
532
+ cleanupUi(ctx);
533
+ });
534
+
535
+ const syncInteractiveAndProjectStateWithUsage = (_event: unknown, ctx: ExtensionContext) => {
536
+ invalidateUsageTotalsCache();
537
+ refreshInteractiveState(ctx, true);
538
+ };
539
+
540
+ pi.on("agent_start", (event, ctx) => {
541
+ liveContext.clear();
542
+ syncInteractiveState(event, ctx);
543
+ });
544
+ pi.on("agent_end", (event, ctx) => {
545
+ liveContext.clear();
546
+ syncInteractiveAndProjectState(event, ctx);
547
+ });
548
+ pi.on("model_select", (event, ctx) => {
549
+ liveContext.clear();
550
+ syncInteractiveState(event, ctx);
551
+ });
552
+ pi.on("thinking_level_select", syncInteractiveState);
553
+ pi.on("message_update", (event) => {
554
+ liveContext.update(event.message);
555
+ });
556
+ pi.on("message_end", (event, ctx) => {
557
+ // Pi notifies extensions before persisting a successful message, so retain its live
558
+ // context until agent_end; failed messages clear immediately instead of showing stale usage.
559
+ if (
560
+ event.message.role === "assistant" &&
561
+ (event.message.stopReason === "error" || event.message.stopReason === "aborted")
562
+ ) {
563
+ liveContext.clear();
564
+ }
565
+ syncInteractiveAndProjectStateWithUsage(event, ctx);
566
+ });
567
+ pi.on("tool_execution_start", (event, ctx) => {
568
+ liveContext.clear();
569
+ syncInteractiveState(event, ctx);
570
+ });
571
+ pi.on("tool_execution_end", syncInteractiveAndProjectState);
572
+ pi.on("session_compact", (event, ctx) => {
573
+ liveContext.clear();
574
+ syncInteractiveAndProjectStateWithUsage(event, ctx);
575
+ });
576
+ pi.on("session_tree", (event, ctx) => {
577
+ liveContext.clear();
578
+ syncInteractiveAndProjectStateWithUsage(event, ctx);
579
+ });
580
+ }
@@ -0,0 +1,75 @@
1
+ import type { AssistantMessage, Usage } from "@earendil-works/pi-ai";
2
+ import type { SessionLifecycle } from "./session-lifecycle.js";
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
+ }