pi-zentui 0.29.0 → 0.29.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.
@@ -81,6 +81,7 @@ import {
81
81
  renderTurnSummaryEntry,
82
82
  TURN_SUMMARY_ENTRY_TYPE,
83
83
  } from "./interaction-summary";
84
+ import { LayeredEditorConsumer } from "./layered-editor";
84
85
  import { LiveContextController } from "./live-context";
85
86
  import { readPackageVersionResult } from "./package-version";
86
87
  import { getComponentPreset } from "./presets";
@@ -277,7 +278,7 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
277
278
  let stopSessionTimer: () => void = () => {};
278
279
  let stopMinimalistDurationUpdates: () => void = () => {};
279
280
  let minimalistDurationUpdatesActive = false;
280
- let minimalistDecorationActive = false;
281
+ let minimalistDecorationEditor: PolishedEditor | WrappedPolishedEditor | undefined;
281
282
  let customVariableSessionReady = false;
282
283
  let sessionTimerRequirements = "";
283
284
  let lastDurationLabel = "";
@@ -324,14 +325,41 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
324
325
  }
325
326
  };
326
327
 
328
+ const layeredEditor = new LayeredEditorConsumer<
329
+ EditorFactory,
330
+ PolishedEditor | WrappedPolishedEditor
331
+ >((generation) => sessionLifecycle.isCurrent(generation));
332
+ const currentEditorFactory = (): EditorFactory | undefined => {
333
+ try {
334
+ return activeTuiContext?.ui.getEditorComponent();
335
+ } catch {
336
+ return undefined;
337
+ }
338
+ };
339
+ // Layered visibility is read-only recognition; installation ownership above stays exclusive.
340
+ // Data, demand, and timer consumers read the visible editor through these two helpers; anything
341
+ // that installs, restores, or mutates host UI must keep using ownsInstalledEditorFactory().
342
+ // Ownership counts before Pi constructs its editor; layered visibility requires the recorded
343
+ // instance to remain reachable through the current factory.
344
+ const layeredVisibleEditor = () => layeredEditor.editorVisibleThrough(currentEditorFactory());
345
+ const editorConsumerActive = () =>
346
+ ownsInstalledEditorFactory() || layeredVisibleEditor() !== undefined;
347
+ const visibleEditor = () =>
348
+ ownsInstalledEditorFactory() ? activeEditor?.editor : layeredVisibleEditor();
349
+ const requestEditorRepaint = () => {
350
+ requestEditorRender?.();
351
+ // The layered record intentionally outlives ownership clearing, so disabling the editor is
352
+ // gated here; a live wrapper keeps repainting while the editor remains enabled.
353
+ if (effectiveEditorEnabled()) layeredEditor.requestRender(currentEditorFactory());
354
+ };
355
+
327
356
  const customVariables = new CustomVariables(
328
357
  pi.events,
329
358
  (key) => {
330
359
  if (!customVariableSessionReady || !sessionLifecycle.isCurrent()) return false;
331
360
  return (
332
361
  (effectiveEditorEnabled() &&
333
- ownsInstalledEditorFactory() &&
334
- activeEditor?.editor.isMetadataDecorated() === true &&
362
+ visibleEditor()?.isMetadataDecorated() === true &&
335
363
  editorDemandsCustomVariable(currentConfig, key)) ||
336
364
  (installedFooterKind === "starship" &&
337
365
  ownsInstalledFooter() &&
@@ -340,7 +368,7 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
340
368
  },
341
369
  () => {
342
370
  if (!customVariableSessionReady || !sessionLifecycle.isCurrent()) return;
343
- requestEditorRender?.();
371
+ requestEditorRepaint();
344
372
  requestFooterRender?.();
345
373
  },
346
374
  );
@@ -349,7 +377,7 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
349
377
  customVariables.reconcile();
350
378
  codexQuota.reconcile();
351
379
  requestFooterRender?.();
352
- requestEditorRender?.();
380
+ requestEditorRepaint();
353
381
  };
354
382
  const thinkingExperimental = new ThinkingExperimentalController(
355
383
  () => currentConfig.components.thinkingSteps,
@@ -391,7 +419,7 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
391
419
  if (!host.skinOnly)
392
420
  workingLine.setRequestRender(() => {
393
421
  if (sessionLifecycle.isCurrent()) {
394
- requestEditorRender?.();
422
+ requestEditorRepaint();
395
423
  }
396
424
  });
397
425
  let workingLineSessionReady = false;
@@ -468,7 +496,7 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
468
496
  return usageTotals.resolve(
469
497
  ctx,
470
498
  (effectiveEditorEnabled() &&
471
- ownsInstalledEditorFactory() &&
499
+ editorConsumerActive() &&
472
500
  ["tokens", "input_tokens", "output_tokens", "cost", "cache_hit"].some((name) =>
473
501
  editorMetadataReferences(currentConfig).has(name),
474
502
  )) ||
@@ -504,9 +532,7 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
504
532
  )
505
533
  return undefined;
506
534
  const editorDemand =
507
- effectiveEditorEnabled() &&
508
- ownsInstalledEditorFactory() &&
509
- editorWantsCodexQuota(currentConfig);
535
+ effectiveEditorEnabled() && editorConsumerActive() && editorWantsCodexQuota(currentConfig);
510
536
  const footerDemand =
511
537
  effectiveFooterStyle() === "starship" &&
512
538
  currentConfig.components.footer.codexQuota &&
@@ -516,7 +542,7 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
516
542
  () => {
517
543
  if (!sessionLifecycle.isCurrent()) return;
518
544
  requestFooterRender?.();
519
- requestEditorRender?.();
545
+ requestEditorRepaint();
520
546
  },
521
547
  );
522
548
  const getEditorQuota = () =>
@@ -598,7 +624,7 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
598
624
  currentConfig,
599
625
  installedFooterReferences(),
600
626
  effectiveEditorEnabled() &&
601
- ownsInstalledEditorFactory() &&
627
+ editorConsumerActive() &&
602
628
  currentConfig.components.editor.style === "minimalist",
603
629
  );
