pi-zentui 0.19.0 → 0.20.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.
package/README.md CHANGED
@@ -41,7 +41,7 @@ Editor, User messages, Working line, and selector borders use independent `enabl
41
41
  - Opencode autocomplete rows retain Pi's original unframed trailing layout; Minimalist keeps autocomplete inside its rounded frame
42
42
  - Configurable model, provider, thinking-level, accent, and border colors
43
43
 
44
- Editor previews:
44
+ Editor styles:
45
45
 
46
46
  <h4 align="center"><code>opencode</code></h4>
47
47
 
@@ -193,15 +193,15 @@ User config lives at `~/.pi/agent/zentui.json`. The file is optional: missing or
193
193
  The interactive `/zentui` menu is split into exactly eight component-oriented sections, in this order. Use `Tab` and `Shift+Tab` to switch sections:
194
194
 
195
195
  1. **Appearance** — selector-border enablement, style, and colors; icon mode.
196
- 2. **Editor** — editor enablement, style, colors, model label, border behavior, viewport indicators, and settings for the selected editor style.
197
- 3. **User messages** — message enablement, `framed | framed-copy-friendly | compact | labeled` style selection (including **Framed (copy-friendly)**), and colors.
198
- 4. **Working line** — ownership, settled Turn summary, spinner and text speeds, optional spinner-color motion, text animation, color source, custom-message toggle and editable list, Tool/Elapsed/Thinking/Tokens toggles, and preview.
196
+ 2. **Editor** — editor enablement, style, colors, model label, border behavior, viewport indicators, settings for the selected editor style, and a static synthetic preview.
197
+ 3. **User messages** — message enablement, `framed | framed-copy-friendly | compact | labeled` style selection (including **Framed (copy-friendly)**), colors, and a static synthetic Markdown preview.
198
+ 4. **Working line** — ownership, settled Turn summary, spinner and text speeds, optional spinner-color motion, text animation, color source, custom-message toggle and editable list, Tool/Elapsed/Thinking/Tokens toggles, and animated preview.
199
199
  5. **Footer** — `Native | Starship | Hidden` style selection. Starship additionally shows colors, model label, responsive layout, separator, context style, and path display.
200
200
  6. **Segments** — visibility toggles for non-Git Starship segments.
201
201
  7. **Git** — Starship Git segment and probe controls.
202
202
  8. **Extensions** — Starship extension-status placement and color controls for active keys.
203
203
 
204
- Editor, User messages, and Working line retain independent configuration. Footer's single style selects Pi's built-in Footer (`Native`), Zentui's Starship Footer, or an owned zero-row Footer (`Hidden`). Color and model-label rows update only their owning component.
204
+ Editor, User messages, and Working line retain independent configuration. Editor and User-message previews use fixed synthetic content and remain visible while their component is disabled; the Working-line preview reflects its current configured sample, state, and animation. Each preview appears above its settings. Only the Working-line preview owns an animation timer. Footer's single style selects Pi's built-in Footer (`Native`), Zentui's Starship Footer, or an owned zero-row Footer (`Hidden`). Color and model-label rows update only their owning component.
205
205
 
206
206
  Starship-specific Footer rows are shown only while Starship is selected. The **Segments**, **Git**, and **Extensions** sections remain available for preconfiguration under every Footer style. Free-form values such as custom formats, Opencode metadata format, raw colors/styles, and inactive extension keys remain JSON-only; Working-line speed accepts validated custom milliseconds in `/zentui`.
207
207
 
@@ -50,6 +50,11 @@ import {
50
50
  import { sanitizeExtensionStatusText } from "./extension-status";
51
51
  import { isIconMode } from "./icons";
52
52
  import type { SessionLifecycle } from "./session-lifecycle";
53
+ import {
54
+ renderEditorSettingsPreview,
55
+ renderUserMessageSettingsPreview,
56
+ SETTINGS_PREVIEW_MAX_WIDTH,
57
+ } from "./settings-previews";
53
58
  import { EDITOR_BORDER_STYLE, renderChromeBorder, safeThemeFg } from "./style";
54
59
  import {
55
60
  buildWorkingLinePreviewFrames,
@@ -120,6 +125,7 @@ const speedValues = (presets: readonly { label: string }[]) => [
120
125
  "Custom…",
121
126
  ];
122
127
  const workingLineTextAnimationValues: WorkingLineTextAnimation[] = ["classic", "kitt", "disabled"];
128
+
123
129
  const settingsSections = [
124
130
  "appearance",
125
131
  "editor",
@@ -1574,6 +1580,22 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
1574
1580
  };
1575
1581
  settingsList = makeSettingsList(initialFocusId);
1576
1582
  startPreview();
1583
+ const renderPreviewRows = (previewWidth: number): string[] => {
1584
+ if (previewWidth <= 0) return [];
1585
+ if (activeSection === "editor")
1586
+ return renderEditorSettingsPreview(deps.getConfig(), theme, previewWidth);
1587
+ if (activeSection === "userMessages")
1588
+ return renderUserMessageSettingsPreview(deps.getConfig(), theme, previewWidth);
1589
+ if (activeSection === "workingLine" && preview && preview.frames.length > 0)
1590
+ return [
1591
+ truncateToWidth(
1592
+ preview.frames[previewFrameIndex] ?? preview.frames[0],
1593
+ Math.min(SETTINGS_PREVIEW_MAX_WIDTH, previewWidth),
1594
+ "",
1595
+ ),
1596
+ ];
1597
+ return [];
1598
+ };
1577
1599
  return {
1578
1600
  render(width: number) {
1579
1601
  const border = renderChromeBorder(
@@ -1582,22 +1604,23 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
1582
1604
  EDITOR_BORDER_STYLE,
1583
1605
  "─".repeat(Math.max(0, width)),
1584
1606
  );
1607
+ const settingsRows = withSectionFooter(settingsList.render(width), theme).map(
1608
+ (line) => truncateToWidth(line, width, ""),
1609
+ );
1610
+ const previewRows = renderPreviewRows(Math.max(0, width - 4));
1611
+ while (previewRows.length > 0 && visibleWidth(previewRows.at(-1) ?? "") === 0)
1612
+ previewRows.pop();
1613
+ const indentedPreviewRows = previewRows.map((line) =>
1614
+ truncateToWidth(` ${line}`, width, ""),
1615
+ );
1616
+ const bodyRows = indentedPreviewRows.some((line) => visibleWidth(line) > 0)
1617
+ ? ["", ...indentedPreviewRows, "", ...settingsRows]
1618
+ : settingsRows;
1585
1619
  return [
1586
1620
  truncateToWidth(border, width, ""),
1587
1621
  truncateToWidth(formatSectionTabs(activeSection, theme, width), width, ""),
1588
1622
  truncateToWidth(border, width, ""),
1589
- ...(activeSection === "workingLine" && preview && preview.frames.length > 0
1590
- ? [
1591
- truncateToWidth(
1592
- ` ${preview.frames[previewFrameIndex] ?? preview.frames[0]}`,
1593
- width,
1594
- "",
1595
- ),
1596
- ]
1597
- : []),
1598
- ...withSectionFooter(settingsList.render(width), theme).map((line) =>
1599
- truncateToWidth(line, width, ""),
1600
- ),
1623
+ ...bodyRows,
1601
1624
  truncateToWidth(border, width, ""),
1602
1625
  ];
1603
1626
  },
@@ -0,0 +1,110 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import type { PolishedTuiConfig } from "./config";
4
+ import { sanitizeEditorMetadataText } from "./editor-metadata-format";
5
+ import { renderMinimalistFrame } from "./minimalist-editor";
6
+ import { safeThemeFg } from "./style";
7
+ import { renderPolishedEditorFrame } from "./ui";
8
+ import { renderUserMessageStyle } from "./user-message-styles";
9
+
10
+ export const SETTINGS_PREVIEW_MAX_WIDTH = 72;
11
+ export const SETTINGS_PREVIEW_MAX_ROWS = 8;
12
+ export const EDITOR_PREVIEW_INPUT = "Explain this change safely.";
13
+ export const USER_MESSAGE_PREVIEW_MARKDOWN = "Please review **this change** safely.";
14
+
15
+ function boundedRows(rows: string[], width: number): string[] {
16
+ const safeWidth = Math.max(0, Math.min(SETTINGS_PREVIEW_MAX_WIDTH, width));
17
+ if (safeWidth <= 0) return [];
18
+ const bounded = rows
19
+ .slice(0, SETTINGS_PREVIEW_MAX_ROWS)
20
+ .map((line) => truncateToWidth(line, safeWidth, ""));
21
+ while (bounded.length > 0 && visibleWidth(bounded.at(-1) ?? "") === 0) bounded.pop();
22
+ return bounded;
23
+ }
24
+
25
+ function previewConfig(config: PolishedTuiConfig): PolishedTuiConfig {
26
+ const derived = structuredClone(config);
27
+ derived.icons = Object.fromEntries(
28
+ Object.entries(derived.icons).map(([key, value]) => [
29
+ key,
30
+ typeof value === "string" ? sanitizeEditorMetadataText(value) : value,
31
+ ]),
32
+ ) as typeof derived.icons;
33
+ return derived;
34
+ }
35
+
36
+ function adaptiveBorder(theme: Theme): (text: string) => string {
37
+ return (text) => safeThemeFg(theme, "thinkingHigh", text);
38
+ }
39
+
40
+ export function renderEditorSettingsPreview(
41
+ config: PolishedTuiConfig,
42
+ theme: Theme,
43
+ width: number,
44
+ ): string[] {
45
+ const previewWidth = Math.max(0, Math.min(SETTINGS_PREVIEW_MAX_WIDTH, width));
46
+ if (previewWidth <= 0) return [];
47
+ const safeConfig = previewConfig(config);
48
+ const editor = safeConfig.components.editor;
49
+ const borderColor = adaptiveBorder(theme);
50
+ const modelLabel = editor.modelLabel === "name" ? "Sonnet 4" : "sonnet-4";
51
+ const editorLines = [EDITOR_PREVIEW_INPUT];
52
+ const viewport = editor.viewportIndicators ? { above: "2", below: "3" } : undefined;
53
+ const frame =
54
+ editor.style === "minimalist"
55
+ ? renderMinimalistFrame({
56
+ width: previewWidth,
57
+ editorLines,
58
+ viewport,
59
+ inputText: EDITOR_PREVIEW_INPUT,
60
+ metadata: {
61
+ cwd: "/workspace/zentui/src",
62
+ projectRoot: "/workspace/zentui",
63
+ branch: "feat/settings-previews",
64
+ dirty: true,
65
+ ahead: 2,
66
+ behind: 1,
67
+ costLabel: "$.12",
68
+ modelLabel,
69
+ thinkingLevel: "high",
70
+ contextPercent: 75,
71
+ contextWindow: 372_000,
72
+ sessionName: "Preview",
73
+ agentDurationMs: 12_000,
74
+ agentActive: true,
75
+ },
76
+ uiTheme: theme,
77
+ config: safeConfig,
78
+ borderColor,
79
+ })
80
+ : renderPolishedEditorFrame({
81
+ width: previewWidth,
82
+ editorLines,
83
+ viewport,
84
+ uiTheme: theme,
85
+ config: safeConfig,
86
+ modelMeta: {
87
+ modelLabel,
88
+ modelId: "sonnet-4",
89
+ modelName: "Sonnet 4",
90
+ providerLabel: "Anthropic",
91
+ sessionName: "Preview",
92
+ },
93
+ thinkingLevel: "high",
94
+ borderColor,
95
+ });
96
+ return boundedRows(frame, previewWidth);
97
+ }
98
+
99
+ export function renderUserMessageSettingsPreview(
100
+ config: PolishedTuiConfig,
101
+ theme: Theme,
102
+ width: number,
103
+ text = USER_MESSAGE_PREVIEW_MARKDOWN,
104
+ ): string[] {
105
+ const previewWidth = Math.max(0, Math.min(SETTINGS_PREVIEW_MAX_WIDTH, width));
106
+ if (previewWidth <= 0) return [];
107
+ const safeConfig = previewConfig(config);
108
+ const frame = renderUserMessageStyle({ text, width: previewWidth, theme, config: safeConfig });
109
+ return boundedRows(frame, previewWidth);
110
+ }
@@ -20,7 +20,7 @@ import {
20
20
 
21
21
  const LEGACY_SPLIT_POLISHED_FRAME = Symbol.for("pi-zentui.polished-frame");
22
22
 
23
- type ViewportCounts = {
23
+ export type ViewportCounts = {
24
24
  above?: string;
25
25
  below?: string;
26
26
  };
@@ -75,7 +75,7 @@ type WrappedEditor = EditorComponent &
75
75
  setAutocompleteMaxVisible?: (maxVisible: number) => void;
76
76
  };
77
77
 
78
- type EditorMeta = {
78
+ export type EditorMeta = {
79
79
  modelLabel: string;
80
80
  modelId?: string;
81
81
  modelName?: string;
@@ -83,6 +83,19 @@ type EditorMeta = {
83
83
  sessionName?: string;
84
84
  };
85
85
 
86
+ export type PolishedEditorFrameOptions = {
87
+ width: number;
88
+ editorLines: string[];
89
+ autocompleteLines?: string[];
90
+ viewport?: ViewportCounts;
91
+ uiTheme: Theme;
92
+ config: ZentuiConfig;
93
+ modelMeta: EditorMeta;
94
+ thinkingLevel?: string;
95
+ rightStatus?: string;
96
+ borderColor?: (text: string) => string;
97
+ };
98
+
86
99
  type PolishedFrameOptions = {
87
100
  width: number;
88
101
  baseRendered: string[];
@@ -542,12 +555,6 @@ function renderPolishedFrame({
542
555
  }: PolishedFrameOptions): PolishedFrameResult {
543
556
  if (width <= 2) return { lines: clampRenderedLines(baseRendered, width), decorated: false };
544
557
 
545
- const reset = "\x1b[0m";
546
- const colorSource = config.components.editor.colorSource;
547
- const { prompt, promptWidth, rail, railWidth } = getEditorChromeWidths(config, uiTheme, reset);
548
- const innerWidth = Math.max(0, width - railWidth);
549
- const lowRailContinuation = " ".repeat(promptWidth);
550
-
551
558
  if (baseRendered.length < 2) {
552
559
  return { lines: clampRenderedLines(baseRendered, width), decorated: false };
553
560
  }
@@ -584,6 +591,48 @@ function renderPolishedFrame({
584
591
  above: parsedTop?.count,
585
592
  below: parsedBottom?.count,
586
593
  };
594
+ const lines = renderPolishedEditorFrame({
595
+ width,
596
+ editorLines,
597
+ autocompleteLines,
598
+ viewport,
599
+ uiTheme,
600
+ config,
601
+ modelMeta,
602
+ thinkingLevel,
603
+ rightStatus,
604
+ borderColor,
605
+ });
606
+ POLISHED_FRAME_SPLITS.set(lines, {
607
+ rows: Object.freeze([...lines]),
608
+ split: {
609
+ editorLines,
610
+ trailingLines: autocompleteLines,
611
+ viewport,
612
+ },
613
+ });
614
+ return { lines, decorated: true };
615
+ }
616
+
617
+ /** Pure Opencode frame composition shared by the live editor and settings preview. */
618
+ export function renderPolishedEditorFrame({
619
+ width,
620
+ editorLines,
621
+ autocompleteLines = [],
622
+ viewport = {},
623
+ uiTheme,
624
+ config,
625
+ modelMeta,
626
+ thinkingLevel,
627
+ rightStatus,
628
+ borderColor,
629
+ }: PolishedEditorFrameOptions): string[] {
630
+ if (width <= 2) return clampRenderedLines(editorLines, width);
631
+ const reset = "\x1b[0m";
632
+ const colorSource = config.components.editor.colorSource;
633
+ const { prompt, promptWidth, rail, railWidth } = getEditorChromeWidths(config, uiTheme, reset);
634
+ const innerWidth = Math.max(0, width - railWidth);
635
+ const lowRailContinuation = " ".repeat(promptWidth);
587
636
  const meta = renderEditorMetadataFormat(
588
637
  selectedPolishedConfig(config)?.metadataFormat ??
589
638
  config.components.editor.styles.opencode.metadataFormat,
@@ -658,19 +707,7 @@ function renderPolishedFrame({
658
707
  ...autocompleteLines,
659
708
  ];
660
709
 
661
- const clamped = clampRenderedLines(renderedLines, width);
662
- POLISHED_FRAME_SPLITS.set(clamped, {
663
- rows: Object.freeze([...clamped]),
664
- split: {
665
- editorLines,
666
- trailingLines: autocompleteLines,
667
- viewport,
668
- },
669
- });
670
- return {
671
- lines: clamped,
672
- decorated: true,
673
- };
710
+ return clampRenderedLines(renderedLines, width);
674
711
  }
675
712
 
676
713
  export class PolishedEditor extends CustomEditor {
@@ -7,6 +7,7 @@ import type {
7
7
  WorkingLineTextAnimation,
8
8
  ZentuiConfig,
9
9
  } from "./config";
10
+ import { formatCount } from "./format";
10
11
  import {
11
12
  isSafeSgrStylePrefix,
12
13
  isSupportedColorSpec,
@@ -561,7 +562,7 @@ export function formatWorkingLineTokens(
561
562
  ) {
562
563
  return undefined;
563
564
  }
564
- return `↑${tokens.input} ↓${tokens.output}`;
565
+ return `↑${formatCount(tokens.input)} ↓${formatCount(tokens.output)}`;
565
566
  }
566
567
 
567
568
  function truncateWithEllipsis(value: string, capacity: number): string {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-zentui",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",