pi-zentui 0.29.2 → 0.29.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.
@@ -336,9 +336,9 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
336
336
  return undefined;
337
337
  }
338
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().
339
+ // Layered visibility does not grant installation ownership: editor install/restore stays exclusive.
340
+ // Data, demand, and timer consumers use these helpers; Working-line placement additionally
341
+ // requires focused, rendered border capability before its controller hides its own host row.
342
342
  // Ownership counts before Pi constructs its editor; layered visibility requires the recorded
343
343
  // instance to remain reachable through the current factory.
344
344
  const layeredVisibleEditor = () => layeredEditor.editorVisibleThrough(currentEditorFactory());
@@ -402,17 +402,13 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
402
402
  undefined,
403
403
  (ctx) => {
404
404
  if (!activeTuiContext || ctx.ui !== activeTuiContext.ui) return false;
405
- if (!ownsInstalledEditorFactory()) {
406
- activeEditor = undefined;
407
- return false;
408
- }
405
+ // Factory visibility alone is insufficient: the current focused editor must have
406
+ // rendered safe border geometry before the separately owned Working row can hide.
409
407
  return Boolean(
410
- activeEditor &&
411
- sessionLifecycle.isCurrent(activeEditor.generation) &&
412
- activeEditor.factory === installedEditorFactory &&
408
+ sessionLifecycle.isCurrent() &&
413
409
  effectiveEditorEnabled() &&
414
410
  currentConfig.components.editor.style !== "accent-rail" &&
415
- activeEditor.editor.canEmbedWorkingLineBorder(),
411
+ visibleEditor()?.canEmbedWorkingLineBorder(),
416
412
  );
417
413
  },
418
414
  );
@@ -939,10 +935,7 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
939
935
  return observed;
940
936
  };
941
937
 
942
- // Exclusive: gates mutation of host UI such as the Working row.
943
- const isActiveEditor = (editor: PolishedEditor | WrappedPolishedEditor, generation: number) =>
944
- sessionLifecycle.isCurrent(generation) && activeEditor?.editor === editor;
945
- // Read-only: also true for an editor still visible beneath a foreign wrapper.
938
+ // Also true for an editor still visible beneath a foreign wrapper; installation stays exclusive.
946
939
  const isVisibleEditor = (editor: PolishedEditor | WrappedPolishedEditor, generation: number) =>
947
940
  sessionLifecycle.isCurrent(generation) && visibleEditor() === editor;
948
941
 
@@ -950,7 +943,7 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
950
943
  editor: PolishedEditor | WrappedPolishedEditor,
951
944
  generation: number,
952
945
  ) => {
953
- if (!isActiveEditor(editor, generation) || !activeTuiContext || !workingLineSessionReady)
946
+ if (!isVisibleEditor(editor, generation) || !activeTuiContext || !workingLineSessionReady)
954
947
  return;
955
948
  if (!host.skinOnly) workingLine.reconcile(activeTuiContext);
956
949
  if (workingLine.isAvailable()) requestEditorRepaint();
@@ -966,10 +959,12 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
966
959
  const editorWorkingLineFrame = (
967
960
  editor: PolishedEditor | WrappedPolishedEditor,
968
961
  generation: number,
969
- ) =>
970
- !host.skinOnly && isActiveEditor(editor, generation)
971
- ? workingLine.currentWorkingLineFrame()
972
- : undefined;
962
+ ) => {
963
+ if (host.skinOnly || !sessionLifecycle.isCurrent(generation)) return undefined;
964
+ // Even a stale instance render must release a row whose current border disappeared.
965
+ const frame = workingLine.currentWorkingLineFrame();
966
+ return isVisibleEditor(editor, generation) ? frame : undefined;
967
+ };
973
968
 
974
969
  /** Records a freshly constructed editor as owned (exclusive) or visible beneath a foreign wrapper. */