604
630
 
@@ -647,6 +673,8 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
647
673
  if (!sessionLifecycle.isCurrent() || !ctx.hasUI) return;
648
674
  if (activeTuiContext?.ui === ctx.ui) activeTuiContext = ctx;
649
675
  if (editorInstalled && !ownsInstalledEditorFactory()) reconcileObservedEditorOwnership(ctx);
676
+ // A layered editor can stop being visible without any ownership transition to observe.
677
+ if (projectRefreshActive && !needsProjectRefresh()) stopProjectRefresh();
650
678
  syncFooterState(ctx);
651
679
  if (project && needsProjectRefresh()) scheduleProjectRefresh(ctx);
652
680
  refresh();
@@ -698,9 +726,8 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
698
726
  sessionLifecycle.isCurrent() &&
699
727
  agentRunActive &&
700
728
  agentDurationClock.isActive() &&
701
- minimalistDecorationActive &&
729
+ minimalistDecorationActive() &&
702
730
  effectiveEditorEnabled() &&
703
- ownsInstalledEditorFactory() &&
704
731
  currentConfig.components.editor.style === "minimalist" &&
705
732
  editorMetadataReferences(currentConfig).has("turn_duration");
706
733
  if (!needed) {
@@ -721,13 +748,25 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
721
748
  });
722
749
  };
723
750
 
724
- const setMinimalistDecorationActive = (active: boolean) => {
725
- const next = sessionLifecycle.isCurrent() && active && ownsInstalledEditorFactory();
726
- if (minimalistDecorationActive === next) return;
727
- minimalistDecorationActive = next;
751
+ // Decoration is bound to the editor instance that reported it, so it stays valid while that
752
+ // editor is visible (owned or layered) and cannot leak to a replacement editor.
753
+ const minimalistDecorationActive = () =>
754
+ sessionLifecycle.isCurrent() &&
755
+ minimalistDecorationEditor !== undefined &&
756
+ visibleEditor() === minimalistDecorationEditor;
757
+ const reconcileMinimalistDecoration = () => {
728
758
  customVariables.reconcile();
729
759
  reconcileAgentTimer();
730
760
  };
761
+ const setMinimalistDecorationActive = (
762
+ active: boolean,
763
+ editor?: PolishedEditor | WrappedPolishedEditor,
764
+ ) => {
765
+ const next = active ? editor : undefined;
766
+ if (minimalistDecorationEditor === next) return;
767
+ minimalistDecorationEditor = next;
768
+ reconcileMinimalistDecoration();
769
+ };
731
770
 
732
771
  const startAgentTurn = (interactionStarted: boolean) => {
733
772
  agentRunActive = true;
@@ -852,12 +891,12 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
852
891
  if (activeTuiContext && workingLineSessionReady) {
853
892
  if (!host.skinOnly) workingLine.reconcile(activeTuiContext);
854
893
  }
855
- setMinimalistDecorationActive(false);
856
894
  requestEditorRender = undefined;
857
895
  wrappedEditorFactory = undefined;
858
896
  installedEditorFactory = undefined;
859
897
  editorInstallMode = "none";
860
898
  editorInstalled = false;
899
+ reconcileMinimalistDecoration();
861
900
  codexQuota.reconcile();
862
901
  };
863
902
 
@@ -900,8 +939,12 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
900
939
  return observed;
901
940
  };
