pi-one-ui 0.1.1

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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/extensions/app/config/renderer.ts +452 -0
  4. package/extensions/app/config/shell.ts +2484 -0
  5. package/extensions/app/ownership.ts +18 -0
  6. package/extensions/app/panel.ts +533 -0
  7. package/extensions/app/presets.ts +36 -0
  8. package/extensions/features/agent-summary/core.ts +194 -0
  9. package/extensions/features/agent-summary/index.ts +37 -0
  10. package/extensions/features/compact-thinking.ts +1262 -0
  11. package/extensions/features/context.ts +857 -0
  12. package/extensions/features/reference/index.ts +643 -0
  13. package/extensions/features/reference/session.ts +278 -0
  14. package/extensions/features/reference/subagent.ts +218 -0
  15. package/extensions/features/shell/aliases.ts +28 -0
  16. package/extensions/features/shell/flush-docked-bash.ts +39 -0
  17. package/extensions/features/shell/startup-header.ts +357 -0
  18. package/extensions/features/shell/working-message.ts +226 -0
  19. package/extensions/index.ts +52 -0
  20. package/extensions/shell/accent-rail-editor.ts +112 -0
  21. package/extensions/shell/accent-rail-layout-patch.ts +530 -0
  22. package/extensions/shell/completion-menu.ts +186 -0
  23. package/extensions/shell/editor-metadata-format.ts +287 -0
  24. package/extensions/shell/editor-transfer.ts +73 -0
  25. package/extensions/shell/extension-status.ts +102 -0
  26. package/extensions/shell/footer-format.ts +388 -0
  27. package/extensions/shell/footer-layout.ts +144 -0
  28. package/extensions/shell/footer.ts +1035 -0
  29. package/extensions/shell/format.ts +577 -0
  30. package/extensions/shell/git.ts +434 -0
  31. package/extensions/shell/icons.ts +261 -0
  32. package/extensions/shell/index.ts +1659 -0
  33. package/extensions/shell/interaction-summary.ts +1194 -0
  34. package/extensions/shell/live-context.ts +82 -0
  35. package/extensions/shell/minimalist-editor.ts +548 -0
  36. package/extensions/shell/package-version.ts +675 -0
  37. package/extensions/shell/project-refresh.ts +130 -0
  38. package/extensions/shell/project-state.ts +60 -0
  39. package/extensions/shell/prototype-patch-registry.ts +183 -0
  40. package/extensions/shell/runtime.ts +915 -0
  41. package/extensions/shell/selector-border.ts +109 -0
  42. package/extensions/shell/session-lifecycle.ts +60 -0
  43. package/extensions/shell/settings-command.ts +2144 -0
  44. package/extensions/shell/settings-previews.ts +137 -0
  45. package/extensions/shell/state.ts +83 -0
  46. package/extensions/shell/style.ts +480 -0
  47. package/extensions/shell/telemetry.ts +90 -0
  48. package/extensions/shell/ui.ts +1326 -0
  49. package/extensions/shell/user-message-osc.ts +279 -0
  50. package/extensions/shell/user-message-styles.ts +266 -0
  51. package/extensions/shell/user-message.ts +226 -0
  52. package/extensions/shell/working-line-messages.ts +24 -0
  53. package/extensions/shell/working-line-spinners.ts +54 -0
  54. package/extensions/shell/working-line.ts +1603 -0
  55. package/extensions/tools/ansi-color.ts +44 -0
  56. package/extensions/tools/ansi-text.ts +52 -0
  57. package/extensions/tools/component-tree.ts +37 -0
  58. package/extensions/tools/format.ts +28 -0
  59. package/extensions/tools/fullscreen-detect.ts +11 -0
  60. package/extensions/tools/patch-keys.ts +158 -0
  61. package/extensions/tools/sgr-mouse.ts +59 -0
  62. package/extensions/tools/tool-loading-icon.ts +20 -0
  63. package/extensions/tools/tool-result-sanitize.ts +27 -0
  64. package/extensions/transcript/index.ts +65 -0
  65. package/extensions/transcript/renderer/compact-mode.ts +1443 -0
  66. package/extensions/transcript/renderer/default-mode.ts +735 -0
  67. package/extensions/transcript/renderer/index.ts +286 -0
  68. package/extensions/transcript/renderer/markdown-enhance.ts +325 -0
  69. package/extensions/transcript/renderer/mouse/hover.ts +175 -0
  70. package/extensions/transcript/renderer/mouse/interaction.ts +1016 -0
  71. package/extensions/transcript/renderer/mouse/layout.ts +207 -0
  72. package/extensions/transcript/renderer/mouse/packets.ts +96 -0
  73. package/extensions/transcript/renderer/mouse/scroll.ts +211 -0
  74. package/extensions/transcript/renderer/tool/diff/ATTRIBUTION.md +28 -0
  75. package/extensions/transcript/renderer/tool/diff/ansi-utils.ts +128 -0
  76. package/extensions/transcript/renderer/tool/diff/diff-component.ts +112 -0
  77. package/extensions/transcript/renderer/tool/diff/diff-edit-render.ts +255 -0
  78. package/extensions/transcript/renderer/tool/diff/diff-header.ts +184 -0
  79. package/extensions/transcript/renderer/tool/diff/diff-highlight.ts +131 -0
  80. package/extensions/transcript/renderer/tool/diff/diff-inline.ts +350 -0
  81. package/extensions/transcript/renderer/tool/diff/diff-layout.ts +870 -0
  82. package/extensions/transcript/renderer/tool/diff/diff-limits.ts +189 -0
  83. package/extensions/transcript/renderer/tool/diff/diff-palette.ts +436 -0
  84. package/extensions/transcript/renderer/tool/diff/diff-parse.ts +536 -0
  85. package/extensions/transcript/renderer/tool/diff/diff-presentation.ts +79 -0
  86. package/extensions/transcript/renderer/tool/diff/diff-renderer.ts +29 -0
  87. package/extensions/transcript/renderer/tool/diff/diff-text.ts +57 -0
  88. package/extensions/transcript/renderer/tool/diff/diff-write-render.ts +629 -0
  89. package/extensions/transcript/renderer/tool/diff/index.ts +147 -0
  90. package/extensions/transcript/renderer/tool/diff/line-width-safety.ts +100 -0
  91. package/extensions/transcript/renderer/tool/diff/shiki-highlight.ts +104 -0
  92. package/extensions/transcript/renderer/tool/diff/write-execution.ts +124 -0
  93. package/extensions/transcript/renderer/tool/grouping.ts +619 -0
  94. package/extensions/transcript/renderer/tool/message-display.ts +183 -0
  95. package/extensions/transcript/renderer/tool/names.ts +195 -0
  96. package/extensions/transcript/renderer/tool/result.ts +842 -0
  97. package/extensions/transcript/renderer/tool/show-more-hint.ts +42 -0
  98. package/extensions/transcript/renderer/transcript-refresh.ts +79 -0
  99. package/package.json +58 -0
  100. package/themes/cc-dark.json +91 -0
  101. package/themes/cc-light.json +91 -0
