pi-zentui 0.1.9 → 0.1.10
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 +4 -2
- package/extensions/zentui/config.ts +52 -23
- package/extensions/zentui/footer.ts +4 -2
- package/extensions/zentui/index.ts +3 -3
- package/extensions/zentui/settings-command.ts +3 -3
- package/extensions/zentui/style.ts +74 -5
- package/extensions/zentui/ui.ts +4 -4
- package/extensions/zentui/user-message.ts +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,13 +121,15 @@ pi install git:github.com/lmilojevicc/pi-zentui
|
|
|
121
121
|
|
|
122
122
|
## Config
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
Zentui uses built-in defaults when no config file exists. User settings live at:
|
|
125
125
|
|
|
126
126
|
```
|
|
127
127
|
~/.pi/agent/zentui.json
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
Zentui treats this file as user-owned and compatibility-sensitive: invalid known values fall back to runtime defaults, unknown keys are ignored at runtime, and `/zentui` patches only the settings it changes instead of rewriting the whole file.
|
|
131
|
+
|
|
132
|
+
### Runtime defaults
|
|
131
133
|
|
|
132
134
|
```json
|
|
133
135
|
{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { isSupportedColorSpec } from "./style";
|
|
4
5
|
|
|
5
6
|
export type ColorSpec = string;
|
|
6
7
|
export type ColorSource = "theme" | "terminal";
|
|
@@ -84,6 +85,22 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
84
85
|
},
|
|
85
86
|
};
|
|
86
87
|
|
|
88
|
+
const iconKeys = [
|
|
89
|
+
"cwd",
|
|
90
|
+
"git",
|
|
91
|
+
"ahead",
|
|
92
|
+
"behind",
|
|
93
|
+
"diverged",
|
|
94
|
+
"conflicted",
|
|
95
|
+
"untracked",
|
|
96
|
+
"stashed",
|
|
97
|
+
"modified",
|
|
98
|
+
"staged",
|
|
99
|
+
"renamed",
|
|
100
|
+
"deleted",
|
|
101
|
+
"typechanged",
|
|
102
|
+
] as const satisfies readonly (keyof PolishedTuiConfig["icons"])[];
|
|
103
|
+
|
|
87
104
|
type ConfigRecord = Record<string, unknown>;
|
|
88
105
|
|
|
89
106
|
function isRecord(value: unknown): value is ConfigRecord {
|
|
@@ -107,6 +124,11 @@ function stringValue(record: Record<string, unknown>, key: string): string | und
|
|
|
107
124
|
return typeof value === "string" ? value : undefined;
|
|
108
125
|
}
|
|
109
126
|
|
|
127
|
+
function colorValue(record: Record<string, unknown>, key: string): string | undefined {
|
|
128
|
+
const value = stringValue(record, key);
|
|
129
|
+
return value !== undefined && isSupportedColorSpec(value) ? value : undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
110
132
|
function colorSourceValue(
|
|
111
133
|
record: Record<string, unknown>,
|
|
112
134
|
key: keyof ColorSourcesConfig,
|
|
@@ -125,18 +147,27 @@ function definedColors(
|
|
|
125
147
|
) as Partial<PolishedTuiConfig["colors"]>;
|
|
126
148
|
}
|
|
127
149
|
|
|
150
|
+
function normalizeIcons(record: Record<string, unknown>): Partial<PolishedTuiConfig["icons"]> {
|
|
151
|
+
return Object.fromEntries(
|
|
152
|
+
iconKeys.flatMap((key) => {
|
|
153
|
+
const value = stringValue(record, key);
|
|
154
|
+
return value === undefined ? [] : [[key, value]];
|
|
155
|
+
}),
|
|
156
|
+
) as Partial<PolishedTuiConfig["icons"]>;
|
|
157
|
+
}
|
|
158
|
+
|
|
128
159
|
function normalizeColors(record: Record<string, unknown>): Partial<PolishedTuiConfig["colors"]> {
|
|
129
160
|
return definedColors({
|
|
130
|
-
cwd:
|
|
131
|
-
gitBranch:
|
|
132
|
-
gitStatus:
|
|
133
|
-
contextNormal:
|
|
134
|
-
contextWarning:
|
|
135
|
-
contextError:
|
|
136
|
-
tokens:
|
|
137
|
-
cost:
|
|
138
|
-
separator:
|
|
139
|
-
runtimePrefix:
|
|
161
|
+
cwd: colorValue(record, "cwd") ?? colorValue(record, "cwdText"),
|
|
162
|
+
gitBranch: colorValue(record, "gitBranch") ?? colorValue(record, "git"),
|
|
163
|
+
gitStatus: colorValue(record, "gitStatus"),
|
|
164
|
+
contextNormal: colorValue(record, "contextNormal"),
|
|
165
|
+
contextWarning: colorValue(record, "contextWarning"),
|
|
166
|
+
contextError: colorValue(record, "contextError"),
|
|
167
|
+
tokens: colorValue(record, "tokens"),
|
|
168
|
+
cost: colorValue(record, "cost"),
|
|
169
|
+
separator: colorValue(record, "separator"),
|
|
170
|
+
runtimePrefix: colorValue(record, "runtimePrefix"),
|
|
140
171
|
});
|
|
141
172
|
}
|
|
142
173
|
|
|
@@ -172,18 +203,17 @@ function readConfigRecord(path = configPath): ConfigRecord {
|
|
|
172
203
|
}
|
|
173
204
|
|
|
174
205
|
export function ensureConfigExists(): void {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
} catch {
|
|
180
|
-
// Ignore config bootstrap failures; extension will fall back to defaults.
|
|
181
|
-
}
|
|
206
|
+
// Intentionally left as a no-op. Zentui config is user-owned and
|
|
207
|
+
// compatibility-sensitive: runtime defaults come from `mergeConfig({})`, and
|
|
208
|
+
// the extension should not persist opinionated defaults unless the user
|
|
209
|
+
// explicitly changes a setting.
|
|
182
210
|
}
|
|
183
211
|
|
|
184
212
|
export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
185
213
|
const config = isRecord(parsed) ? parsed : {};
|
|
186
|
-
const icons = isRecord(config.icons)
|
|
214
|
+
const icons = isRecord(config.icons)
|
|
215
|
+
? normalizeIcons(config.icons as Record<string, unknown>)
|
|
216
|
+
: {};
|
|
187
217
|
const colors = isRecord(config.colors)
|
|
188
218
|
? normalizeColors(config.colors as Record<string, unknown>)
|
|
189
219
|
: {};
|
|
@@ -200,16 +230,16 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
200
230
|
...defaultConfig.colors,
|
|
201
231
|
...colors,
|
|
202
232
|
},
|
|
203
|
-
colorSources,
|
|
233
|
+
colorSources: { ...colorSources },
|
|
204
234
|
};
|
|
205
235
|
}
|
|
206
236
|
|
|
207
237
|
export function loadConfig(): PolishedTuiConfig {
|
|
208
238
|
try {
|
|
209
|
-
if (!existsSync(configPath)) return
|
|
239
|
+
if (!existsSync(configPath)) return mergeConfig({});
|
|
210
240
|
return mergeConfig(JSON.parse(readFileSync(configPath, "utf8")));
|
|
211
241
|
} catch {
|
|
212
|
-
return
|
|
242
|
+
return mergeConfig({});
|
|
213
243
|
}
|
|
214
244
|
}
|
|
215
245
|
|
|
@@ -219,10 +249,9 @@ export function saveColorSourcesPatch(
|
|
|
219
249
|
): PolishedTuiConfig {
|
|
220
250
|
const record = readConfigRecord(path);
|
|
221
251
|
const existing = isRecord(record.colorSources)
|
|
222
|
-
?
|
|
252
|
+
? { ...(record.colorSources as Record<string, unknown>) }
|
|
223
253
|
: {};
|
|
224
254
|
record.colorSources = {
|
|
225
|
-
...defaultConfig.colorSources,
|
|
226
255
|
...existing,
|
|
227
256
|
...validColorSourceEntries(patch),
|
|
228
257
|
};
|
|
@@ -53,7 +53,7 @@ export function installFooter(
|
|
|
53
53
|
renderStyleForSource(theme, colorSource, config.colors.gitBranch, text);
|
|
54
54
|
const gitStatusColor = (text: string) =>
|
|
55
55
|
renderStyleForSource(theme, colorSource, config.colors.gitStatus, text);
|
|
56
|
-
const gitIcon = gitColor(config.icons.git);
|
|
56
|
+
const gitIcon = config.icons.git ? gitColor(config.icons.git) : "";
|
|
57
57
|
const allStatus = [
|
|
58
58
|
state.conflicted > 0 ? config.icons.conflicted : "",
|
|
59
59
|
state.stashed ? config.icons.stashed : "",
|
|
@@ -75,7 +75,9 @@ export function installFooter(
|
|
|
75
75
|
const statusBlock =
|
|
76
76
|
allStatus || aheadBehind ? gitStatusColor(`[${allStatus}${aheadBehind}]`) : "";
|
|
77
77
|
const branchLabel = branch
|
|
78
|
-
?
|
|
78
|
+
? [...["on", gitIcon, gitColor(branch)].filter(Boolean), statusBlock]
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.join(" ")
|
|
79
81
|
: "";
|
|
80
82
|
const runtimeLabel = formatRuntimeSegment(
|
|
81
83
|
theme,
|
|
@@ -19,7 +19,7 @@ import { readRuntimeInfo } from "./runtime";
|
|
|
19
19
|
import { installSelectorBorderStyle } from "./selector-border";
|
|
20
20
|
import { registerZentuiSettingsCommand } from "./settings-command";
|
|
21
21
|
import { type FooterState, createInitialState, syncState } from "./state";
|
|
22
|
-
import { renderAccentLine } from "./style";
|
|
22
|
+
import { renderAccentLine, safeThemeFg } from "./style";
|
|
23
23
|
import { PolishedEditor } from "./ui";
|
|
24
24
|
import { installUserMessageStyle } from "./user-message";
|
|
25
25
|
|
|
@@ -88,8 +88,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
88
88
|
getCurrentConfig().colorSources.editor,
|
|
89
89
|
state.modelLabel,
|
|
90
90
|
),
|
|
91
|
-
ctx.ui.theme
|
|
92
|
-
].join(ctx.ui.theme
|
|
91
|
+
safeThemeFg(ctx.ui.theme, "text", state.providerLabel),
|
|
92
|
+
].join(safeThemeFg(ctx.ui.theme, "borderMuted", " ")),
|
|
93
93
|
getThinkingLevel,
|
|
94
94
|
),
|
|
95
95
|
);
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
truncateToWidth,
|
|
8
8
|
} from "@earendil-works/pi-tui";
|
|
9
9
|
import type { ColorSource, ColorSourcesConfig, PolishedTuiConfig } from "./config";
|
|
10
|
-
import { EDITOR_BORDER_STYLE, renderChromeBorder } from "./style";
|
|
10
|
+
import { EDITOR_BORDER_STYLE, renderChromeBorder, safeThemeFg } from "./style";
|
|
11
11
|
|
|
12
12
|
const colorSourceValues: ColorSource[] = ["theme", "terminal"];
|
|
13
13
|
|
|
@@ -97,8 +97,8 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
|
|
|
97
97
|
EDITOR_BORDER_STYLE,
|
|
98
98
|
"─".repeat(Math.max(0, width)),
|
|
99
99
|
);
|
|
100
|
-
const header = theme
|
|
101
|
-
const hint = theme
|
|
100
|
+
const header = safeThemeFg(theme, "accent", theme.bold("Zentui settings"));
|
|
101
|
+
const hint = safeThemeFg(theme, "muted", "Enter/Space cycles values · Esc closes");
|
|
102
102
|
return [
|
|
103
103
|
truncateToWidth(border, width, ""),
|
|
104
104
|
truncateToWidth(header, width, ""),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import type { ColorSource, ColorSpec } from "./config";
|
|
2
3
|
|
|
3
4
|
type ThemeLike = {
|
|
@@ -72,6 +73,54 @@ const themeColorNameMap = new Map([
|
|
|
72
73
|
|
|
73
74
|
const themeStyleModifiers = new Set(["bold", "italic", "underline"]);
|
|
74
75
|
|
|
76
|
+
const themeColorTokens = new Set<ThemeColor>([
|
|
77
|
+
"accent",
|
|
78
|
+
"border",
|
|
79
|
+
"borderAccent",
|
|
80
|
+
"borderMuted",
|
|
81
|
+
"success",
|
|
82
|
+
"error",
|
|
83
|
+
"warning",
|
|
84
|
+
"muted",
|
|
85
|
+
"dim",
|
|
86
|
+
"text",
|
|
87
|
+
"thinkingText",
|
|
88
|
+
"userMessageText",
|
|
89
|
+
"customMessageText",
|
|
90
|
+
"customMessageLabel",
|
|
91
|
+
"toolTitle",
|
|
92
|
+
"toolOutput",
|
|
93
|
+
"mdHeading",
|
|
94
|
+
"mdLink",
|
|
95
|
+
"mdLinkUrl",
|
|
96
|
+
"mdCode",
|
|
97
|
+
"mdCodeBlock",
|
|
98
|
+
"mdCodeBlockBorder",
|
|
99
|
+
"mdQuote",
|
|
100
|
+
"mdQuoteBorder",
|
|
101
|
+
"mdHr",
|
|
102
|
+
"mdListBullet",
|
|
103
|
+
"toolDiffAdded",
|
|
104
|
+
"toolDiffRemoved",
|
|
105
|
+
"toolDiffContext",
|
|
106
|
+
"syntaxComment",
|
|
107
|
+
"syntaxKeyword",
|
|
108
|
+
"syntaxFunction",
|
|
109
|
+
"syntaxVariable",
|
|
110
|
+
"syntaxString",
|
|
111
|
+
"syntaxNumber",
|
|
112
|
+
"syntaxType",
|
|
113
|
+
"syntaxOperator",
|
|
114
|
+
"syntaxPunctuation",
|
|
115
|
+
"thinkingOff",
|
|
116
|
+
"thinkingMinimal",
|
|
117
|
+
"thinkingLow",
|
|
118
|
+
"thinkingMedium",
|
|
119
|
+
"thinkingHigh",
|
|
120
|
+
"thinkingXhigh",
|
|
121
|
+
"bashMode",
|
|
122
|
+
]);
|
|
123
|
+
|
|
75
124
|
function terminalColorToAnsi(color: string, isBackground = false): string | undefined {
|
|
76
125
|
const normalized = color.toLowerCase();
|
|
77
126
|
const colorCode = terminalColorCodes.get(normalized);
|
|
@@ -92,6 +141,26 @@ function isExplicitTerminalColorToken(token: string): boolean {
|
|
|
92
141
|
return /^(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(normalized);
|
|
93
142
|
}
|
|
94
143
|
|
|
144
|
+
function isSupportedStyleToken(token: string): boolean {
|
|
145
|
+
const normalized = token.toLowerCase();
|
|
146
|
+
if (terminalStyleModifiers.has(normalized)) return true;
|
|
147
|
+
if (terminalColorToAnsi(normalized) !== undefined) return true;
|
|
148
|
+
|
|
149
|
+
const isForeground = normalized.startsWith("fg:");
|
|
150
|
+
const isBackground = normalized.startsWith("bg:");
|
|
151
|
+
if (isForeground || isBackground) {
|
|
152
|
+
return terminalColorToAnsi(normalized.slice(3), isBackground) !== undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return themeColorTokens.has(token as ThemeColor);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function isSupportedColorSpec(style: ColorSpec): boolean {
|
|
159
|
+
const trimmed = style.trim();
|
|
160
|
+
if (trimmed === "") return true;
|
|
161
|
+
return trimmed.split(/\s+/).every(isSupportedStyleToken);
|
|
162
|
+
}
|
|
163
|
+
|
|
95
164
|
function applyThemeModifiers(theme: ThemeLike, styleTokens: string[], text: string): string {
|
|
96
165
|
let rendered = text;
|
|
97
166
|
for (const token of styleTokens) {
|
|
@@ -103,7 +172,7 @@ function applyThemeModifiers(theme: ThemeLike, styleTokens: string[], text: stri
|
|
|
103
172
|
return rendered;
|
|
104
173
|
}
|
|
105
174
|
|
|
106
|
-
function
|
|
175
|
+
export function safeThemeFg(theme: ThemeLike, color: string, text: string): string {
|
|
107
176
|
try {
|
|
108
177
|
return theme.fg(color, text);
|
|
109
178
|
} catch {
|
|
@@ -137,7 +206,7 @@ export function colorize(theme: ThemeLike, color: ColorSpec, text: string): stri
|
|
|
137
206
|
if (isHexColor(color)) {
|
|
138
207
|
return `${hexToAnsi(color)}${text}\x1b[39m`;
|
|
139
208
|
}
|
|
140
|
-
return
|
|
209
|
+
return safeThemeFg(theme, color, text);
|
|
141
210
|
}
|
|
142
211
|
|
|
143
212
|
/**
|
|
@@ -184,7 +253,7 @@ export function renderThemeStyle(theme: ThemeLike, style: ColorSpec, text: strin
|
|
|
184
253
|
if (tokens.some(isExplicitTerminalColorToken)) return renderTerminalStyle(style, text);
|
|
185
254
|
|
|
186
255
|
const color = mapThemeColor(tokens) ?? "text";
|
|
187
|
-
return
|
|
256
|
+
return safeThemeFg(theme, color, applyThemeModifiers(theme, tokens, text));
|
|
188
257
|
}
|
|
189
258
|
|
|
190
259
|
export function renderStyleForSource(
|
|
@@ -207,7 +276,7 @@ export function renderEditorBorder(text: string): string {
|
|
|
207
276
|
}
|
|
208
277
|
|
|
209
278
|
export function renderAccentLine(theme: ThemeLike, source: ColorSource, text: string): string {
|
|
210
|
-
return source === "terminal" ? renderEditorAccent(text) :
|
|
279
|
+
return source === "terminal" ? renderEditorAccent(text) : safeThemeFg(theme, "accent", text);
|
|
211
280
|
}
|
|
212
281
|
|
|
213
282
|
export function renderChromeBorder(
|
|
@@ -217,5 +286,5 @@ export function renderChromeBorder(
|
|
|
217
286
|
text: string,
|
|
218
287
|
): string {
|
|
219
288
|
if (source === "terminal") return renderTerminalStyle(terminalFallbackStyle, text);
|
|
220
|
-
return
|
|
289
|
+
return safeThemeFg(theme, "borderMuted", text);
|
|
221
290
|
}
|
package/extensions/zentui/ui.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
visibleWidth,
|
|
8
8
|
} from "@earendil-works/pi-tui";
|
|
9
9
|
import type { PolishedTuiConfig } from "./config";
|
|
10
|
-
import { EDITOR_BORDER_STYLE, renderAccentLine, renderChromeBorder } from "./style";
|
|
10
|
+
import { EDITOR_BORDER_STYLE, renderAccentLine, renderChromeBorder, safeThemeFg } from "./style";
|
|
11
11
|
|
|
12
12
|
type AutocompleteEditorInternals = {
|
|
13
13
|
autocompleteList?: Pick<Component, "render">;
|
|
@@ -36,7 +36,7 @@ export class PolishedEditor extends CustomEditor {
|
|
|
36
36
|
getThinkingLevel: () => string | undefined,
|
|
37
37
|
) {
|
|
38
38
|
super(tui, theme, keybindings, { paddingX: 0 });
|
|
39
|
-
this.borderColor = (text: string) => uiTheme
|
|
39
|
+
this.borderColor = (text: string) => safeThemeFg(uiTheme, "border", text);
|
|
40
40
|
this.uiTheme = uiTheme;
|
|
41
41
|
this.getConfig = getConfig;
|
|
42
42
|
this.getModelMeta = getModelMeta;
|
|
@@ -88,9 +88,9 @@ export class PolishedEditor extends CustomEditor {
|
|
|
88
88
|
const metaParts = [this.getModelMeta()];
|
|
89
89
|
const thinkingLevel = this.getThinkingLevel();
|
|
90
90
|
if (thinkingLevel && thinkingLevel !== "off") {
|
|
91
|
-
metaParts.push(this.uiTheme
|
|
91
|
+
metaParts.push(safeThemeFg(this.uiTheme, "muted", thinkingLevel));
|
|
92
92
|
}
|
|
93
|
-
const meta = metaParts.filter(Boolean).join(this.uiTheme
|
|
93
|
+
const meta = metaParts.filter(Boolean).join(safeThemeFg(this.uiTheme, "border", " "));
|
|
94
94
|
|
|
95
95
|
const colorSource = this.getConfig().colorSources.editor;
|
|
96
96
|
const rail = `${renderAccentLine(this.uiTheme, colorSource, "│")}${this.reset} `;
|
|
@@ -52,7 +52,12 @@ function findMarkdownText(value: unknown): string | undefined {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function themeFg(theme: Theme | undefined, color: ThemeColor, text: string): string {
|
|
55
|
-
|
|
55
|
+
if (!theme) return text;
|
|
56
|
+
try {
|
|
57
|
+
return theme.fg(color, text);
|
|
58
|
+
} catch {
|
|
59
|
+
return text;
|
|
60
|
+
}
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
function makeMarkdownTheme(theme: Theme | undefined): MarkdownTheme {
|