902
941
 
942
+ // Exclusive: gates mutation of host UI such as the Working row.
903
943
  const isActiveEditor = (editor: PolishedEditor | WrappedPolishedEditor, generation: number) =>
904
944
  sessionLifecycle.isCurrent(generation) && activeEditor?.editor === editor;
945
+ // Read-only: also true for an editor still visible beneath a foreign wrapper.
946
+ const isVisibleEditor = (editor: PolishedEditor | WrappedPolishedEditor, generation: number) =>
947
+ sessionLifecycle.isCurrent(generation) && visibleEditor() === editor;
905
948
 
906
949
  const editorBorderCapabilityChanged = (
907
950
  editor: PolishedEditor | WrappedPolishedEditor,
@@ -910,14 +953,14 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
910
953
  if (!isActiveEditor(editor, generation) || !activeTuiContext || !workingLineSessionReady)
911
954
  return;
912
955
  if (!host.skinOnly) workingLine.reconcile(activeTuiContext);
913
- if (workingLine.isAvailable()) requestEditorRender?.();
956
+ if (workingLine.isAvailable()) requestEditorRepaint();
914
957
  };
915
958
 
916
959
  const editorMetadataDecorationChanged = (
917
960
  editor: PolishedEditor | WrappedPolishedEditor,
918
961
  generation: number,
919
962
  ) => {
920
- if (isActiveEditor(editor, generation)) customVariables.reconcile();
963
+ if (isVisibleEditor(editor, generation)) customVariables.reconcile();
921
964
  };
922
965
 
923
966
  const editorWorkingLineFrame = (
@@ -928,6 +971,31 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
928
971
  ? workingLine.currentWorkingLineFrame()
929
972
  : undefined;
930
973
 
974
+ /** Records a freshly constructed editor as owned (exclusive) or visible beneath a foreign wrapper. */
975
+ const registerConstructedEditor = (
976
+ ctx: ExtensionContext,
977
+ factory: ZentuiEditorFactory,
978
+ generation: number,
979
+ editor: PolishedEditor | WrappedPolishedEditor,
980
+ tui: TUI,
981
+ ) => {
982
+ const observed = observeEditorFactory(ctx);
983
+ if (
984
+ !sessionLifecycle.isCurrent(generation) ||
985
+ activeTuiContext?.ui !== ctx.ui ||
986
+ !observed.known
987
+ ) {
988
+ return;
989
+ }
990
+ if (observed.factory === factory) {
991
+ activeEditor = { editor, factory, generation };
992
+ requestEditorRender = () => tui.requestRender();
993
+ } else if (observed.factory && !isZentuiEditorFactory(observed.factory)) {
994
+ // A foreign wrapper built this editor beneath its own outer factory.
995
+ layeredEditor.attach(observed.factory, generation, editor, () => tui.requestRender());
996
+ }
997
+ };
998
+
931
999
  const makeEditorFactory = (
932
1000
  ctx: ExtensionContext,
933
1001
  baseFactory: EditorFactory | undefined,
@@ -977,21 +1045,12 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
977
1045
  };
978
1046
  },
979
1047
  (active) => {
980
- if (isActiveEditor(editor, generation)) setMinimalistDecorationActive(active);
1048
+ if (isVisibleEditor(editor, generation)) setMinimalistDecorationActive(active, editor);
981
1049
  },
982
1050
  () => editorBorderCapabilityChanged(editor, generation),
983
1051
  () => editorMetadataDecorationChanged(editor, generation),
984
1052
  );
985
- const observed = observeEditorFactory(ctx);
986
- if (
987
- sessionLifecycle.isCurrent(generation) &&
988
- activeTuiContext?.ui === ctx.ui &&
989
- observed.known &&
990
- observed.factory === factory
991
- ) {
992
- activeEditor = { editor, factory, generation };
993
- requestEditorRender = () => tui.requestRender();
994
- }
1053
+ registerConstructedEditor(ctx, factory, generation, editor, tui);
995
1054
  return editor;
996
1055
  }) as ZentuiEditorFactory;
997
1056
  factory[ZENTUI_EDITOR_FACTORY] = true;
@@ -1048,21 +1107,12 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
1048
1107
  };
1049
1108
  },