@@ -0,0 +1,1659 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import type {
4
+ ExtensionAPI,
5
+ ExtensionContext,
6
+ KeybindingsManager,
7
+ Theme,
8
+ } from "@earendil-works/pi-coding-agent";
9
+ import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
10
+ import {
11
+ type AccentRailLayoutPatchDiagnostic,
12
+ installHostAccentRailLayoutPatch,
13
+ markAccentRailLayoutEditor,
14
+ retainAccentRailLayoutPatchInstallation,
15
+ } from "./accent-rail-layout-patch.ts";
16
+ import {
17
+ type AccentRailEditorStyleConfig,
18
+ type ContextStyle,
19
+ type EditorComponentConfig,
20
+ type ExtensionStatusColorMode,
21
+ type ExtensionStatusPlacement,
22
+ ensureConfigExists,
23
+ FOOTER_FORMAT_ALIASES,
24
+ type FooterComponentConfig,
25
+ type FooterSegmentsConfig,
26
+ type GitBranchConfig,
27
+ type GitCommitConfig,
28
+ type GitMetricsConfig,
29
+ hasUnsupportedComponentStyle,
30
+ type IconMode,
31
+ loadConfig,
32
+ type MinimalistConfig,
33
+ type PathDisplayConfig,
34
+ type PolishedCopyFriendlyEditorStyleConfig,
35
+ type PolishedEditorStyleConfig,
36
+ type PolishedTuiConfig,
37
+ type SelectorBordersComponentConfig,
38
+ type SeparatorStyle,
39
+ saveAccentRailEditorStylePatch,
40
+ saveEditorComponentPatch,
41
+ saveExtensionStatusColorMode,
42
+ saveExtensionStatusDefaultPlacement,
43
+ saveExtensionStatusPlacement,
44
+ saveFooterComponentPatch,
45
+ saveIconsModePatch,
46
+ saveMinimalistEditorStylePatch,
47
+ savePolishedCopyFriendlyEditorStylePatch,
48
+ savePolishedEditorStylePatch,
49
+ saveSelectorBordersComponentPatch,
50
+ saveStarshipFooterStylePatch,
51
+ saveUserMessagesComponentPatch,
52
+ saveWorkingLineComponentPatch,
53
+ type UserMessagesComponentConfig,
54
+ type WorkingLineComponentPatch,
55
+ type ZentuiConfig,
56
+ } from "../app/config/shell.ts";
57
+ import {
58
+ type EditorTransferFailureReason,
59
+ replaceEditorComponentWithExpandedText,
60
+ } from "./editor-transfer.ts";
61
+ import { installFooter, installHiddenFooter } from "./footer.ts";
62
+ import {
63
+ collectFooterFormatReferences,
64
+ parseFooterFormat,
65
+ } from "./footer-format.ts";
66
+ import {
67
+ buildSessionDurationLabel,
68
+ invalidateUsageTotalsCache,
69
+ } from "./format.ts";
70
+ import { emptyGitStatus, readGitStatus } from "./git.ts";
71
+ import {
72
+ InteractionMetricsTracker,
73
+ renderTurnSummaryEntry,
74
+ TURN_SUMMARY_ENTRY_TYPE,
75
+ } from "./interaction-summary.ts";
76
+ import { LiveContextController } from "./live-context.ts";
77
+ import { readPackageVersionResult } from "./package-version.ts";
78
+ import {
79
+ createProjectRefreshScheduler,
80
+ type ProjectRefreshRun,
81
+ type ScheduleProjectRefreshOptions,
82
+ type StopProjectRefreshInterval,
83
+ startProjectRefreshInterval,
84
+ } from "./project-refresh.ts";
85
+ import { applyProjectRefreshToState } from "./project-state.ts";
86
+ import { readRuntimeInfo } from "./runtime.ts";
87
+ import {
88
+ installSelectorBorderStyle,
89
+ removeSelectorBorderStyle,
90
+ } from "./selector-border.ts";
91
+ import { SessionLifecycle } from "./session-lifecycle.ts";
92
+ import { registerShellSettingsCommand } from "./settings-command.ts";
93
+ import {
94
+ createInitialState,
95
+ type FooterState,
96
+ modelLabelFor,
97
+ syncState,
98
+ } from "./state.ts";
99
+ import { resolveFooterTelemetry } from "./telemetry.ts";
100
+ import { PolishedEditor, WrappedPolishedEditor } from "./ui.ts";
101
+ import {
102
+ installUserMessageStyle,
103
+ removeUserMessageStyle,
104
+ } from "./user-message.ts";
105
+ import {
106
+ AgentDurationClock,
107
+ snapshotWorkingLineHighStyle,
108
+ WorkingLineController,
109
+ } from "./working-line.ts";
110
+
111
+ const ZENTUI_EDITOR_FACTORY = Symbol.for("pi-zentui.editor-factory");
112
+ const ZENTUI_EDITOR_BASE_FACTORY = Symbol.for("pi-zentui.editor-base-factory");
113
+ const ZENTUI_EDITOR_OWNER = Symbol.for("pi-zentui.editor-owner");
114
+ const ZENTUI_FOOTER_OWNER = Symbol.for("pi-zentui.footer-owner");
115
+
116
+ type EditorFactory = NonNullable<
117
+ Parameters<ExtensionContext["ui"]["setEditorComponent"]>[0]
118
+ >;
119
+
120
+ type ZentuiEditorFactory = EditorFactory & {
121
+ [ZENTUI_EDITOR_FACTORY]?: true;
122
+ [ZENTUI_EDITOR_BASE_FACTORY]?: EditorFactory;
123
+ [ZENTUI_EDITOR_OWNER]?: symbol;
124
+ };
125
+
126
+ type ApplyUiResult = {
127
+ editorBlocked: boolean;
128
+ editorReason?: string;
129
+ };
130
+
131
+ type EditorChangeResult = { ok: true } | { ok: false; reason: string };
132
+
133
+ type EditorInstallMode = "none" | "standalone" | "wrapper";
134
+ type InstalledFooterKind = "starship" | "hidden";
135
+
136
+ function editorTransferFailureMessage(
137
+ reason: EditorTransferFailureReason,
138
+ ): string {
139
+ switch (reason) {
140
+ case "unsupported-transfer-api":
141
+ return "this Pi version cannot safely transfer expanded editor text; reload Pi to apply this change";
142
+ case "editor-factory-snapshot-failed":
143
+ return "the current editor factory could not be read safely; reload Pi to apply this change";
144
+ case "editor-text-snapshot-failed":
145
+ return "expanded editor text could not be read safely; reload Pi to apply this change";
146
+ case "editor-text-preparation-failed":
147
+ return "expanded editor text could not be prepared safely; reload Pi to apply this change";
148
+ case "editor-replacement-failed-with-rollback":
149
+ return "the editor replacement failed; the previous factory was reapplied, but editor instance identity is not guaranteed";
150
+ case "editor-replacement-rollback-failed":
151
+ return "the editor replacement and previous-factory rollback both failed; reload Pi before editing";
152
+ }
153
+ }
154
+
155
+ function isZentuiEditorFactory(factory: EditorFactory | undefined): boolean {
156
+ return Boolean(
157
+ (factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_FACTORY],
158
+ );
159
+ }
160
+
161
+ function getZentuiEditorBaseFactory(
162
+ factory: EditorFactory | undefined,
163
+ ): EditorFactory | undefined {
164
+ return (factory as ZentuiEditorFactory | undefined)?.[
165
+ ZENTUI_EDITOR_BASE_FACTORY
166
+ ];
167
+ }
168
+
169
+ export function activeFooterReferences(config: ZentuiConfig): Set<string> {
170
+ const starship = config.components.footer.styles.starship;
171
+ const references = starship.format
172
+ ? collectFooterFormatReferences(
173
+ parseFooterFormat(starship.format),
174
+ FOOTER_FORMAT_ALIASES,
175
+ )
176
+ : new Set<string>([
177
+ ...(starship.segments.sessionName ? ["session_name"] : []),
178
+ ...(starship.segments.runtime ? ["runtime"] : []),
179
+ ...(starship.segments.gitCommit ? ["git_commit"] : []),
180
+ ...(starship.segments.gitMetrics ? ["git_metrics"] : []),
181
+ ...(starship.segments.packageVersion ? ["package"] : []),
182
+ ...(starship.segments.sessionDuration ? ["session_duration"] : []),
183
+ ...(starship.segments.time ? ["time"] : []),
184
+ ]);
185
+ if (starship.responsive) {
186
+ for (const name of collectFooterFormatReferences(
187
+ parseFooterFormat(starship.compactFormat),
188
+ FOOTER_FORMAT_ALIASES,
189
+ )) {
190
+ references.add(name);
191
+ }
192
+ }
193
+ return references;
194
+ }
195
+
196
+ function findRepositoryRoot(cwd: string): string | undefined {
197
+ let current = resolve(cwd);
198
+ while (true) {
199
+ if (existsSync(join(current, ".git"))) return current;
200
+ const parent = dirname(current);
201
+ if (parent === current) return undefined;
202
+ current = parent;
203
+ }
204
+ }
205
+
206
+ function isTuiContext(ctx: ExtensionContext): boolean {
207
+ try {
208
+ const mode = (ctx as ExtensionContext & { mode?: string }).mode;
209
+ return ctx.hasUI && (mode === undefined || mode === "tui");
210
+ } catch {
211
+ return false;
212
+ }
213
+ }
214
+
215
+ export type ShellRuntimeController = {
216
+ setEditorComponent: (
217
+ patch: Partial<EditorComponentConfig>,
218
+ ctx: ExtensionContext,
219
+ ) => { applied: boolean; reason?: string };
220
+ setUserMessagesComponent: (
221
+ patch: Partial<UserMessagesComponentConfig>,
222
+ ctx: ExtensionContext,
223
+ ) => void;
224
+ setWorkingLineComponent: (
225
+ patch: WorkingLineComponentPatch,
226
+ ctx: ExtensionContext,
227
+ ) => { applied: boolean; reason?: string };
228
+ setFooterComponent: (
229
+ patch: Partial<FooterComponentConfig>,
230
+ ctx: ExtensionContext,
231
+ ) => void;
232
+ };
233
+
234
+ export type ShellExtensionOptions = {
235
+ /** Expose the live shell reconciler to the unified settings panel. */
236
+ /** Keep legacy standalone registration disabled in the unified package. */
237
+ registerCommand?: boolean;
238
+ onRuntimeController?: (controller: ShellRuntimeController) => void;
239
+ };
240
+
241
+ export default function (
242
+ pi: ExtensionAPI,
243
+ options: ShellExtensionOptions = {},
244
+ ) {
245
+ const state: FooterState = createInitialState(emptyGitStatus());
246
+ const sessionLifecycle = new SessionLifecycle();
247
+ const editorOwnerToken = Symbol("zentui-editor-owner");
248
+
249
+ let currentConfig: PolishedTuiConfig = loadConfig();
250
+ // Keep the capability guard defensive for hosts with incomplete extension APIs.
251
+ if (typeof pi.registerEntryRenderer === "function") {
252
+ pi.registerEntryRenderer(TURN_SUMMARY_ENTRY_TYPE, (entry, options, theme) =>
253
+ renderTurnSummaryEntry(
254
+ entry,
255
+ {
256
+ ...options,
257
+ colorSource: currentConfig.components.workingLine.colorSource,
258
+ workingLineHigh: currentConfig.colors.workingLineHigh,
259
+ },
260
+ theme,
261
+ ),
262
+ );
263
+ }
264
+ let activeTheme: Theme | undefined;
265
+ let requestFooterRender: (() => void) | undefined;
266
+ let requestEditorRender: (() => void) | undefined;
267
+ let getActiveExtensionStatuses: () => ReadonlyMap<string, string> = () =>
268
+ new Map();
269
+ let stopRefreshInterval: StopProjectRefreshInterval = () => {};
270
+ let cleanupUserMessageStyle: () => void = () => {};
271
+ let userMessageStyleInstalled = false;
272
+ let cleanupSelectorBorderStyle: () => void = () => {};
273
+ let selectorBorderStyleInstalled = false;
274
+ let installedFooterKind: InstalledFooterKind | undefined;
275
+ let installedFooterToken: symbol | undefined;
276
+ let editorInstalled = false;
277
+ let editorInstallMode: EditorInstallMode = "none";
278
+ let installedEditorFactory: EditorFactory | undefined;
279
+ let wrappedEditorFactory: EditorFactory | undefined;
280
+ let stopSessionTimer: () => void = () => {};
281
+ let stopMinimalistDurationUpdates: () => void = () => {};
282
+ let minimalistDurationUpdatesActive = false;
283
+ let minimalistDecorationActive = false;
284
+ let sessionTimerRequirements = "";
285
+ let lastDurationLabel = "";
286
+ let lastProjectCwd: string | undefined;
287
+ let requestedProjectCwd: string | undefined;
288
+ const agentDurationClock = new AgentDurationClock();
289
+ const interactionMetrics = new InteractionMetricsTracker();
290
+ let agentRunActive = false;
291
+ let minimalistProjectRoot: string | undefined;
292
+ let projectRefreshActive = false;
293
+ let activeTuiContext: ExtensionContext | undefined;
294
+ let cleanupAccentRailLayoutPatch: () => void = () => {};
295
+ let accentRailLayoutPatchInstallSerial = 0;
296
+
297
+ const recordAccentRailLayoutPatchDiagnostic = (
298
+ diagnostic: AccentRailLayoutPatchDiagnostic,
299
+ version?: string,
300
+ ) => {
301
+ if (process.env.ZENTUI_DEBUG === "1") {
302
+ console.error(
303
+ `[zentui] Accent Rail fullscreen layout patch: ${diagnostic}${version ? ` (Pi TUI ${version})` : ""}`,
304
+ );
305
+ }
306
+ };
307
+
308
+ const isOwnedEditorFactory = (factory: EditorFactory | undefined) =>
309
+ (factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_OWNER] ===
310
+ editorOwnerToken;
311
+ const effectiveEditorEnabled = () =>
312
+ currentConfig.components.editor.enabled &&
313
+ !hasUnsupportedComponentStyle(currentConfig, "editor");
314
+ const effectiveUserMessagesEnabled = () =>
315
+ currentConfig.components.userMessages.enabled &&
316
+ !hasUnsupportedComponentStyle(currentConfig, "userMessages");
317
+ const effectiveSelectorBordersEnabled = () =>
318
+ currentConfig.components.selectorBorders.enabled &&
319
+ !hasUnsupportedComponentStyle(currentConfig, "selectorBorders");
320
+ const effectiveFooterStyle = () =>
321
+ hasUnsupportedComponentStyle(currentConfig, "footer")
322
+ ? ("native" as const)
323
+ : currentConfig.components.footer.style;
324
+
325
+ const ownsInstalledEditorFactory = () => {
326
+ if (
327
+ !sessionLifecycle.isCurrent() ||
328
+ !editorInstalled ||
329
+ !installedEditorFactory ||
330
+ !activeTuiContext
331
+ ) {
332
+ return false;
333
+ }
334
+ try {
335
+ return (
336
+ isOwnedEditorFactory(installedEditorFactory) &&
337
+ activeTuiContext.ui.getEditorComponent() === installedEditorFactory
338
+ );
339
+ } catch {
340
+ return false;
341
+ }
342
+ };
343
+
344
+ const refresh = () => {
345
+ if (!sessionLifecycle.isCurrent()) return;
346
+ requestFooterRender?.();
347
+ requestEditorRender?.();
348
+ };
349
+ const liveContext = new LiveContextController(sessionLifecycle, refresh);
350
+ const getActiveTheme = () => activeTheme;
351
+ const getCurrentConfig = () => currentConfig;
352
+ const workingLine = new WorkingLineController(
353
+ getCurrentConfig,
354
+ () => activeTheme as Theme,
355
+ agentDurationClock,
356
+ Math.random,
357
+ Date.now,
358
+ () => interactionMetrics.currentThought(),
359
+ );
360
+ const getContextWindow = (ctx: ExtensionContext): number | undefined =>
361
+ ctx.model?.contextWindow ?? ctx.getContextUsage()?.contextWindow;
362
+ const getContextPercent = (ctx: ExtensionContext): number | undefined => {
363
+ const usage = ctx.getContextUsage();
364
+ const contextWindow = getContextWindow(ctx);
365
+ const live = liveContext.get();
366
+ return live && contextWindow && contextWindow > 0
367
+ ? (live.tokens / contextWindow) * 100
368
+ : (usage?.percent ?? undefined);
369
+ };
370
+ const getAgentDurationMs = () => agentDurationClock.elapsedMs();
371
+ const getThinkingLevel = () =>
372
+ sessionLifecycle.isCurrent() ? pi.getThinkingLevel() : ("off" as const);
373
+ const syncFooterState = (ctx: ExtensionContext) =>
374
+ syncState(
375
+ state,
376
+ ctx,
377
+ currentConfig.icons.cacheHit,
378
+ resolveFooterTelemetry(ctx),
379
+ );
380
+ const ownsInstalledFooter = () =>
381
+ Boolean(
382
+ activeTuiContext &&
383
+ installedFooterToken &&
384
+ ctxFooterOwner(activeTuiContext) === installedFooterToken,
385
+ );
386
+ const installedFooterReferences = () =>
387
+ installedFooterKind === "starship" && ownsInstalledFooter()
388
+ ? activeFooterReferences(currentConfig)
389
+ : new Set<string>();
390
+
391
+ type ProjectRefreshTarget = { cwd: string; generation: number };
392
+ const refreshProjectState = async (
393
+ { cwd, generation }: ProjectRefreshTarget,
394
+ run: ProjectRefreshRun,
395
+ ) => {
396
+ if (!run.isCurrent() || !sessionLifecycle.isCurrent(generation)) return;
397
+ const starship = currentConfig.components.footer.styles.starship;
398
+ const gitCommitConfig = starship.gitCommit;
399
+ const gitMetricsConfig = starship.gitMetrics;
400
+ const references = installedFooterReferences();
401
+ const wantExactTag =
402
+ (references.has("git_commit") && gitCommitConfig.showTag) ||
403
+ references.has("git_tag");
404
+ const wantMetrics =
405
+ references.has("git_metrics") ||
406
+ references.has("git_added") ||
407
+ references.has("git_deleted");
408
+ const wantPackage =
409
+ references.has("package") || references.has("package_version");
410
+ const wantRuntime = references.has("runtime");
411
+ const [git, runtime, packageVersion] = await Promise.all([
412
+ readGitStatus(cwd, {
413
+ readExactTag: wantExactTag,
414
+ readMetrics: wantMetrics,
415
+ ignoreSubmodules: gitMetricsConfig.ignoreSubmodules,
416
+ }),
417
+ wantRuntime
418
+ ? readRuntimeInfo(cwd)
419
+ : Promise.resolve({ kind: "ok" as const, runtime: undefined }),
420
+ wantPackage
421
+ ? readPackageVersionResult(cwd)
422
+ : Promise.resolve({ kind: "ok" as const, result: null }),
423
+ ]);
424
+ if (
425
+ !run.isCurrent() ||
426
+ !sessionLifecycle.isCurrent(generation) ||
427
+ requestedProjectCwd !== cwd
428
+ ) {
429
+ return;
430
+ }
431
+ minimalistProjectRoot =
432
+ git.kind === "ok" ? findRepositoryRoot(cwd) : undefined;
433
+ lastProjectCwd = applyProjectRefreshToState(state, {
434
+ cwd,
435
+ previousCwd: lastProjectCwd,
436
+ git,
437
+ runtime,
438
+ packageVersion,
439
+ });
440
+ };
441
+
442
+ const projectRefreshScheduler = createProjectRefreshScheduler(
443
+ refreshProjectState,
444
+ refresh,
445
+ );
446
+ const scheduleProjectRefresh = (
447
+ ctx: ExtensionContext,
448
+ options?: ScheduleProjectRefreshOptions,
449
+ ) => {
450
+ const generation = sessionLifecycle.currentGeneration();
451
+ if (!sessionLifecycle.isCurrent(generation)) return;
452
+ const cwd = ctx.cwd;
453
+ requestedProjectCwd = cwd;
454
+ projectRefreshScheduler.schedule({ cwd, generation }, options);
455
+ };
456
+
457
+ const minimalistProjectRequired = () => {
458
+ const editor = currentConfig.components.editor;
459
+ const minimalist = editor.styles.minimalist;
460
+ return (
461
+ effectiveEditorEnabled() &&
462
+ ownsInstalledEditorFactory() &&
463
+ editor.style === "minimalist" &&
464
+ (minimalist.showGit || minimalist.pathDisplay === "project")
465
+ );
466
+ };
467
+
468
+ const needsProjectRefresh = () =>
469
+ (installedFooterKind === "starship" && ownsInstalledFooter()) ||
470
+ minimalistProjectRequired();
471
+
472
+ const stopProjectRefresh = () => {
473
+ stopRefreshInterval();
474
+ stopRefreshInterval = () => {};
475
+ projectRefreshScheduler.stop();
476
+ projectRefreshActive = false;
477
+ };
478
+
479
+ const reconcileProjectRefresh = (ctx: ExtensionContext, force = false) => {
480
+ if (!sessionLifecycle.isCurrent() || !needsProjectRefresh()) {
481
+ stopProjectRefresh();
482
+ return;
483
+ }
484
+ const activated = !projectRefreshActive;
485
+ if (activated) {
486
+ stopRefreshInterval = startProjectRefreshInterval(
487
+ currentConfig.projectRefreshIntervalMs,
488
+ () => {
489
+ if (editorInstalled && !ownsInstalledEditorFactory()) {
490
+ reconcileObservedEditorOwnership(ctx);
491
+ }
492
+ if (!needsProjectRefresh()) {
493
+ stopProjectRefresh();
494
+ return;
495
+ }
496
+ scheduleProjectRefresh(ctx);
497
+ },
498
+ );
499
+ projectRefreshActive = true;
500
+ }
501
+ if (force && !activated) projectRefreshScheduler.invalidate();
502
+ if (force || activated) scheduleProjectRefresh(ctx, { force: true });
503
+ };
504
+
505
+ const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
506
+ if (!sessionLifecycle.isCurrent() || !ctx.hasUI) return;
507
+ if (editorInstalled && !ownsInstalledEditorFactory())
508
+ reconcileObservedEditorOwnership(ctx);
509
+ syncFooterState(ctx);
510
+ if (project && needsProjectRefresh()) scheduleProjectRefresh(ctx);
511
+ refresh();
512
+ };
513
+
514
+ const reconcileSessionTimer = () => {
515
+ const references = installedFooterReferences();
516
+ const needsTime = references.has("time");
517
+ const needsDuration = references.has("session_duration");
518
+ const nextRequirements =
519
+ needsTime || needsDuration ? `${needsTime}:${needsDuration}` : "";
520
+ if (
521
+ !sessionLifecycle.isCurrent() ||
522
+ installedFooterKind !== "starship" ||
523
+ !ownsInstalledFooter() ||
524
+ !nextRequirements
525
+ ) {
526
+ stopSessionTimer();
527
+ sessionTimerRequirements = "";
528
+ lastDurationLabel = "";
529
+ return;
530
+ }
531
+ if (sessionTimerRequirements === nextRequirements) return;
532
+
533
+ stopSessionTimer();
534
+ sessionTimerRequirements = nextRequirements;
535
+ lastDurationLabel = "";
536
+ const timer = setInterval(() => {
537
+ if (!sessionLifecycle.isCurrent()) return;
538
+ if (needsTime) {
539
+ refresh();
540
+ return;
541
+ }
542
+ const label = state.sessionStartEpoch
543
+ ? buildSessionDurationLabel(state.sessionStartEpoch)
544
+ : "";
545
+ if (label === lastDurationLabel) return;
546
+ lastDurationLabel = label;
547
+ refresh();
548
+ }, 1000);
549
+ stopSessionTimer = () => {
550
+ clearInterval(timer);
551
+ sessionTimerRequirements = "";
552
+ stopSessionTimer = () => {};
553
+ };
554
+ };
555
+
556
+ const reconcileAgentTimer = () => {
557
+ const needed =
558
+ sessionLifecycle.isCurrent() &&
559
+ agentRunActive &&
560
+ agentDurationClock.isActive() &&
561
+ minimalistDecorationActive &&
562
+ effectiveEditorEnabled() &&
563
+ ownsInstalledEditorFactory() &&
564
+ currentConfig.components.editor.style === "minimalist" &&
565
+ currentConfig.components.editor.styles.minimalist.showTimer;
566
+ if (!needed) {
567
+ stopMinimalistDurationUpdates();
568
+ stopMinimalistDurationUpdates = () => {};
569
+ minimalistDurationUpdatesActive = false;
570
+ return;
571
+ }
572
+ if (minimalistDurationUpdatesActive) return;
573
+ minimalistDurationUpdatesActive = true;
574
+ stopMinimalistDurationUpdates = agentDurationClock.subscribe(() => {
575
+ const ctx = activeTuiContext;
576
+ if (ctx && editorInstalled && !ownsInstalledEditorFactory()) {
577
+ reconcileObservedEditorOwnership(ctx);
578
+ }
579
+ reconcileAgentTimer();
580
+ if (minimalistDurationUpdatesActive) refresh();
581
+ });
582
+ };
583
+
584
+ const setMinimalistDecorationActive = (active: boolean) => {
585
+ const next =
586
+ sessionLifecycle.isCurrent() && active && ownsInstalledEditorFactory();
587
+ if (minimalistDecorationActive === next) return;
588
+ minimalistDecorationActive = next;
589
+ reconcileAgentTimer();
590
+ };
591
+
592
+ const startAgentTurn = (interactionStarted: boolean) => {
593
+ agentRunActive = true;
594
+ if (interactionStarted) agentDurationClock.start();
595
+ reconcileAgentTimer();
596
+ refresh();
597
+ };
598
+
599
+ const pauseAgentRun = () => {
600
+ agentRunActive = false;
601
+ reconcileAgentTimer();
602
+ refresh();
603
+ };
604
+
605
+ const settleAgentTurn = (nextStartedAt?: number) => {
606
+ agentRunActive = nextStartedAt !== undefined;
607
+ if (nextStartedAt === undefined) agentDurationClock.finish();
608
+ else agentDurationClock.start(nextStartedAt);
609
+ reconcileAgentTimer();
610
+ refresh();
611
+ };
612
+
613
+ const resetAgentTimer = () => {
614
+ stopMinimalistDurationUpdates();
615
+ stopMinimalistDurationUpdates = () => {};
616
+ minimalistDurationUpdatesActive = false;
617
+ agentRunActive = false;
618
+ agentDurationClock.reset();
619
+ };
620
+
621
+ const sameReferences = (left: Set<string>, right: Set<string>) =>
622
+ left.size === right.size && [...left].every((name) => right.has(name));
623
+
624
+ const applyFooterDependencyConfigChange = (
625
+ ctx: ExtensionContext,
626
+ save: () => PolishedTuiConfig,
627
+ ) => {
628
+ const before = activeFooterReferences(currentConfig);
629
+ const nextConfig = save();
630
+ const after = activeFooterReferences(nextConfig);
631
+ currentConfig = nextConfig;
632
+ if (sameReferences(before, after)) return;
633
+ reconcileSessionTimer();
634
+ reconcileProjectRefresh(ctx, true);
635
+ };
636
+
637
+ const installUserMessages = () => {
638
+ if (userMessageStyleInstalled) return;
639
+ let cleanup: (() => void) | undefined;
640
+ try {
641
+ cleanup = installUserMessageStyle(getActiveTheme, getCurrentConfig);
642
+ cleanupUserMessageStyle = cleanup;
643
+ userMessageStyleInstalled = true;
644
+ } catch {
645
+ try {
646
+ cleanup?.();
647
+ } catch {
648
+ // Best effort: the installer is locally transactional.
649
+ }
650
+ cleanupUserMessageStyle = () => {};
651
+ userMessageStyleInstalled = false;
652
+ }
653
+ };
654
+
655
+ const uninstallUserMessages = () => {
656
+ try {
657
+ cleanupUserMessageStyle();
658
+ } catch {
659
+ // Best effort cleanup.
660
+ } finally {
661
+ cleanupUserMessageStyle = () => {};
662
+ userMessageStyleInstalled = false;
663
+ }
664
+ };
665
+
666
+ const reconcileUserMessages = () => {
667
+ if (effectiveUserMessagesEnabled()) installUserMessages();
668
+ else uninstallUserMessages();
669
+ };
670
+
671
+ const installSelectorBorders = () => {
672
+ if (selectorBorderStyleInstalled) return;
673
+ let cleanup: (() => void) | undefined;
674
+ try {
675
+ cleanup = installSelectorBorderStyle(getActiveTheme, getCurrentConfig);
676
+ cleanupSelectorBorderStyle = cleanup;
677
+ selectorBorderStyleInstalled = true;
678
+ } catch {
679
+ try {
680
+ cleanup?.();
681
+ } catch {
682
+ // Best effort: the installer is locally transactional.
683
+ }
684
+ cleanupSelectorBorderStyle = () => {};
685
+ selectorBorderStyleInstalled = false;
686
+ }
687
+ };
688
+
689
+ const uninstallSelectorBorders = () => {
690
+ try {
691
+ cleanupSelectorBorderStyle();
692
+ } catch {
693
+ // Best effort cleanup.
694
+ } finally {
695
+ cleanupSelectorBorderStyle = () => {};
696
+ selectorBorderStyleInstalled = false;
697
+ }
698
+ };
699
+
700
+ const reconcileSelectorBorders = () => {
701
+ const selectors = currentConfig.components.selectorBorders;
702
+ if (effectiveSelectorBordersEnabled() && selectors.style === "zentui") {
703
+ installSelectorBorders();
704
+ } else uninstallSelectorBorders();
705
+ };
706
+
707
+ const clearEditorOwnership = () => {
708
+ setMinimalistDecorationActive(false);
709
+ requestEditorRender = undefined;
710
+ wrappedEditorFactory = undefined;
711
+ installedEditorFactory = undefined;
712
+ editorInstallMode = "none";
713
+ editorInstalled = false;
714
+ };
715
+
716
+ const trackZentuiEditorFactory = (factory: EditorFactory): boolean => {
717
+ if (!isOwnedEditorFactory(factory)) return false;
718
+ const baseFactory = getZentuiEditorBaseFactory(factory);
719
+ wrappedEditorFactory = baseFactory;
720
+ installedEditorFactory = factory;
721
+ editorInstallMode = baseFactory ? "wrapper" : "standalone";
722
+ editorInstalled = true;
723
+ return true;
724
+ };
725
+
726
+ const observeEditorFactory = (
727
+ ctx: ExtensionContext,
728
+ ): { known: true; factory: EditorFactory | undefined } | { known: false } => {
729
+ try {
730
+ return { known: true, factory: ctx.ui.getEditorComponent() };
731
+ } catch {
732
+ return { known: false };
733
+ }
734
+ };
735
+
736
+ const reconcileObservedEditorOwnership = (ctx: ExtensionContext) => {
737
+ const observed = observeEditorFactory(ctx);
738
+ if (!observed.known) return observed;
739
+ if (observed.factory && isOwnedEditorFactory(observed.factory)) {
740
+ trackZentuiEditorFactory(observed.factory);
741
+ } else {
742
+ clearEditorOwnership();
743
+ reconcileProjectRefresh(ctx);
744
+ }
745
+ return observed;
746
+ };
747
+
748
+ const accentRailLayoutActive = () =>
749
+ sessionLifecycle.isCurrent() &&
750
+ ownsInstalledEditorFactory() &&
751
+ effectiveEditorEnabled() &&
752
+ currentConfig.components.editor.style === "accent-rail";
753
+
754
+ const markOwnedAccentRailEditor = <T extends object>(editor: T): T => {
755
+ markAccentRailLayoutEditor(
756
+ editor,
757
+ editorOwnerToken,
758
+ accentRailLayoutActive,
759
+ );
760
+ return editor;
761
+ };
762
+
763
+ const makeEditorFactory = (ctx: ExtensionContext): ZentuiEditorFactory => {
764
+ const sessionTheme = ctx.ui.theme;
765
+ const factory = ((
766
+ tui: TUI,
767
+ theme: EditorTheme,
768
+ keybindings: KeybindingsManager,
769
+ ) => {
770
+ requestEditorRender = () => tui.requestRender();
771
+ return markOwnedAccentRailEditor(
772
+ new PolishedEditor(
773
+ tui,
774
+ theme,
775
+ keybindings,
776
+ sessionTheme,
777
+ getCurrentConfig,
778
+ () => ({
779
+ modelLabel: modelLabelFor(
780
+ state,
781
+ currentConfig.components.editor.modelLabel,
782
+ ),
783
+ modelId: state.modelId,
784
+ modelName: state.modelName,
785
+ providerLabel: state.providerLabel,
786
+ sessionName: ctx.sessionManager.getSessionName() ?? "",
787
+ }),
788
+ getThinkingLevel,
789
+ () => ({
790
+ cwd: ctx.cwd,
791
+ projectRoot: minimalistProjectRoot,
792
+ branch: state.branch,
793
+ dirty: state.dirty,
794
+ ahead: state.ahead,
795
+ behind: state.behind,
796
+ costLabel: state.costLabel,
797
+ modelLabel: modelLabelFor(
798
+ state,
799
+ currentConfig.components.editor.modelLabel,
800
+ ),
801
+ thinkingLevel: getThinkingLevel(),
802
+ contextPercent: getContextPercent(ctx),
803
+ contextWindow: getContextWindow(ctx),
804
+ sessionName: ctx.sessionManager.getSessionName() ?? "",
805
+ agentDurationMs: getAgentDurationMs(),
806
+ agentActive: agentRunActive,
807
+ }),
808
+ setMinimalistDecorationActive,
809
+ ),
810
+ );
811
+ }) as ZentuiEditorFactory;
812
+ factory[ZENTUI_EDITOR_FACTORY] = true;
813
+ factory[ZENTUI_EDITOR_OWNER] = editorOwnerToken;
814
+ return factory;
815
+ };
816
+
817
+ const makeWrappedEditorFactory = (
818
+ ctx: ExtensionContext,
819
+ baseFactory: EditorFactory,
820
+ ): ZentuiEditorFactory => {
821
+ const sessionTheme = ctx.ui.theme;
822
+ const factory = ((
823
+ tui: TUI,
824
+ theme: EditorTheme,
825
+ keybindings: KeybindingsManager,
826
+ ) => {
827
+ requestEditorRender = () => tui.requestRender();
828
+ return markOwnedAccentRailEditor(
829
+ new WrappedPolishedEditor(
830
+ baseFactory(tui, theme, keybindings),
831
+ sessionTheme,
832
+ getCurrentConfig,
833
+ () => ({
834
+ modelLabel: modelLabelFor(
835
+ state,
836
+ currentConfig.components.editor.modelLabel,
837
+ ),
838
+ modelId: state.modelId,
839
+ modelName: state.modelName,
840
+ providerLabel: state.providerLabel,
841
+ sessionName: ctx.sessionManager.getSessionName() ?? "",
842
+ }),
843
+ getThinkingLevel,
844
+ () => ({
845
+ cwd: ctx.cwd,
846
+ projectRoot: minimalistProjectRoot,
847
+ branch: state.branch,
848
+ dirty: state.dirty,
849
+ ahead: state.ahead,
850
+ behind: state.behind,
851
+ costLabel: state.costLabel,
852
+ modelLabel: modelLabelFor(
853
+ state,
854
+ currentConfig.components.editor.modelLabel,
855
+ ),
856
+ thinkingLevel: getThinkingLevel(),
857
+ contextPercent: getContextPercent(ctx),
858
+ contextWindow: getContextWindow(ctx),
859
+ sessionName: ctx.sessionManager.getSessionName() ?? "",
860
+ agentDurationMs: getAgentDurationMs(),
861
+ agentActive: agentRunActive,
862
+ }),
863
+ setMinimalistDecorationActive,
864
+ ),
865
+ );
866
+ }) as ZentuiEditorFactory;
867
+ factory[ZENTUI_EDITOR_FACTORY] = true;
868
+ factory[ZENTUI_EDITOR_OWNER] = editorOwnerToken;
869
+ factory[ZENTUI_EDITOR_BASE_FACTORY] = baseFactory;
870
+ return factory;
871
+ };
872
+
873
+ const replaceEditor = (
874
+ ctx: ExtensionContext,
875
+ factory: EditorFactory | undefined,
876
+ ): EditorChangeResult => {
877
+ const result = replaceEditorComponentWithExpandedText(ctx.ui, factory);
878
+ return result.ok
879
+ ? result
880
+ : { ok: false, reason: editorTransferFailureMessage(result.reason) };
881
+ };
882
+
883
+ const installEditor = (ctx: ExtensionContext): EditorChangeResult => {
884
+ const currentFactory = ctx.ui.getEditorComponent();
885
+ if (currentFactory && currentFactory === installedEditorFactory) {
886
+ editorInstalled = true;
887
+ return { ok: true };
888
+ }
889
+
890
+ const currentZentuiBaseFactory = getZentuiEditorBaseFactory(currentFactory);
891
+ const baseFactory =
892
+ currentZentuiBaseFactory ??
893
+ (currentFactory && !isZentuiEditorFactory(currentFactory)
894
+ ? currentFactory
895
+ : undefined);
896
+ const nextFactory = baseFactory
897
+ ? makeWrappedEditorFactory(ctx, baseFactory)
898
+ : makeEditorFactory(ctx);
899
+ const replacement = replaceEditor(ctx, nextFactory);
900
+ if (!replacement.ok) return replacement;
901
+
902
+ trackZentuiEditorFactory(nextFactory);
903
+ return { ok: true };
904
+ };
905
+
906
+ const uninstallEditor = (
907
+ ctx: ExtensionContext,
908
+ options: { allowStaleZentui?: boolean } = {},
909
+ ): EditorChangeResult => {
910
+ const observed = observeEditorFactory(ctx);
911
+ if (!observed.known) {
912
+ return {
913
+ ok: false,
914
+ reason:
915
+ "the current editor factory could not be observed safely; reload Pi to apply this change",
916
+ };
917
+ }
918
+ const currentFactory = observed.factory;
919
+ if (!currentFactory || !isZentuiEditorFactory(currentFactory)) {
920
+ clearEditorOwnership();
921
+ return { ok: true };
922
+ }
923
+ if (!isOwnedEditorFactory(currentFactory) && !options.allowStaleZentui) {
924
+ clearEditorOwnership();
925
+ return { ok: true };
926
+ }
927
+
928
+ const replacement = replaceEditor(
929
+ ctx,
930
+ getZentuiEditorBaseFactory(currentFactory) ??
931
+ (editorInstallMode === "wrapper" && wrappedEditorFactory
932
+ ? wrappedEditorFactory
933
+ : undefined),
934
+ );
935
+ if (!replacement.ok) return replacement;
936
+
937
+ clearEditorOwnership();
938
+ return { ok: true };
939
+ };
940
+
941
+ const ctxFooterOwner = (ctx: ExtensionContext): unknown =>
942
+ (ctx.ui as unknown as Record<PropertyKey, unknown>)[ZENTUI_FOOTER_OWNER];
943
+
944
+ const setStatusLineOwnership = (
945
+ ctx: ExtensionContext,
946
+ token: symbol | undefined,
947
+ ) => {
948
+ const ui = ctx.ui as unknown as Record<PropertyKey, unknown>;
949
+ try {
950
+ if (token) ui[ZENTUI_FOOTER_OWNER] = token;
951
+ else delete ui[ZENTUI_FOOTER_OWNER];
952
+ } catch {
953
+ // Failure to mark ownership intentionally prevents Native from restoring it.
954
+ }
955
+ };
956
+
957
+ const ownsStatusLine = (ctx: ExtensionContext) =>
958
+ installedFooterToken !== undefined &&
959
+ ctxFooterOwner(ctx) === installedFooterToken;
960
+
961
+ const clearFooterOwnership = (ctx: ExtensionContext, token: symbol) => {
962
+ if (installedFooterToken !== token) return;
963
+ installedFooterKind = undefined;
964
+ installedFooterToken = undefined;
965
+ if (ctxFooterOwner(ctx) === token) setStatusLineOwnership(ctx, undefined);
966
+ requestFooterRender = undefined;
967
+ getActiveExtensionStatuses = () => new Map();
968
+ stopSessionTimer();
969
+ if (sessionLifecycle.isCurrent()) reconcileProjectRefresh(ctx, true);
970
+ };
971
+
972
+ type FooterBookkeepingSnapshot = {
973
+ token: symbol | undefined;
974
+ requestRender: (() => void) | undefined;
975
+ getExtensionStatuses: () => ReadonlyMap<string, string>;
976
+ };
977
+
978
+ const snapshotFooterBookkeeping = (
979
+ ctx: ExtensionContext,
980
+ ): FooterBookkeepingSnapshot => ({
981
+ token: ownsStatusLine(ctx) ? installedFooterToken : undefined,
982
+ requestRender: requestFooterRender,
983
+ getExtensionStatuses: getActiveExtensionStatuses,
984
+ });
985
+
986
+ const resetFailedFooterInstallation = (
987
+ ctx: ExtensionContext,
988
+ token: symbol,
989
+ previous: FooterBookkeepingSnapshot,
990
+ ) => {
991
+ // Pi has no transactional Footer replacement API. If a live replacement
992
+ // fails while retaining our predecessor, preserve that Footer's callbacks
993
+ // and timer. Otherwise clear only local bookkeeping; never issue a
994
+ // destructive setFooter(undefined) rollback.
995
+ if (
996
+ previous.token !== undefined &&
997
+ installedFooterToken === previous.token &&
998
+ ctxFooterOwner(ctx) === previous.token
999
+ ) {
1000
+ requestFooterRender = previous.requestRender;
1001
+ getActiveExtensionStatuses = previous.getExtensionStatuses;
1002
+ return;
1003
+ }
1004
+ clearFooterOwnership(ctx, token);
1005
+ requestFooterRender = undefined;
1006
+ getActiveExtensionStatuses = () => new Map();
1007
+ stopSessionTimer();
1008
+ };
1009
+
1010
+ const installStatusLine = (ctx: ExtensionContext) => {
1011
+ if (installedFooterKind === "starship" && ownsStatusLine(ctx)) return;
1012
+ const token = Symbol("zentui-starship-footer");
1013
+ const previous = snapshotFooterBookkeeping(ctx);
1014
+ try {
1015
+ installFooter(ctx, state, getCurrentConfig, {
1016
+ setRequestRender: (fn) => {
1017
+ requestFooterRender = fn;
1018
+ },
1019
+ scheduleProjectRefresh,
1020
+ setExtensionStatusesGetter(fn) {
1021
+ getActiveExtensionStatuses = fn ?? (() => new Map());
1022
+ },
1023
+ getLiveContext: () => liveContext.get(),
1024
+ onDispose: () => clearFooterOwnership(ctx, token),
1025
+ });
1026
+ installedFooterKind = "starship";
1027
+ installedFooterToken = token;
1028
+ setStatusLineOwnership(ctx, token);
1029
+ refresh();
1030
+ reconcileSessionTimer();
1031
+ } catch {
1032
+ resetFailedFooterInstallation(ctx, token, previous);
1033
+ }
1034
+ };
1035
+
1036
+ const installHiddenStatusLine = (ctx: ExtensionContext) => {
1037
+ if (installedFooterKind === "hidden" && ownsStatusLine(ctx)) return;
1038
+ const token = Symbol("zentui-hidden-footer");
1039
+ const previous = snapshotFooterBookkeeping(ctx);
1040
+ try {
1041
+ installHiddenFooter(ctx, () => clearFooterOwnership(ctx, token));
1042
+ installedFooterKind = "hidden";
1043
+ installedFooterToken = token;
1044
+ setStatusLineOwnership(ctx, token);
1045
+ requestFooterRender = undefined;
1046
+ getActiveExtensionStatuses = () => new Map();
1047
+ stopSessionTimer();
1048
+ } catch {
1049
+ resetFailedFooterInstallation(ctx, token, previous);
1050
+ }
1051
+ };
1052
+
1053
+ const uninstallStatusLine = (
1054
+ ctx: ExtensionContext,
1055
+ options: { forceLocalCleanup?: boolean } = {},
1056
+ ) => {
1057
+ const ownedToken = ownsStatusLine(ctx) ? installedFooterToken : undefined;
1058
+ if (!ownedToken) return;
1059
+ try {
1060
+ ctx.ui.setFooter(undefined);
1061
+ } catch {
1062
+ // A live transition must preserve an owned Footer that Pi retained.
1063
+ // Shutdown cannot keep local callbacks alive, so it clears bookkeeping.
1064
+ if (!options.forceLocalCleanup) return;
1065
+ }
1066
+ clearFooterOwnership(ctx, ownedToken);
1067
+ };
1068
+
1069
+ const reconcileFooter = (ctx: ExtensionContext) => {
1070
+ switch (effectiveFooterStyle()) {
1071
+ case "starship":
1072
+ installStatusLine(ctx);
1073
+ break;
1074
+ case "hidden":
1075
+ installHiddenStatusLine(ctx);
1076
+ break;
1077
+ case "native":
1078
+ uninstallStatusLine(ctx);
1079
+ break;
1080
+ }
1081
+ };
1082
+
1083
+ const reconcileEditor = (
1084
+ ctx: ExtensionContext,
1085
+ options: { allowStaleZentui?: boolean } = {},
1086
+ ): EditorChangeResult | undefined => {
1087
+ try {
1088
+ if (effectiveEditorEnabled()) {
1089
+ const currentFactory = ctx.ui.getEditorComponent();
1090
+ if (
1091
+ isZentuiEditorFactory(currentFactory) &&
1092
+ !isOwnedEditorFactory(currentFactory) &&
1093
+ !options.allowStaleZentui
1094
+ ) {
1095
+ clearEditorOwnership();
1096
+ return;
1097
+ }
1098
+ const editorMissingOrReplaced =
1099
+ !editorInstalled || !isOwnedEditorFactory(currentFactory);
1100
+ if (editorMissingOrReplaced) return installEditor(ctx);
1101
+ } else {
1102
+ const currentFactory = ctx.ui.getEditorComponent();
1103
+ if (editorInstalled || isOwnedEditorFactory(currentFactory))
1104
+ return uninstallEditor(ctx);
1105
+ }
1106
+ } catch {
1107
+ return {
1108
+ ok: false,
1109
+ reason:
1110
+ "the editor could not be reconciled safely; reload Pi to apply this change",
1111
+ };
1112
+ }
1113
+ };
1114
+
1115
+ const applyConfiguredUi = (
1116
+ ctx: ExtensionContext,
1117
+ options: { allowStaleZentui?: boolean } = {},
1118
+ ): ApplyUiResult => {
1119
+ const result: ApplyUiResult = { editorBlocked: false };
1120
+ if (!isTuiContext(ctx)) return result;
1121
+ activeTheme = ctx.ui.theme;
1122
+
1123
+ const editorChange = reconcileEditor(ctx, options);
1124
+ if (editorChange && !editorChange.ok) {
1125
+ result.editorBlocked = true;
1126
+ result.editorReason = editorChange.reason;
1127
+ }
1128
+ reconcileUserMessages();
1129
+ reconcileSelectorBorders();
1130
+ reconcileFooter(ctx);
1131
+ reconcileProjectRefresh(ctx);
1132
+ reconcileSessionTimer();
1133
+ reconcileAgentTimer();
1134
+ return result;
1135
+ };
1136
+
1137
+ const installUi = (ctx: ExtensionContext) => {
1138
+ if (!isTuiContext(ctx)) return;
1139
+ activeTuiContext = ctx;
1140
+ activeTheme = ctx.ui.theme;
1141
+ const staleFooterOwner = ctxFooterOwner(ctx);
1142
+ if (typeof staleFooterOwner === "symbol")
1143
+ installedFooterToken = staleFooterOwner;
1144
+ ensureConfigExists();
1145
+ currentConfig = loadConfig();
1146
+ syncFooterState(ctx);
1147
+ stopProjectRefresh();
1148
+
1149
+ uninstallUserMessages();
1150
+ uninstallSelectorBorders();
1151
+ try {
1152
+ removeUserMessageStyle();
1153
+ } catch {
1154
+ // Startup alone may supersede a stale registration from an earlier reload.
1155
+ }
1156
+ try {
1157
+ removeSelectorBorderStyle();
1158
+ } catch {
1159
+ // Startup alone may supersede a stale registration from an earlier reload.
1160
+ }
1161
+ uninstallStatusLine(ctx);
1162
+ if (effectiveEditorEnabled()) clearEditorOwnership();
1163
+ else {
1164
+ try {
1165
+ uninstallEditor(ctx, { allowStaleZentui: true });
1166
+ } catch {
1167
+ // Reconciliation below retries observable stale ownership.
1168
+ }
1169
+ }
1170
+
1171
+ applyConfiguredUi(ctx, { allowStaleZentui: true });
1172
+ refresh();
1173
+ };
1174
+
1175
+ const scheduleEditorReconciliation = (ctx: ExtensionContext) => {
1176
+ sessionLifecycle.defer(() => {
1177
+ if (!isTuiContext(ctx) || !effectiveEditorEnabled()) return;
1178
+ const observed = observeEditorFactory(ctx);
1179
+ if (!observed.known) return;
1180
+ if (observed.factory === installedEditorFactory) {
1181
+ reconcileProjectRefresh(ctx);
1182
+ return;
1183
+ }
1184
+ if (!observed.factory || !isOwnedEditorFactory(observed.factory)) {
1185
+ clearEditorOwnership();
1186
+ reconcileProjectRefresh(ctx);
1187
+ refresh();
1188
+ return;
1189
+ }
1190
+ trackZentuiEditorFactory(observed.factory);
1191
+ reconcileProjectRefresh(ctx);
1192
+ refresh();
1193
+ });
1194
+ };
1195
+
1196
+ const cleanupUi = (ctx?: ExtensionContext) => {
1197
+ if (!ctx || !sessionLifecycle.isCurrent()) return;
1198
+ sessionLifecycle.shutdown();
1199
+ stopSessionTimer();
1200
+ resetAgentTimer();
1201
+ stopProjectRefresh();
1202
+
1203
+ let retainedEditorOwnership = false;
1204
+ if (isTuiContext(ctx)) {
1205
+ uninstallStatusLine(ctx, { forceLocalCleanup: true });
1206
+ try {
1207
+ const before = observeEditorFactory(ctx);
1208
+ if (before.known) {
1209
+ const currentFactory = before.factory;
1210
+ if (currentFactory && isOwnedEditorFactory(currentFactory)) {
1211
+ replaceEditor(
1212
+ ctx,
1213
+ getZentuiEditorBaseFactory(currentFactory) ??
1214
+ (editorInstallMode === "wrapper" && wrappedEditorFactory
1215
+ ? wrappedEditorFactory
1216
+ : undefined),
1217
+ );
1218
+ }
1219
+ }
1220
+ const after = observeEditorFactory(ctx);
1221
+ if (
1222
+ after.known &&
1223
+ after.factory &&
1224
+ isOwnedEditorFactory(after.factory)
1225
+ ) {
1226
+ trackZentuiEditorFactory(after.factory);
1227
+ retainedEditorOwnership = true;
1228
+ }
1229
+ } catch {
1230
+ // Continue cleaning independent surfaces.
1231
+ }
1232
+ }
1233
+ if (!retainedEditorOwnership) clearEditorOwnership();
1234
+ accentRailLayoutPatchInstallSerial += 1;
1235
+ cleanupAccentRailLayoutPatch();
1236
+ cleanupAccentRailLayoutPatch = () => {};
1237
+ uninstallUserMessages();
1238
+ uninstallSelectorBorders();
1239
+ installedFooterKind = undefined;
1240
+ installedFooterToken = undefined;
1241
+ requestFooterRender = undefined;
1242
+ requestEditorRender = undefined;
1243
+ getActiveExtensionStatuses = () => new Map();
1244
+ activeTheme = undefined;
1245
+ activeTuiContext = undefined;
1246
+ };
1247
+
1248
+ const syncInteractiveState = (_event: unknown, ctx: ExtensionContext) => {
1249
+ refreshInteractiveState(ctx);
1250
+ };
1251
+ const syncInteractiveAndProjectState = (
1252
+ _event: unknown,
1253
+ ctx: ExtensionContext,
1254
+ ) => {
1255
+ refreshInteractiveState(ctx, true);
1256
+ };
1257
+
1258
+ pi.on("session_start", async (_event, ctx) => {
1259
+ const lifecycleGeneration = sessionLifecycle.start();
1260
+ const layoutInstallSerial = ++accentRailLayoutPatchInstallSerial;
1261
+ cleanupAccentRailLayoutPatch();
1262
+ cleanupAccentRailLayoutPatch = () => {};
1263
+ if (isTuiContext(ctx)) {
1264
+ const layoutPatchRetention =
1265
+ await retainAccentRailLayoutPatchInstallation(
1266
+ () => installHostAccentRailLayoutPatch(editorOwnerToken),
1267
+ () =>
1268
+ sessionLifecycle.isCurrent(lifecycleGeneration) &&
1269
+ layoutInstallSerial === accentRailLayoutPatchInstallSerial,
1270
+ (layoutPatch) => {
1271
+ cleanupAccentRailLayoutPatch = layoutPatch.cleanup;
1272
+ recordAccentRailLayoutPatchDiagnostic(
1273
+ layoutPatch.diagnostic,
1274
+ layoutPatch.version,
1275
+ );
1276
+ },
1277
+ );
1278
+ if (layoutPatchRetention === "stale") return;
1279
+ if (layoutPatchRetention === "failed") {
1280
+ recordAccentRailLayoutPatchDiagnostic("host-module-unavailable");
1281
+ }
1282
+ }
1283
+ if (!sessionLifecycle.isCurrent(lifecycleGeneration)) return;
1284
+ liveContext.clear();
1285
+ interactionMetrics.shutdown();
1286
+ state.sessionStartEpoch = Date.now();
1287
+ invalidateUsageTotalsCache();
1288
+ resetAgentTimer();
1289
+ lastProjectCwd = undefined;
1290
+ requestedProjectCwd = undefined;
1291
+ minimalistProjectRoot = undefined;
1292
+ installUi(ctx);
1293
+ workingLine.startSession(ctx);
1294
+ scheduleEditorReconciliation(ctx);
1295
+ });
1296
+
1297
+ const setEditorComponent = (
1298
+ patch: Partial<EditorComponentConfig>,
1299
+ ctx: ExtensionContext,
1300
+ ) => {
1301
+ currentConfig = saveEditorComponentPatch(patch);
1302
+ let result: EditorChangeResult | undefined;
1303
+ if (patch.enabled !== undefined && isTuiContext(ctx))
1304
+ result = reconcileEditor(ctx);
1305
+ if (patch.style !== undefined && patch.style !== "minimalist") {
1306
+ setMinimalistDecorationActive(false);
1307
+ }
1308
+ if (patch.modelLabel !== undefined) syncFooterState(ctx);
1309
+ reconcileProjectRefresh(ctx);
1310
+ reconcileAgentTimer();
1311
+ refresh();
1312
+ return {
1313
+ applied: !result || result.ok,
1314
+ reason: result && !result.ok ? result.reason : undefined,
1315
+ };
1316
+ };
1317
+ const setUserMessagesComponent = (
1318
+ patch: Partial<UserMessagesComponentConfig>,
1319
+ _ctx: ExtensionContext,
1320
+ ) => {
1321
+ currentConfig = saveUserMessagesComponentPatch(patch);
1322
+ if (patch.enabled !== undefined || patch.style !== undefined)
1323
+ reconcileUserMessages();
1324
+ refresh();
1325
+ };
1326
+ const setWorkingLineComponent = (
1327
+ patch: WorkingLineComponentPatch,
1328
+ ctx: ExtensionContext,
1329
+ ) => {
1330
+ currentConfig = saveWorkingLineComponentPatch(patch);
1331
+ return workingLine.reconcile(ctx);
1332
+ };
1333
+ const setFooterComponent = (
1334
+ patch: Partial<FooterComponentConfig>,
1335
+ ctx: ExtensionContext,
1336
+ ) => {
1337
+ const previousStyle = effectiveFooterStyle();
1338
+ currentConfig = saveFooterComponentPatch(patch);
1339
+ const styleChanged = effectiveFooterStyle() !== previousStyle;
1340
+ if (patch.style !== undefined) reconcileFooter(ctx);
1341
+ if (patch.modelLabel !== undefined) syncFooterState(ctx);
1342
+ reconcileProjectRefresh(ctx, styleChanged);
1343
+ reconcileSessionTimer();
1344
+ refresh();
1345
+ };
1346
+ options.onRuntimeController?.({
1347
+ setEditorComponent,
1348
+ setUserMessagesComponent,
1349
+ setWorkingLineComponent,
1350
+ setFooterComponent,
1351
+ });
1352
+
1353
+ if (options.registerCommand !== false)
1354
+ registerShellSettingsCommand(pi, {
1355
+ sessionLifecycle,
1356
+ getConfig: getCurrentConfig,
1357
+ setEditorComponent(
1358
+ patch: Partial<EditorComponentConfig>,
1359
+ ctx: ExtensionContext,
1360
+ ) {
1361
+ currentConfig = saveEditorComponentPatch(patch);
1362
+ let result: EditorChangeResult | undefined;
1363
+ if (patch.enabled !== undefined && isTuiContext(ctx)) {
1364
+ result = reconcileEditor(ctx);
1365
+ }
1366
+ if (patch.style !== undefined && patch.style !== "minimalist") {
1367
+ setMinimalistDecorationActive(false);
1368
+ }
1369
+ if (patch.modelLabel !== undefined) syncFooterState(ctx);
1370
+ reconcileProjectRefresh(ctx);
1371
+ reconcileAgentTimer();
1372
+ refresh();
1373
+ return {
1374
+ applied: !result || result.ok,
1375
+ reason: result && !result.ok ? result.reason : undefined,
1376
+ };
1377
+ },
1378
+ setPolished(
1379
+ patch: Partial<PolishedEditorStyleConfig>,
1380
+ _ctx: ExtensionContext,
1381
+ ) {
1382
+ currentConfig = savePolishedEditorStylePatch(patch);
1383
+ refresh();
1384
+ },
1385
+ setPolishedCopyFriendly(
1386
+ patch: Partial<PolishedCopyFriendlyEditorStyleConfig>,
1387
+ _ctx: ExtensionContext,
1388
+ ) {
1389
+ currentConfig = savePolishedCopyFriendlyEditorStylePatch(patch);
1390
+ refresh();
1391
+ },
1392
+ setAccentRail(
1393
+ patch: Partial<AccentRailEditorStyleConfig>,
1394
+ _ctx: ExtensionContext,
1395
+ ) {
1396
+ currentConfig = saveAccentRailEditorStylePatch(patch);
1397
+ refresh();
1398
+ },
1399
+ setMinimalist(patch: Partial<MinimalistConfig>, ctx: ExtensionContext) {
1400
+ currentConfig = saveMinimalistEditorStylePatch(patch);
1401
+ reconcileAgentTimer();
1402
+ reconcileProjectRefresh(
1403
+ ctx,
1404
+ patch.pathDisplay !== undefined || patch.showGit !== undefined,
1405
+ );
1406
+ refresh();
1407
+ },
1408
+ setUserMessagesComponent(
1409
+ patch: Partial<UserMessagesComponentConfig>,
1410
+ _ctx: ExtensionContext,
1411
+ ) {
1412
+ currentConfig = saveUserMessagesComponentPatch(patch);
1413
+ if (patch.enabled !== undefined || patch.style !== undefined)
1414
+ reconcileUserMessages();
1415
+ refresh();
1416
+ },
1417
+ setWorkingLineComponent(
1418
+ patch: WorkingLineComponentPatch,
1419
+ ctx: ExtensionContext,
1420
+ ) {
1421
+ currentConfig = saveWorkingLineComponentPatch(patch);
1422
+ return workingLine.reconcile(ctx);
1423
+ },
1424
+ setSelectorBordersComponent(
1425
+ patch: Partial<SelectorBordersComponentConfig>,
1426
+ _ctx: ExtensionContext,
1427
+ ) {
1428
+ currentConfig = saveSelectorBordersComponentPatch(patch);
1429
+ if (patch.enabled !== undefined || patch.style !== undefined)
1430
+ reconcileSelectorBorders();
1431
+ refresh();
1432
+ },
1433
+ setFooterComponent(
1434
+ patch: Partial<FooterComponentConfig>,
1435
+ ctx: ExtensionContext,
1436
+ ) {
1437
+ const previousStyle = effectiveFooterStyle();
1438
+ currentConfig = saveFooterComponentPatch(patch);
1439
+ const styleChanged = effectiveFooterStyle() !== previousStyle;
1440
+ if (patch.style !== undefined) reconcileFooter(ctx);
1441
+ if (patch.modelLabel !== undefined) syncFooterState(ctx);
1442
+ reconcileProjectRefresh(ctx, styleChanged);
1443
+ reconcileSessionTimer();
1444
+ refresh();
1445
+ },
1446
+ setFooterSegments(
1447
+ patch: Partial<FooterSegmentsConfig>,
1448
+ ctx: ExtensionContext,
1449
+ ) {
1450
+ applyFooterDependencyConfigChange(ctx, () =>
1451
+ saveStarshipFooterStylePatch({
1452
+ segments: patch as FooterSegmentsConfig,
1453
+ }),
1454
+ );
1455
+ },
1456
+ setFooterFormat(value: string, ctx: ExtensionContext) {
1457
+ applyFooterDependencyConfigChange(ctx, () =>
1458
+ saveStarshipFooterStylePatch({ format: value }),
1459
+ );
1460
+ },
1461
+ setResponsiveFooter(
1462
+ patch: Partial<
1463
+ Pick<PolishedTuiConfig, "responsiveFooter" | "compactFooterMaxLines">
1464
+ >,
1465
+ ctx: ExtensionContext,
1466
+ ) {
1467
+ applyFooterDependencyConfigChange(ctx, () =>
1468
+ saveStarshipFooterStylePatch({
1469
+ ...(patch.responsiveFooter === undefined
1470
+ ? {}
1471
+ : { responsive: patch.responsiveFooter }),
1472
+ ...(patch.compactFooterMaxLines === undefined
1473
+ ? {}
1474
+ : { compactMaxLines: patch.compactFooterMaxLines }),
1475
+ }),
1476
+ );
1477
+ },
1478
+ setIconMode(mode: IconMode) {
1479
+ currentConfig = saveIconsModePatch(mode);
1480
+ },
1481
+ setContextStyle(style: ContextStyle) {
1482
+ currentConfig = saveStarshipFooterStylePatch({ contextStyle: style });
1483
+ },
1484
+ setSeparator(separator: SeparatorStyle) {
1485
+ currentConfig = saveStarshipFooterStylePatch({ separator });
1486
+ },
1487
+ setPathDisplay(patch: Partial<PathDisplayConfig>) {
1488
+ currentConfig = saveStarshipFooterStylePatch({
1489
+ pathDisplay: patch as PathDisplayConfig,
1490
+ });
1491
+ },
1492
+ setGitBranch(patch: Partial<GitBranchConfig>) {
1493
+ currentConfig = saveStarshipFooterStylePatch({
1494
+ gitBranch: patch as GitBranchConfig,
1495
+ });
1496
+ },
1497
+ setGitCommit(
1498
+ patch: Partial<Pick<GitCommitConfig, "onlyDetached" | "showTag">>,
1499
+ ctx: ExtensionContext,
1500
+ ) {
1501
+ currentConfig = saveStarshipFooterStylePatch({
1502
+ gitCommit: patch as GitCommitConfig,
1503
+ });
1504
+ if (patch.showTag !== undefined) reconcileProjectRefresh(ctx, true);
1505
+ },
1506
+ setGitMetrics(patch: Partial<GitMetricsConfig>, ctx: ExtensionContext) {
1507
+ currentConfig = saveStarshipFooterStylePatch({
1508
+ gitMetrics: patch as GitMetricsConfig,
1509
+ });
1510
+ if (patch.ignoreSubmodules !== undefined)
1511
+ reconcileProjectRefresh(ctx, true);
1512
+ },
1513
+ getActiveExtensionStatuses() {
1514
+ return getActiveExtensionStatuses();
1515
+ },
1516
+ setExtensionStatusDefaultPlacement(placement: ExtensionStatusPlacement) {
1517
+ currentConfig = saveExtensionStatusDefaultPlacement(placement);
1518
+ },
1519
+ setExtensionStatusPlacement(
1520
+ key: string,
1521
+ placement: ExtensionStatusPlacement,
1522
+ ) {
1523
+ currentConfig = saveExtensionStatusPlacement(key, placement);
1524
+ },
1525
+ setExtensionStatusColorMode(
1526
+ key: string,
1527
+ colorMode: ExtensionStatusColorMode,
1528
+ ) {
1529
+ currentConfig = saveExtensionStatusColorMode(key, colorMode);
1530
+ },
1531
+ requestRender() {
1532
+ refresh();
1533
+ },
1534
+ });
1535
+
1536
+ pi.on("session_shutdown", async (_event, ctx) => {
1537
+ liveContext.clear();
1538
+ interactionMetrics.shutdown();
1539
+ workingLine.dispose(ctx);
1540
+ cleanupUi(ctx);
1541
+ });
1542
+
1543
+ const syncInteractiveAndProjectStateWithUsage = (
1544
+ _event: unknown,
1545
+ ctx: ExtensionContext,
1546
+ ) => {
1547
+ invalidateUsageTotalsCache();
1548
+ refreshInteractiveState(ctx, true);
1549
+ };
1550
+
1551
+ pi.on("agent_start", (event, ctx) => {
1552
+ liveContext.clear();
1553
+ const { interactionStarted } = interactionMetrics.agentStart();
1554
+ startAgentTurn(interactionStarted);
1555
+ workingLine.startAgent(ctx);
1556
+ syncInteractiveState(event, ctx);
1557
+ });
1558
+ pi.on("turn_start", (_event, ctx) => {
1559
+ interactionMetrics.turnStart();
1560
+ workingLine.startTurn(ctx);
1561
+ });
1562
+ pi.on("agent_end", (event, ctx) => {
1563
+ liveContext.clear();
1564
+ const displayTokens = interactionMetrics.currentDisplayTokens();
1565
+ interactionMetrics.agentEnd();
1566
+ pauseAgentRun();
1567
+ workingLine.finishAgent(ctx);
1568
+ workingLine.updateMetrics(
1569
+ displayTokens,
1570
+ interactionMetrics.currentThought(),
1571
+ ctx,
1572
+ );
1573
+ // Reconcile once more after Pi has persisted the assistant message.
1574
+ syncInteractiveAndProjectStateWithUsage(event, ctx);
1575
+ });
1576
+ pi.on("model_select", (event, ctx) => {
1577
+ liveContext.clear();
1578
+ syncInteractiveState(event, ctx);
1579
+ });
1580
+ pi.on("thinking_level_select", syncInteractiveState);
1581
+ pi.on("session_info_changed", syncInteractiveState);
1582
+ pi.on("message_update", (event, ctx) => {
1583
+ liveContext.update(event.message);
1584
+ const metrics = interactionMetrics.messageUpdate(
1585
+ event.message,
1586
+ "assistantMessageEvent" in event
1587
+ ? event.assistantMessageEvent
1588
+ : undefined,
1589
+ );
1590
+ if (metrics.usageChanged || metrics.thoughtChanged) {
1591
+ workingLine.updateMetrics(
1592
+ metrics.displayTokens,
1593
+ interactionMetrics.currentThought(),
1594
+ ctx,
1595
+ );
1596
+ }
1597
+ });
1598
+ pi.on("message_end", (event, ctx) => {
1599
+ const result = interactionMetrics.messageEnd(event.message);
1600
+ if (result.status === "accepted") {
1601
+ workingLine.updateMetrics(
1602
+ result.displayTokens,
1603
+ interactionMetrics.currentThought(),
1604
+ ctx,
1605
+ );
1606
+ }
1607
+ // Pi notifies extensions before persisting a successful message, so retain its live
1608
+ // context until agent_end; accepted failed messages clear immediately instead of showing
1609
+ // stale usage. Rejected and duplicate finals are not authoritative.
1610
+ if (
1611
+ result.status === "accepted" &&
1612
+ event.message.role === "assistant" &&
1613
+ (event.message.stopReason === "error" ||
1614
+ event.message.stopReason === "aborted")
1615
+ ) {
1616
+ liveContext.clear();
1617
+ }
1618
+ syncInteractiveAndProjectStateWithUsage(event, ctx);
1619
+ });
1620
+ pi.on("agent_settled", (_event, ctx) => {
1621
+ const settled = interactionMetrics.settle(ctx.isIdle());
1622
+ if (!settled) return;
1623
+ settleAgentTurn(settled.nextStartedAt);
1624
+ workingLine.settle(settled.nextTokens, settled.nextThought, ctx);
1625
+ const config = currentConfig.components.workingLine;
1626
+ if (config.enabled && config.turnSummary) {
1627
+ try {
1628
+ pi.appendEntry(TURN_SUMMARY_ENTRY_TYPE, {
1629
+ version: 3,
1630
+ ...settled.summary,
1631
+ stylePrefix: snapshotWorkingLineHighStyle(
1632
+ ctx.ui.theme,
1633
+ config,
1634
+ currentConfig.colors,
1635
+ ),
1636
+ });
1637
+ } catch {
1638
+ // A transcript persistence failure must not break settlement cleanup.
1639
+ }
1640
+ }
1641
+ });
1642
+ pi.on("tool_execution_start", (event, ctx) => {
1643
+ liveContext.clear();
1644
+ workingLine.startTool(event.toolCallId, event.toolName, ctx);
1645
+ syncInteractiveState(event, ctx);
1646
+ });
1647
+ pi.on("tool_execution_end", (event, ctx) => {
1648
+ workingLine.finishTool(event.toolCallId, ctx);
1649
+ syncInteractiveAndProjectState(event, ctx);
1650
+ });
1651
+ pi.on("session_compact", (event, ctx) => {
1652
+ liveContext.clear();
1653
+ syncInteractiveAndProjectStateWithUsage(event, ctx);
1654
+ });
1655
+ pi.on("session_tree", (event, ctx) => {
1656
+ liveContext.clear();
1657
+ syncInteractiveAndProjectStateWithUsage(event, ctx);
1658
+ });
1659
+ }