975
970
  const registerConstructedEditor = (
@@ -8,9 +8,9 @@
8
8
  * and redraw requests. The record is valid only while the outer factory observed at construction is
9
9
  * still the current editor factory, so any replacement of the chain invalidates it.
10
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.
11
+ * Construction alone cannot authorize hiding the Working row: its controller additionally requires
12
+ * a focused editor that has rendered safe border geometry. A wrapper that renders then discards those
13
+ * rows remains undetectable; supporting such wrappers requires an explicit host visibility contract.
14
14
  */
15
15
  export class LayeredEditorConsumer<Factory, Editor = unknown> {
16
16
  private record?: {
@@ -172,9 +172,11 @@ function renderTopLeft(
172
172
  metadata: MinimalistEditorMetadata,
173
173
  uiTheme: Theme,
174
174
  config: ZentuiConfig,
175
+ renderBorder: (text: string) => string,
175
176
  includeSessionName = true,
176
177
  ): string {
177
178
  const source = config.components.editor.colorSource;
179
+ const separator = safeThemeFg(uiTheme, "muted", " · ");
178
180
  const bashMode = bashModeLabel(inputText);
179
181
  const parts: string[] = [];
180
182
  if (bashMode) {
@@ -206,16 +208,20 @@ function renderTopLeft(
206
208
  ? sanitizeEditorMetadataText(metadata.sessionName ?? "")
207
209
  : "";
208
210
  if (config.components.editor.styles.minimalist.showSessionName && sessionName) {
209
- parts.push(
210
- renderStyleForSource(
211
- uiTheme,
212
- source,
213
- componentColor(config, "editor", "sessionName"),
214
- sessionName,
215
- ),
211
+ return joinStyled(
212
+ [
213
+ joinStyled(parts, separator),
214
+ renderStyleForSource(
215
+ uiTheme,
216
+ source,
217
+ componentColor(config, "editor", "sessionName"),
218
+ sessionName,
219
+ ),
220
+ ],
221
+ metadata.workingLineFrame ? renderBorder(" ─ ") : separator,
216
222
  );
217
223
  }
218
- return joinStyled(parts, safeThemeFg(uiTheme, "muted", " · "));
224
+ return joinStyled(parts, separator);
219
225
  }
220
226
 
221
227
  function renderTopRight(
@@ -448,6 +454,7 @@ function renderTemplateBuiltin(
448
454
  { cwd: "", sessionName: metadata.sessionName },
449
455
  uiTheme,
450
456
  explicitConfig,
457
+ renderBorder,
451
458
  );
452
459
  case "turn_duration":
453
460
  return renderTopLeft(
@@ -455,6 +462,7 @@ function renderTemplateBuiltin(
455
462
  { cwd: "", agentDurationMs: metadata.agentDurationMs, agentActive: metadata.agentActive },
456
463
  uiTheme,
457
464
  explicitConfig,
465
+ renderBorder,
458
466
  );
459
467
  case "model":
460
468
  isolated.modelLabel = metadata.modelLabel;
@@ -693,8 +701,8 @@ export function renderMinimalistFrame({
693
701
  if (!count || !/^[1-9]\d*$/.test(count)) return "";
694
702
  return safeThemeFg(uiTheme, "muted", `${direction === "above" ? "↑" : "↓"} ${count} more`);
695
703
  };
696
- const topMetadata = renderTopLeft(inputText, metadata, uiTheme, config);
697
- const topOperational = renderTopLeft(inputText, metadata, uiTheme, config, false);
704
+ const topMetadata = renderTopLeft(inputText, metadata, uiTheme, config, renderBorder);
705
+ const topOperational = renderTopLeft(inputText, metadata, uiTheme, config, renderBorder, false);
698
706
  const topViewport = viewportLabel("above", viewport?.above);
699
707
  const topLeft = joinStyled([topViewport, topMetadata], separator);
700
708
  const topRightBudget = Math.max(0, width - 8 - visibleWidth(topLeft));
@@ -775,6 +783,7 @@ export function renderMinimalistFrame({
775
783
  { cwd: "", workingLineFrame: metadata.workingLineFrame },
776
784
  uiTheme,
777
785
  config,
786
+ renderBorder,
778
787
  false,
779
788
  )
780
789
  : "";
@@ -784,13 +793,18 @@ export function renderMinimalistFrame({
784
793
  if (visibleWidth(prefix) > budget) prefix = operational;
785
794
  if (visibleWidth(prefix) > budget)
786
795
  prefix = truncateToWidth(prefix, Math.max(0, budget), "…");
796
+ const metadataSeparator =
797
+ operational && metadata.workingLineFrame ? renderBorder(" ─ ") : separator;
787
798
  rendered = renderMinimalistTemplate(
788
799
  format,
789
800
  resolve,
790
801
  customNames,
791
- Math.max(0, budget - visibleWidth(prefix) - (prefix ? visibleWidth(separator) : 0)),
802
+ Math.max(
803
+ 0,
804
+ budget - visibleWidth(prefix) - (prefix ? visibleWidth(metadataSeparator) : 0),
805
+ ),
792
806
  );
793
- return joinStyled([prefix, rendered], separator);
807
+ return joinStyled([prefix, rendered], metadataSeparator);
794
808
  }
795
809
  switch (slot) {
796
810
  case "topLeft": {
@@ -179,18 +179,20 @@ function nativeMarkdown(instance: PatchableUserMessagePrototype):
179
179
  }
180
180
  const children = direct;
181
181
  if (!Array.isArray(children) || children.length !== 1) return undefined;
182
- const box = children[0];
183
- if (!isRecord(box) || !Array.isArray(box.children) || box.children.length !== 1) return undefined;
184
- const renderer = box.children[0];
182
+ // Pi >=1.0 holds the Markdown directly (it pads/colors itself); older Pi
183
+ // wrapped it in a Box. Accept both shapes.
184
+ let renderer = children[0];
185
+ const direct1 = isNativeMarkdown(renderer);
186
+ if (isRecord(renderer) && !direct1) {
187
+ const box = renderer;
188
+ if (!Array.isArray(box.children) || box.children.length !== 1) return undefined;
189
+ renderer = box.children[0];
190
+ }
185
191
  if (!isNativeMarkdown(renderer)) return undefined;
186
192
  const child = renderer as unknown as Record<string, unknown>;
187
- if (
188
- typeof child.text !== "string" ||
189
- child.paddingX !== 0 ||
190
- child.paddingY !== 0 ||
191
- !isRecord(child.theme)
192
- )
193
- return undefined;
193
+ if (typeof child.text !== "string" || !isRecord(child.theme)) return undefined;
194
+ // Box-wrapped (older Pi) Markdown must be unpadded; Pi >=1.0 pads it itself.
195
+ if (!direct1 && (child.paddingX !== 0 || child.paddingY !== 0)) return undefined;
194
196
  for (const key of [
195
197
  "heading",
196
198
  "link",
@@ -217,6 +219,8 @@ function nativeMarkdown(instance: PatchableUserMessagePrototype):
217
219
  )
218
220
  return undefined;
219
221
  if (child.defaultTextStyle !== undefined && !isRecord(child.defaultTextStyle)) return undefined;
222
+ // Pi >=1.0 puts the message background in defaultTextStyle; Zentui frames draw their own.
223
+ const { bgColor: _bg, ...textStyle } = (child.defaultTextStyle ?? {}) as Record<string, unknown>;
220
224
  return {
221
225
  renderer: renderer as unknown as Markdown,
222
226
  text: child.text,
@@ -224,9 +228,9 @@ function nativeMarkdown(instance: PatchableUserMessagePrototype):
224
228
  theme: child.theme as unknown as NonNullable<
225
229
  UserMessageStyleRenderInput["markdown"]
226
230
  >["theme"],
227
- defaultTextStyle: child.defaultTextStyle as NonNullable<
228
- UserMessageStyleRenderInput["markdown"]
229
- >["defaultTextStyle"],
231
+ defaultTextStyle: (child.defaultTextStyle === undefined
232
+ ? undefined
233
+ : textStyle) as NonNullable<UserMessageStyleRenderInput["markdown"]>["defaultTextStyle"],
230
234
  options: child.options as NonNullable<UserMessageStyleRenderInput["markdown"]>["options"],
231
235
  },
232
236
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-zentui",
3
- "version": "0.29.2",
3
+ "version": "0.29.3",
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",