1050
1109
  (active) => {
1051
- if (isActiveEditor(editor, generation)) setMinimalistDecorationActive(active);
1110
+ if (isVisibleEditor(editor, generation)) setMinimalistDecorationActive(active, editor);
1052
1111
  },
1053
1112
  () => editorBorderCapabilityChanged(editor, generation),
1054
1113
  () => editorMetadataDecorationChanged(editor, generation),
1055
1114
  );
1056
- const observed = observeEditorFactory(ctx);
1057
- if (
1058
- sessionLifecycle.isCurrent(generation) &&
1059
- activeTuiContext?.ui === ctx.ui &&
1060
- observed.known &&
1061
- observed.factory === factory
1062
- ) {
1063
- activeEditor = { editor, factory, generation };
1064
- requestEditorRender = () => tui.requestRender();
1065
- }
1115
+ registerConstructedEditor(ctx, factory, generation, editor, tui);
1066
1116
  return editor;
1067
1117
  }) as ZentuiEditorFactory;
1068
1118
  factory[ZENTUI_EDITOR_FACTORY] = true;
@@ -1415,6 +1465,8 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
1415
1465
  if (!ctx || !sessionLifecycle.isCurrent()) return;
1416
1466
  sessionLifecycle.shutdown();
1417
1467
  activeEditor = undefined;
1468
+ minimalistDecorationEditor = undefined;
1469
+ layeredEditor.detach();
1418
1470
  extensionStatuses.dispose();
1419
1471
  codexQuota.stop();
1420
1472
  stopSessionTimer();
@@ -1474,6 +1526,7 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
1474
1526
  customVariables.clear();
1475
1527
  const lifecycleGeneration = sessionLifecycle.start();
1476
1528
  activeEditor = undefined;
1529
+ minimalistDecorationEditor = undefined;
1477
1530
  // A new generation must not expose or route extension segments through the previous
1478
1531
  // session while TUI startup is in progress.
1479
1532
  workingLineSessionReady = false;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Records that a Zentui editor instance is still visible beneath a foreign predecessor-preserving
3
+ * wrapper (`setEditorComponent((tui, theme, kb) => predecessor(tui, theme, kb))`).
4
+ *
5
+ * This is deliberately NOT installation ownership: Zentui does not own the outer factory, must not
6
+ * replace or restore it, and keeps this record across ownership clearing. It only answers
7
+ * "may a surface Zentui rendered still be on screen?" for read-only consumers such as usage demand
8
+ * and redraw requests. The record is valid only while the outer factory observed at construction is
9
+ * still the current editor factory, so any replacement of the chain invalidates it.
10
+ *
11
+ * Visibility is only evidence that a wrapper constructed the editor, not that its rows reach the
12
+ * screen, so surfaces that mutate host UI (for example hiding the native Working row) must keep
13
+ * requiring exclusive installation ownership instead of consulting this record.
14
+ */
15
+ export class LayeredEditorConsumer<Factory, Editor = unknown> {
16
+ private record?: {
17
+ outerFactory: Factory;
18
+ generation: number;
19
+ editor: Editor;
20
+ requestRender: () => void;
21
+ };
22
+
23
+ constructor(private readonly isCurrentGeneration: (generation: number) => boolean) {}
24
+
25
+ attach(
26
+ outerFactory: Factory,
27
+ generation: number,
28
+ editor: Editor,
29
+ requestRender: () => void,
30
+ ): void {
31
+ this.record = { outerFactory, generation, editor, requestRender };
32
+ }
33
+
34
+ detach(): void {
35
+ this.record = undefined;
36
+ }
37
+
38
+ editorVisibleThrough(currentFactory: Factory | undefined): Editor | undefined {
39
+ const record = this.record;
40
+ if (!record) return undefined;
41
+ if (!this.isCurrentGeneration(record.generation) || record.outerFactory !== currentFactory) {
42
+ this.record = undefined;
43
+ return undefined;
44
+ }
45
+ return record.editor;
46
+ }
47
+
48
+ isVisibleThrough(currentFactory: Factory | undefined): boolean {
49
+ return this.editorVisibleThrough(currentFactory) !== undefined;
50
+ }
51
+
52
+ requestRender(currentFactory: Factory | undefined): void {
53
+ if (this.isVisibleThrough(currentFactory)) this.record?.requestRender();
54
+ }
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-zentui",
3
- "version": "0.29.0",
3
+ "version": "0.29.1",
4
4
  "description": "Modular UI components for Pi and Oh My Pi — customizable editors, messages, progress displays, and statuslines.",
5
5
  "type": "module",
6
6
  "license": "MIT",