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
|
|
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,
|
|
197
|
-
3. **User messages** — message enablement, `framed | framed-copy-friendly | compact | labeled` style selection (including **Framed (copy-friendly)**), and
|
|
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
|
-
...
|
|
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
|
+
}
|
package/extensions/zentui/ui.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|