pi-zentui 0.1.8 → 0.1.9
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 +31 -16
- package/extensions/zentui/config.ts +118 -13
- package/extensions/zentui/footer.ts +22 -12
- package/extensions/zentui/format.ts +5 -3
- package/extensions/zentui/index.ts +46 -6
- package/extensions/zentui/selector-border.ts +106 -0
- package/extensions/zentui/settings-command.ts +121 -0
- package/extensions/zentui/style.ts +138 -11
- package/extensions/zentui/ui.ts +19 -3
- package/extensions/zentui/user-message.ts +57 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,12 +20,12 @@ Zentui brings two popular aesthetics to Pi:
|
|
|
20
20
|
- ` dirname` — current directory with icon
|
|
21
21
|
- `on branch` — git branch with icon
|
|
22
22
|
- `[!?↑]` — git status indicators (modified, untracked, ahead/behind, stashed, etc.)
|
|
23
|
-
- `via v5.5.0` — runtime detection with version and Starship
|
|
23
|
+
- `via v5.5.0` — runtime detection with version and Starship-style Nerd Font runtime/language modules
|
|
24
24
|
- Right side shows context usage, token counts, and cost
|
|
25
25
|
|
|
26
26
|
### Editor (Opencode-inspired)
|
|
27
27
|
|
|
28
|
-
- Bordered input box with accent-
|
|
28
|
+
- Bordered input box with theme accent rail and thinking-level border color
|
|
29
29
|
- Model name and provider displayed inside the editor frame
|
|
30
30
|
- Thinking level indicator when enabled
|
|
31
31
|
- Prompt-box-style user messages matching the ZentUI input chrome
|
|
@@ -47,7 +47,7 @@ Zentui brings two popular aesthetics to Pi:
|
|
|
47
47
|
|
|
48
48
|
### Runtime Detection
|
|
49
49
|
|
|
50
|
-
Detects Starship Nerd Font runtime/language modules, uses the Starship Nerd Font symbols, and
|
|
50
|
+
Detects Starship Nerd Font runtime/language modules, uses the Starship Nerd Font symbols, and keeps Starship-style defaults such as `bold green` for Node.js. By default Zentui maps those styles through your active Pi theme; switch the Starship/footer color source to `terminal` in `/zentui` if you want your terminal colorscheme to supply the exact ANSI colors.
|
|
51
51
|
|
|
52
52
|
| Runtime/language | Detection examples |
|
|
53
53
|
| ---------------- | ------------------------------------------------------------ |
|
|
@@ -148,15 +148,21 @@ On first run, Zentui creates a config file at:
|
|
|
148
148
|
"typechanged": "T"
|
|
149
149
|
},
|
|
150
150
|
"colors": {
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"gitStatus": "
|
|
154
|
-
"contextNormal": "
|
|
155
|
-
"contextWarning": "
|
|
156
|
-
"contextError": "
|
|
157
|
-
"tokens": "
|
|
158
|
-
"cost": "
|
|
159
|
-
"separator": "
|
|
151
|
+
"cwd": "bold cyan",
|
|
152
|
+
"gitBranch": "bold purple",
|
|
153
|
+
"gitStatus": "bold red",
|
|
154
|
+
"contextNormal": "dimmed",
|
|
155
|
+
"contextWarning": "bold yellow",
|
|
156
|
+
"contextError": "bold red",
|
|
157
|
+
"tokens": "dimmed",
|
|
158
|
+
"cost": "bold green",
|
|
159
|
+
"separator": "dimmed",
|
|
160
|
+
"runtimePrefix": ""
|
|
161
|
+
},
|
|
162
|
+
"colorSources": {
|
|
163
|
+
"starship": "theme",
|
|
164
|
+
"editor": "theme",
|
|
165
|
+
"userMessages": "theme"
|
|
160
166
|
}
|
|
161
167
|
}
|
|
162
168
|
```
|
|
@@ -165,12 +171,21 @@ On first run, Zentui creates a config file at:
|
|
|
165
171
|
|
|
166
172
|
### Color values
|
|
167
173
|
|
|
168
|
-
|
|
174
|
+
Use `/zentui` inside Pi to switch color sources between Pi theme colors and terminal colors:
|
|
175
|
+
|
|
176
|
+
- `starship` — footer/runtime/git/context/cost colors
|
|
177
|
+
- `editor + previous messages` — input editor and previous user-message rails/borders
|
|
178
|
+
|
|
179
|
+
Both settings default to `theme`. The config still stores editor and previous user-message sources separately as `editor` and `userMessages`, but `/zentui` changes them together so the prompt chrome stays consistent.
|
|
180
|
+
|
|
181
|
+
Color values can use terminal-palette style strings, hex colors, or Pi theme color tokens:
|
|
169
182
|
|
|
170
|
-
-
|
|
171
|
-
-
|
|
183
|
+
- Named terminal colors: `bold purple`, `yellow`, `bright-cyan`
|
|
184
|
+
- 256-color / hex: `bold 149`, `fg:202`, `#89b4fa`
|
|
185
|
+
- Backgrounds and modifiers: `bg:blue fg:bright-green`, `underline bg:#bf5700`
|
|
186
|
+
- Pi theme tokens: `accent`, `borderMuted`, `syntaxKeyword`
|
|
172
187
|
|
|
173
|
-
|
|
188
|
+
Use the `colors` config key for these values.
|
|
174
189
|
|
|
175
190
|
## Requirements
|
|
176
191
|
|
|
@@ -3,6 +3,13 @@ import { join } from "node:path";
|
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
export type ColorSpec = string;
|
|
6
|
+
export type ColorSource = "theme" | "terminal";
|
|
7
|
+
|
|
8
|
+
export type ColorSourcesConfig = {
|
|
9
|
+
starship: ColorSource;
|
|
10
|
+
editor: ColorSource;
|
|
11
|
+
userMessages: ColorSource;
|
|
12
|
+
};
|
|
6
13
|
|
|
7
14
|
const DEFAULT_PROJECT_REFRESH_INTERVAL_MS = 30_000;
|
|
8
15
|
const MIN_PROJECT_REFRESH_INTERVAL_MS = 5_000;
|
|
@@ -25,8 +32,8 @@ export type PolishedTuiConfig = {
|
|
|
25
32
|
typechanged: string;
|
|
26
33
|
};
|
|
27
34
|
colors: {
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
cwd: ColorSpec;
|
|
36
|
+
gitBranch: ColorSpec;
|
|
30
37
|
gitStatus: ColorSpec;
|
|
31
38
|
contextNormal: ColorSpec;
|
|
32
39
|
contextWarning: ColorSpec;
|
|
@@ -34,7 +41,9 @@ export type PolishedTuiConfig = {
|
|
|
34
41
|
tokens: ColorSpec;
|
|
35
42
|
cost: ColorSpec;
|
|
36
43
|
separator: ColorSpec;
|
|
44
|
+
runtimePrefix: ColorSpec;
|
|
37
45
|
};
|
|
46
|
+
colorSources: ColorSourcesConfig;
|
|
38
47
|
};
|
|
39
48
|
|
|
40
49
|
export const configPath = join(getAgentDir(), "zentui.json");
|
|
@@ -43,7 +52,7 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
43
52
|
projectRefreshIntervalMs: DEFAULT_PROJECT_REFRESH_INTERVAL_MS,
|
|
44
53
|
icons: {
|
|
45
54
|
cwd: "",
|
|
46
|
-
git: "",
|
|
55
|
+
git: "",
|
|
47
56
|
ahead: "↑",
|
|
48
57
|
behind: "↓",
|
|
49
58
|
diverged: "⇕",
|
|
@@ -57,15 +66,21 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
57
66
|
typechanged: "T",
|
|
58
67
|
},
|
|
59
68
|
colors: {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
gitStatus: "
|
|
63
|
-
contextNormal: "
|
|
64
|
-
contextWarning: "
|
|
65
|
-
contextError: "
|
|
66
|
-
tokens: "
|
|
67
|
-
cost: "
|
|
68
|
-
separator: "
|
|
69
|
+
cwd: "bold cyan",
|
|
70
|
+
gitBranch: "bold purple",
|
|
71
|
+
gitStatus: "bold red",
|
|
72
|
+
contextNormal: "dimmed",
|
|
73
|
+
contextWarning: "bold yellow",
|
|
74
|
+
contextError: "bold red",
|
|
75
|
+
tokens: "dimmed",
|
|
76
|
+
cost: "bold green",
|
|
77
|
+
separator: "dimmed",
|
|
78
|
+
runtimePrefix: "",
|
|
79
|
+
},
|
|
80
|
+
colorSources: {
|
|
81
|
+
starship: "theme",
|
|
82
|
+
editor: "theme",
|
|
83
|
+
userMessages: "theme",
|
|
69
84
|
},
|
|
70
85
|
};
|
|
71
86
|
|
|
@@ -87,6 +102,75 @@ function parseProjectRefreshIntervalMs(value: unknown): number {
|
|
|
87
102
|
: defaultConfig.projectRefreshIntervalMs;
|
|
88
103
|
}
|
|
89
104
|
|
|
105
|
+
function stringValue(record: Record<string, unknown>, key: string): string | undefined {
|
|
106
|
+
const value = record[key];
|
|
107
|
+
return typeof value === "string" ? value : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function colorSourceValue(
|
|
111
|
+
record: Record<string, unknown>,
|
|
112
|
+
key: keyof ColorSourcesConfig,
|
|
113
|
+
): ColorSource {
|
|
114
|
+
const value = record[key];
|
|
115
|
+
return value === "terminal" || value === "theme" ? value : defaultConfig.colorSources[key];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function definedColors(
|
|
119
|
+
colors: Partial<Record<keyof PolishedTuiConfig["colors"], string | undefined>>,
|
|
120
|
+
): Partial<PolishedTuiConfig["colors"]> {
|
|
121
|
+
return Object.fromEntries(
|
|
122
|
+
Object.entries(colors).filter(
|
|
123
|
+
(entry): entry is [keyof PolishedTuiConfig["colors"], string] => typeof entry[1] === "string",
|
|
124
|
+
),
|
|
125
|
+
) as Partial<PolishedTuiConfig["colors"]>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeColors(record: Record<string, unknown>): Partial<PolishedTuiConfig["colors"]> {
|
|
129
|
+
return definedColors({
|
|
130
|
+
cwd: stringValue(record, "cwd") ?? stringValue(record, "cwdText"),
|
|
131
|
+
gitBranch: stringValue(record, "gitBranch") ?? stringValue(record, "git"),
|
|
132
|
+
gitStatus: stringValue(record, "gitStatus"),
|
|
133
|
+
contextNormal: stringValue(record, "contextNormal"),
|
|
134
|
+
contextWarning: stringValue(record, "contextWarning"),
|
|
135
|
+
contextError: stringValue(record, "contextError"),
|
|
136
|
+
tokens: stringValue(record, "tokens"),
|
|
137
|
+
cost: stringValue(record, "cost"),
|
|
138
|
+
separator: stringValue(record, "separator"),
|
|
139
|
+
runtimePrefix: stringValue(record, "runtimePrefix"),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeColorSources(record: Record<string, unknown>): ColorSourcesConfig {
|
|
144
|
+
return {
|
|
145
|
+
starship: colorSourceValue(record, "starship"),
|
|
146
|
+
editor: colorSourceValue(record, "editor"),
|
|
147
|
+
userMessages: colorSourceValue(record, "userMessages"),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isColorSourceKey(value: string): value is keyof ColorSourcesConfig {
|
|
152
|
+
return value === "starship" || value === "editor" || value === "userMessages";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function validColorSourceEntries(record: Record<string, unknown>): Partial<ColorSourcesConfig> {
|
|
156
|
+
return Object.fromEntries(
|
|
157
|
+
Object.entries(record).filter((entry): entry is [keyof ColorSourcesConfig, ColorSource] => {
|
|
158
|
+
const [key, value] = entry;
|
|
159
|
+
return isColorSourceKey(key) && (value === "theme" || value === "terminal");
|
|
160
|
+
}),
|
|
161
|
+
) as Partial<ColorSourcesConfig>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readConfigRecord(path = configPath): ConfigRecord {
|
|
165
|
+
try {
|
|
166
|
+
if (!existsSync(path)) return {};
|
|
167
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
168
|
+
return isRecord(parsed) ? parsed : {};
|
|
169
|
+
} catch {
|
|
170
|
+
return {};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
90
174
|
export function ensureConfigExists(): void {
|
|
91
175
|
try {
|
|
92
176
|
if (!existsSync(configPath)) {
|
|
@@ -101,8 +185,11 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
101
185
|
const config = isRecord(parsed) ? parsed : {};
|
|
102
186
|
const icons = isRecord(config.icons) ? (config.icons as Partial<PolishedTuiConfig["icons"]>) : {};
|
|
103
187
|
const colors = isRecord(config.colors)
|
|
104
|
-
? (config.colors as
|
|
188
|
+
? normalizeColors(config.colors as Record<string, unknown>)
|
|
105
189
|
: {};
|
|
190
|
+
const colorSources = isRecord(config.colorSources)
|
|
191
|
+
? normalizeColorSources(config.colorSources as Record<string, unknown>)
|
|
192
|
+
: defaultConfig.colorSources;
|
|
106
193
|
return {
|
|
107
194
|
projectRefreshIntervalMs: parseProjectRefreshIntervalMs(config.projectRefreshIntervalMs),
|
|
108
195
|
icons: {
|
|
@@ -113,6 +200,7 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
113
200
|
...defaultConfig.colors,
|
|
114
201
|
...colors,
|
|
115
202
|
},
|
|
203
|
+
colorSources,
|
|
116
204
|
};
|
|
117
205
|
}
|
|
118
206
|
|
|
@@ -124,3 +212,20 @@ export function loadConfig(): PolishedTuiConfig {
|
|
|
124
212
|
return defaultConfig;
|
|
125
213
|
}
|
|
126
214
|
}
|
|
215
|
+
|
|
216
|
+
export function saveColorSourcesPatch(
|
|
217
|
+
patch: Partial<ColorSourcesConfig>,
|
|
218
|
+
path = configPath,
|
|
219
|
+
): PolishedTuiConfig {
|
|
220
|
+
const record = readConfigRecord(path);
|
|
221
|
+
const existing = isRecord(record.colorSources)
|
|
222
|
+
? validColorSourceEntries(record.colorSources as Record<string, unknown>)
|
|
223
|
+
: {};
|
|
224
|
+
record.colorSources = {
|
|
225
|
+
...defaultConfig.colorSources,
|
|
226
|
+
...existing,
|
|
227
|
+
...validColorSourceEntries(patch),
|
|
228
|
+
};
|
|
229
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
230
|
+
return mergeConfig(record);
|
|
231
|
+
}
|
|
@@ -3,12 +3,12 @@ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import type { PolishedTuiConfig } from "./config";
|
|
4
4
|
import { formatCwdLabel, formatRuntimeSegment } from "./format";
|
|
5
5
|
import type { FooterState } from "./state";
|
|
6
|
-
import {
|
|
6
|
+
import { renderStyleForSource } from "./style";
|
|
7
7
|
|
|
8
8
|
export function installFooter(
|
|
9
9
|
ctx: ExtensionContext,
|
|
10
10
|
state: FooterState,
|
|
11
|
-
|
|
11
|
+
getConfig: () => PolishedTuiConfig,
|
|
12
12
|
hooks: {
|
|
13
13
|
setRequestRender: (fn: (() => void) | undefined) => void;
|
|
14
14
|
scheduleProjectRefresh: (ctx: ExtensionContext) => void;
|
|
@@ -20,7 +20,6 @@ export function installFooter(
|
|
|
20
20
|
hooks.scheduleProjectRefresh(ctx);
|
|
21
21
|
tui.requestRender();
|
|
22
22
|
});
|
|
23
|
-
const separator = colorize(theme, config.colors.separator, " | ");
|
|
24
23
|
|
|
25
24
|
return {
|
|
26
25
|
dispose: () => {
|
|
@@ -30,10 +29,14 @@ export function installFooter(
|
|
|
30
29
|
invalidate() {},
|
|
31
30
|
render(width: number): string[] {
|
|
32
31
|
if (width <= 0) return [""];
|
|
32
|
+
const config = getConfig();
|
|
33
|
+
const colorSource = config.colorSources.starship;
|
|
34
|
+
const separator = renderStyleForSource(theme, colorSource, config.colors.separator, " | ");
|
|
33
35
|
const innerWidth = Math.max(1, width - 2);
|
|
34
|
-
const cwdLabel =
|
|
36
|
+
const cwdLabel = renderStyleForSource(
|
|
35
37
|
theme,
|
|
36
|
-
|
|
38
|
+
colorSource,
|
|
39
|
+
config.colors.cwd,
|
|
37
40
|
formatCwdLabel(ctx.cwd, config.icons.cwd),
|
|
38
41
|
);
|
|
39
42
|
const branch = state.branch;
|
|
@@ -46,8 +49,10 @@ export function installFooter(
|
|
|
46
49
|
? config.colors.contextWarning
|
|
47
50
|
: config.colors.contextNormal
|
|
48
51
|
: config.colors.contextNormal;
|
|
49
|
-
const gitColor = (text: string) =>
|
|
50
|
-
|
|
52
|
+
const gitColor = (text: string) =>
|
|
53
|
+
renderStyleForSource(theme, colorSource, config.colors.gitBranch, text);
|
|
54
|
+
const gitStatusColor = (text: string) =>
|
|
55
|
+
renderStyleForSource(theme, colorSource, config.colors.gitStatus, text);
|
|
51
56
|
const gitIcon = gitColor(config.icons.git);
|
|
52
57
|
const allStatus = [
|
|
53
58
|
state.conflicted > 0 ? config.icons.conflicted : "",
|
|
@@ -70,15 +75,20 @@ export function installFooter(
|
|
|
70
75
|
const statusBlock =
|
|
71
76
|
allStatus || aheadBehind ? gitStatusColor(`[${allStatus}${aheadBehind}]`) : "";
|
|
72
77
|
const branchLabel = branch
|
|
73
|
-
?
|
|
78
|
+
? `on ${gitIcon} ${gitColor(branch)}${statusBlock ? ` ${statusBlock}` : ""}`
|
|
74
79
|
: "";
|
|
75
|
-
const runtimeLabel = formatRuntimeSegment(
|
|
80
|
+
const runtimeLabel = formatRuntimeSegment(
|
|
81
|
+
theme,
|
|
82
|
+
state.runtime,
|
|
83
|
+
config.colors.runtimePrefix,
|
|
84
|
+
colorSource,
|
|
85
|
+
);
|
|
76
86
|
|
|
77
87
|
const left = [cwdLabel, branchLabel, runtimeLabel].filter(Boolean).join(" ");
|
|
78
88
|
const right = [
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
renderStyleForSource(theme, colorSource, contextColor, state.contextLabel),
|
|
90
|
+
renderStyleForSource(theme, colorSource, config.colors.tokens, state.tokenLabel),
|
|
91
|
+
renderStyleForSource(theme, colorSource, config.colors.cost, state.costLabel),
|
|
82
92
|
].join(separator);
|
|
83
93
|
|
|
84
94
|
const leftWidth = visibleWidth(left);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { ColorSource, ColorSpec } from "./config";
|
|
3
4
|
import type { RuntimeInfo } from "./runtime";
|
|
4
|
-
import {
|
|
5
|
+
import { renderStyleForSource } from "./style";
|
|
5
6
|
|
|
6
7
|
export type UsageTotals = {
|
|
7
8
|
input: number;
|
|
@@ -70,11 +71,12 @@ export function buildContextLabel(ctx: ExtensionContext): string {
|
|
|
70
71
|
export function formatRuntimeSegment(
|
|
71
72
|
theme: Pick<Theme, "fg">,
|
|
72
73
|
runtime: RuntimeInfo | undefined,
|
|
73
|
-
|
|
74
|
+
prefixStyle: ColorSpec,
|
|
75
|
+
colorSource: ColorSource,
|
|
74
76
|
): string {
|
|
75
77
|
if (!runtime) return "";
|
|
76
78
|
const label = runtime.version ? `${runtime.symbol} ${runtime.version}` : runtime.symbol;
|
|
77
|
-
return `${
|
|
79
|
+
return `${renderStyleForSource(theme, colorSource, prefixStyle, "via")} ${renderStyleForSource(theme, colorSource, runtime.style, label)}`;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
export function formatCwdLabel(cwd: string, cwdIcon: string): string {
|
|
@@ -5,12 +5,21 @@ import type {
|
|
|
5
5
|
Theme,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
type ColorSourcesConfig,
|
|
10
|
+
type PolishedTuiConfig,
|
|
11
|
+
ensureConfigExists,
|
|
12
|
+
loadConfig,
|
|
13
|
+
saveColorSourcesPatch,
|
|
14
|
+
} from "./config";
|
|
9
15
|
import { installFooter } from "./footer";
|
|
10
16
|
import { emptyGitStatus, readGitStatus } from "./git";
|
|
11
17
|
import { type StopProjectRefreshInterval, startProjectRefreshInterval } from "./project-refresh";
|
|
12
18
|
import { readRuntimeInfo } from "./runtime";
|
|
19
|
+
import { installSelectorBorderStyle } from "./selector-border";
|
|
20
|
+
import { registerZentuiSettingsCommand } from "./settings-command";
|
|
13
21
|
import { type FooterState, createInitialState, syncState } from "./state";
|
|
22
|
+
import { renderAccentLine } from "./style";
|
|
14
23
|
import { PolishedEditor } from "./ui";
|
|
15
24
|
import { installUserMessageStyle } from "./user-message";
|
|
16
25
|
|
|
@@ -21,11 +30,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
21
30
|
let activeTheme: Theme | undefined;
|
|
22
31
|
let requestFooterRender: (() => void) | undefined;
|
|
23
32
|
let stopRefreshInterval: StopProjectRefreshInterval = () => {};
|
|
33
|
+
let cleanupPrototypePatches: () => void = () => {};
|
|
24
34
|
let projectRefreshInFlight = false;
|
|
25
35
|
let projectRefreshPending = false;
|
|
26
36
|
|
|
27
37
|
const refresh = () => requestFooterRender?.();
|
|
28
38
|
const getActiveTheme = () => activeTheme;
|
|
39
|
+
const getCurrentConfig = () => currentConfig;
|
|
40
|
+
const getThinkingLevel = () => pi.getThinkingLevel();
|
|
29
41
|
|
|
30
42
|
const refreshProjectState = async (ctx: ExtensionContext) => {
|
|
31
43
|
const [gitStatus, runtime] = await Promise.all([
|
|
@@ -68,12 +80,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
68
80
|
theme,
|
|
69
81
|
keybindings,
|
|
70
82
|
ctx.ui.theme,
|
|
83
|
+
getCurrentConfig,
|
|
71
84
|
() =>
|
|
72
85
|
[
|
|
73
|
-
|
|
86
|
+
renderAccentLine(
|
|
87
|
+
ctx.ui.theme,
|
|
88
|
+
getCurrentConfig().colorSources.editor,
|
|
89
|
+
state.modelLabel,
|
|
90
|
+
),
|
|
74
91
|
ctx.ui.theme.fg("text", state.providerLabel),
|
|
75
92
|
].join(ctx.ui.theme.fg("borderMuted", " ")),
|
|
76
|
-
|
|
93
|
+
getThinkingLevel,
|
|
77
94
|
),
|
|
78
95
|
);
|
|
79
96
|
};
|
|
@@ -81,18 +98,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
81
98
|
const installUi = (ctx: ExtensionContext) => {
|
|
82
99
|
if (!ctx.hasUI) return;
|
|
83
100
|
activeTheme = ctx.ui.theme;
|
|
84
|
-
|
|
101
|
+
cleanupPrototypePatches();
|
|
102
|
+
const cleanupSelectorBorderStyle = installSelectorBorderStyle(getActiveTheme, getCurrentConfig);
|
|
103
|
+
const cleanupUserMessageStyle = installUserMessageStyle(getActiveTheme, getCurrentConfig);
|
|
104
|
+
cleanupPrototypePatches = () => {
|
|
105
|
+
cleanupSelectorBorderStyle();
|
|
106
|
+
cleanupUserMessageStyle();
|
|
107
|
+
};
|
|
85
108
|
ensureConfigExists();
|
|
86
109
|
currentConfig = loadConfig();
|
|
87
110
|
syncState(state, ctx);
|
|
88
|
-
|
|
111
|
+
stopRefreshInterval();
|
|
112
|
+
stopRefreshInterval = () => {};
|
|
113
|
+
installFooter(ctx, state, getCurrentConfig, {
|
|
89
114
|
setRequestRender: (fn) => {
|
|
90
115
|
requestFooterRender = fn;
|
|
91
116
|
},
|
|
92
117
|
scheduleProjectRefresh,
|
|
93
118
|
});
|
|
94
119
|
installEditor(ctx);
|
|
95
|
-
stopRefreshInterval();
|
|
96
120
|
stopRefreshInterval = startProjectRefreshInterval(currentConfig.projectRefreshIntervalMs, () =>
|
|
97
121
|
scheduleProjectRefresh(ctx),
|
|
98
122
|
);
|
|
@@ -101,6 +125,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
101
125
|
};
|
|
102
126
|
|
|
103
127
|
const cleanupUi = (ctx?: ExtensionContext) => {
|
|
128
|
+
cleanupPrototypePatches();
|
|
129
|
+
cleanupPrototypePatches = () => {};
|
|
104
130
|
stopRefreshInterval();
|
|
105
131
|
stopRefreshInterval = () => {};
|
|
106
132
|
projectRefreshInFlight = false;
|
|
@@ -117,6 +143,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
117
143
|
installUi(ctx);
|
|
118
144
|
});
|
|
119
145
|
|
|
146
|
+
registerZentuiSettingsCommand(pi, {
|
|
147
|
+
getConfig: getCurrentConfig,
|
|
148
|
+
setColorSources(patch: Partial<ColorSourcesConfig>) {
|
|
149
|
+
currentConfig = saveColorSourcesPatch(patch);
|
|
150
|
+
},
|
|
151
|
+
requestRender() {
|
|
152
|
+
refresh();
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
120
156
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
121
157
|
cleanupUi(ctx);
|
|
122
158
|
});
|
|
@@ -133,6 +169,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
133
169
|
refreshInteractiveState(ctx);
|
|
134
170
|
});
|
|
135
171
|
|
|
172
|
+
pi.on("thinking_level_select", async (_event, ctx) => {
|
|
173
|
+
refreshInteractiveState(ctx);
|
|
174
|
+
});
|
|
175
|
+
|
|
136
176
|
pi.on("message_end", async (_event, ctx) => {
|
|
137
177
|
refreshInteractiveState(ctx, true);
|
|
138
178
|
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ModelSelectorComponent,
|
|
3
|
+
SettingsSelectorComponent,
|
|
4
|
+
type Theme,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { PolishedTuiConfig } from "./config";
|
|
7
|
+
import { EDITOR_BORDER_STYLE, renderChromeBorder, renderEditorBorder } from "./style";
|
|
8
|
+
|
|
9
|
+
type RenderFn = (width: number) => string[];
|
|
10
|
+
|
|
11
|
+
type PatchableSelectorPrototype = {
|
|
12
|
+
render: RenderFn;
|
|
13
|
+
__zentuiSelectorBorderOriginalRender?: RenderFn;
|
|
14
|
+
__zentuiSelectorBorderPatched?: boolean;
|
|
15
|
+
__zentuiSelectorBorderWrapper?: RenderFn;
|
|
16
|
+
__zentuiSelectorBorderActive?: boolean;
|
|
17
|
+
__zentuiSelectorBorderGetTheme?: () => Theme | undefined;
|
|
18
|
+
__zentuiSelectorBorderGetConfig?: () => PolishedTuiConfig;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type Cleanup = () => void;
|
|
22
|
+
|
|
23
|
+
function stripAnsi(text: string): string {
|
|
24
|
+
return text.replaceAll(/\x1b\[[0-9;]*m/g, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isHorizontalBorderLine(line: string): boolean {
|
|
28
|
+
return /^─+$/.test(stripAnsi(line));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderBorderLine(
|
|
32
|
+
width: number,
|
|
33
|
+
theme: Theme | undefined,
|
|
34
|
+
config: PolishedTuiConfig | undefined,
|
|
35
|
+
): string {
|
|
36
|
+
const text = "─".repeat(Math.max(1, width));
|
|
37
|
+
if (theme && config) {
|
|
38
|
+
return renderChromeBorder(theme, config.colorSources.editor, EDITOR_BORDER_STYLE, text);
|
|
39
|
+
}
|
|
40
|
+
return renderEditorBorder(text);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function patchSelectorBorderStyle(
|
|
44
|
+
prototype: PatchableSelectorPrototype,
|
|
45
|
+
getTheme?: () => Theme | undefined,
|
|
46
|
+
getConfig?: () => PolishedTuiConfig,
|
|
47
|
+
): Cleanup {
|
|
48
|
+
prototype.__zentuiSelectorBorderGetTheme = getTheme;
|
|
49
|
+
prototype.__zentuiSelectorBorderGetConfig = getConfig;
|
|
50
|
+
prototype.__zentuiSelectorBorderActive = true;
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
prototype.__zentuiSelectorBorderPatched &&
|
|
54
|
+
prototype.render === prototype.__zentuiSelectorBorderWrapper
|
|
55
|
+
) {
|
|
56
|
+
return () => {
|
|
57
|
+
prototype.__zentuiSelectorBorderActive = false;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
prototype.__zentuiSelectorBorderOriginalRender = prototype.render;
|
|
62
|
+
const wrapper = function renderWithZentuiSelectorBorders(this: unknown, width: number): string[] {
|
|
63
|
+
const original = prototype.__zentuiSelectorBorderOriginalRender ?? prototype.render;
|
|
64
|
+
if (!prototype.__zentuiSelectorBorderActive) return original.call(this, width);
|
|
65
|
+
|
|
66
|
+
const lines = original.call(this, width);
|
|
67
|
+
if (lines.length === 0 || width <= 0) return lines;
|
|
68
|
+
|
|
69
|
+
return lines.map((line, index) => {
|
|
70
|
+
if (index !== 0 && index !== lines.length - 1) return line;
|
|
71
|
+
if (!isHorizontalBorderLine(line)) return line;
|
|
72
|
+
return renderBorderLine(
|
|
73
|
+
width,
|
|
74
|
+
prototype.__zentuiSelectorBorderGetTheme?.(),
|
|
75
|
+
prototype.__zentuiSelectorBorderGetConfig?.(),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
prototype.__zentuiSelectorBorderWrapper = wrapper;
|
|
80
|
+
prototype.render = wrapper;
|
|
81
|
+
prototype.__zentuiSelectorBorderPatched = true;
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
prototype.__zentuiSelectorBorderActive = false;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function installSelectorBorderStyle(
|
|
89
|
+
getTheme?: () => Theme | undefined,
|
|
90
|
+
getConfig?: () => PolishedTuiConfig,
|
|
91
|
+
): Cleanup {
|
|
92
|
+
const cleanupModel = patchSelectorBorderStyle(
|
|
93
|
+
ModelSelectorComponent.prototype as unknown as PatchableSelectorPrototype,
|
|
94
|
+
getTheme,
|
|
95
|
+
getConfig,
|
|
96
|
+
);
|
|
97
|
+
const cleanupSettings = patchSelectorBorderStyle(
|
|
98
|
+
SettingsSelectorComponent.prototype as unknown as PatchableSelectorPrototype,
|
|
99
|
+
getTheme,
|
|
100
|
+
getConfig,
|
|
101
|
+
);
|
|
102
|
+
return () => {
|
|
103
|
+
cleanupModel();
|
|
104
|
+
cleanupSettings();
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
type SettingItem,
|
|
5
|
+
SettingsList,
|
|
6
|
+
type SettingsListTheme,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
} from "@earendil-works/pi-tui";
|
|
9
|
+
import type { ColorSource, ColorSourcesConfig, PolishedTuiConfig } from "./config";
|
|
10
|
+
import { EDITOR_BORDER_STYLE, renderChromeBorder } from "./style";
|
|
11
|
+
|
|
12
|
+
const colorSourceValues: ColorSource[] = ["theme", "terminal"];
|
|
13
|
+
|
|
14
|
+
type SettingId = "starship" | "editorMessages";
|
|
15
|
+
|
|
16
|
+
type SettingsCommandDeps = {
|
|
17
|
+
getConfig: () => PolishedTuiConfig;
|
|
18
|
+
setColorSources: (patch: Partial<ColorSourcesConfig>) => void;
|
|
19
|
+
requestRender: () => void;
|
|
20
|
+
settingsListTheme?: SettingsListTheme;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const settingLabels: Record<SettingId, string> = {
|
|
24
|
+
starship: "Starship/footer colors",
|
|
25
|
+
editorMessages: "Editor + previous messages",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const settingDescriptions: Record<SettingId, string> = {
|
|
29
|
+
starship:
|
|
30
|
+
"Choose whether footer runtime/git/context colors use Pi theme tokens or terminal palette styles.",
|
|
31
|
+
editorMessages:
|
|
32
|
+
"Choose whether editor and previous user-message borders/rails use Pi theme colors or terminal palette styles.",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function isColorSource(value: string): value is ColorSource {
|
|
36
|
+
return value === "theme" || value === "terminal";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isSettingId(value: string): value is SettingId {
|
|
40
|
+
return value === "starship" || value === "editorMessages";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function editorMessageValue(config: PolishedTuiConfig): ColorSource | "mixed" {
|
|
44
|
+
return config.colorSources.editor === config.colorSources.userMessages
|
|
45
|
+
? config.colorSources.editor
|
|
46
|
+
: "mixed";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function patchForSetting(id: SettingId, value: ColorSource): Partial<ColorSourcesConfig> {
|
|
50
|
+
return id === "starship" ? { starship: value } : { editor: value, userMessages: value };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildItems(config: PolishedTuiConfig): SettingItem[] {
|
|
54
|
+
return (Object.keys(settingLabels) as SettingId[]).map((key) => ({
|
|
55
|
+
id: key,
|
|
56
|
+
label: settingLabels[key],
|
|
57
|
+
description: settingDescriptions[key],
|
|
58
|
+
currentValue: key === "starship" ? config.colorSources.starship : editorMessageValue(config),
|
|
59
|
+
values: colorSourceValues,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCommandDeps): void {
|
|
64
|
+
pi.registerCommand("zentui", {
|
|
65
|
+
description: "Configure Zentui colors",
|
|
66
|
+
handler: async (_args, ctx) => {
|
|
67
|
+
if (!ctx.hasUI) return;
|
|
68
|
+
|
|
69
|
+
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
70
|
+
const settingsList = new SettingsList(
|
|
71
|
+
buildItems(deps.getConfig()),
|
|
72
|
+
5,
|
|
73
|
+
deps.settingsListTheme ?? getSettingsListTheme(),
|
|
74
|
+
(id, newValue) => {
|
|
75
|
+
if (!isSettingId(id) || !isColorSource(newValue)) return;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
deps.setColorSources(patchForSetting(id, newValue));
|
|
79
|
+
settingsList.updateValue(id, newValue);
|
|
80
|
+
deps.requestRender();
|
|
81
|
+
ctx.ui.notify(`${settingLabels[id]}: ${newValue}`, "info");
|
|
82
|
+
tui.requestRender();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
85
|
+
ctx.ui.notify(`Could not update Zentui settings: ${message}`, "error");
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
() => done(undefined),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
render(width: number) {
|
|
93
|
+
const colorSource = deps.getConfig().colorSources.editor;
|
|
94
|
+
const border = renderChromeBorder(
|
|
95
|
+
theme,
|
|
96
|
+
colorSource,
|
|
97
|
+
EDITOR_BORDER_STYLE,
|
|
98
|
+
"─".repeat(Math.max(0, width)),
|
|
99
|
+
);
|
|
100
|
+
const header = theme.fg("accent", theme.bold("Zentui settings"));
|
|
101
|
+
const hint = theme.fg("dim", "Enter/Space cycles values · Esc closes");
|
|
102
|
+
return [
|
|
103
|
+
truncateToWidth(border, width, ""),
|
|
104
|
+
truncateToWidth(header, width, ""),
|
|
105
|
+
truncateToWidth(hint, width, ""),
|
|
106
|
+
"",
|
|
107
|
+
...settingsList.render(width),
|
|
108
|
+
truncateToWidth(border, width, ""),
|
|
109
|
+
];
|
|
110
|
+
},
|
|
111
|
+
invalidate() {
|
|
112
|
+
settingsList.invalidate();
|
|
113
|
+
},
|
|
114
|
+
handleInput(data: string) {
|
|
115
|
+
settingsList.handleInput(data);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import type { ColorSpec } from "./config";
|
|
1
|
+
import type { ColorSource, ColorSpec } from "./config";
|
|
2
2
|
|
|
3
3
|
type ThemeLike = {
|
|
4
4
|
fg(color: string, text: string): string;
|
|
5
|
+
bold?: (text: string) => string;
|
|
6
|
+
italic?: (text: string) => string;
|
|
7
|
+
underline?: (text: string) => string;
|
|
5
8
|
};
|
|
6
9
|
|
|
7
10
|
export type { ThemeLike };
|
|
8
11
|
|
|
12
|
+
export const EDITOR_ACCENT_STYLE = "blue";
|
|
13
|
+
export const EDITOR_BORDER_STYLE = "bright-black";
|
|
14
|
+
|
|
9
15
|
function isHexColor(value: string): boolean {
|
|
10
16
|
return /^#(?:[0-9a-fA-F]{6})$/.test(value);
|
|
11
17
|
}
|
|
@@ -45,33 +51,98 @@ const terminalStyleModifiers = new Map([
|
|
|
45
51
|
["underline", 4],
|
|
46
52
|
]);
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
const themeColorNameMap = new Map([
|
|
55
|
+
["red", "error"],
|
|
56
|
+
["bright-red", "error"],
|
|
57
|
+
["green", "success"],
|
|
58
|
+
["bright-green", "success"],
|
|
59
|
+
["yellow", "warning"],
|
|
60
|
+
["bright-yellow", "warning"],
|
|
61
|
+
["blue", "syntaxFunction"],
|
|
62
|
+
["bright-blue", "syntaxFunction"],
|
|
63
|
+
["cyan", "syntaxFunction"],
|
|
64
|
+
["bright-cyan", "syntaxFunction"],
|
|
65
|
+
["purple", "syntaxKeyword"],
|
|
66
|
+
["bright-purple", "syntaxKeyword"],
|
|
67
|
+
["black", "muted"],
|
|
68
|
+
["bright-black", "muted"],
|
|
69
|
+
["white", "text"],
|
|
70
|
+
["bright-white", "text"],
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
const themeStyleModifiers = new Set(["bold", "italic", "underline"]);
|
|
74
|
+
|
|
75
|
+
function terminalColorToAnsi(color: string, isBackground = false): string | undefined {
|
|
49
76
|
const normalized = color.toLowerCase();
|
|
50
77
|
const colorCode = terminalColorCodes.get(normalized);
|
|
51
|
-
if (colorCode !== undefined) return `${colorCode}`;
|
|
78
|
+
if (colorCode !== undefined) return `${isBackground ? colorCode + 10 : colorCode}`;
|
|
52
79
|
|
|
53
80
|
if (/^(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(normalized)) {
|
|
54
|
-
return
|
|
81
|
+
return `${isBackground ? 48 : 38};5;${normalized}`;
|
|
55
82
|
}
|
|
56
83
|
|
|
57
|
-
if (isHexColor(normalized)) return hexToAnsi(normalized).slice(2, -1);
|
|
84
|
+
if (isHexColor(normalized)) return hexToAnsi(normalized, isBackground).slice(2, -1);
|
|
58
85
|
return undefined;
|
|
59
86
|
}
|
|
60
87
|
|
|
88
|
+
function isExplicitTerminalColorToken(token: string): boolean {
|
|
89
|
+
const normalized = token.toLowerCase();
|
|
90
|
+
if (normalized.startsWith("fg:") || normalized.startsWith("bg:")) return true;
|
|
91
|
+
if (isHexColor(normalized)) return true;
|
|
92
|
+
return /^(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(normalized);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function applyThemeModifiers(theme: ThemeLike, styleTokens: string[], text: string): string {
|
|
96
|
+
let rendered = text;
|
|
97
|
+
for (const token of styleTokens) {
|
|
98
|
+
const normalized = token.toLowerCase();
|
|
99
|
+
if (normalized === "bold") rendered = theme.bold?.(rendered) ?? rendered;
|
|
100
|
+
if (normalized === "italic") rendered = theme.italic?.(rendered) ?? rendered;
|
|
101
|
+
if (normalized === "underline") rendered = theme.underline?.(rendered) ?? rendered;
|
|
102
|
+
}
|
|
103
|
+
return rendered;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function themeFg(theme: ThemeLike, color: string, text: string): string {
|
|
107
|
+
try {
|
|
108
|
+
return theme.fg(color, text);
|
|
109
|
+
} catch {
|
|
110
|
+
return text;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function mapThemeColor(styleTokens: string[]): string | undefined {
|
|
115
|
+
let fallback: string | undefined;
|
|
116
|
+
for (const token of styleTokens) {
|
|
117
|
+
const normalized = token.toLowerCase();
|
|
118
|
+
if (themeStyleModifiers.has(normalized)) continue;
|
|
119
|
+
if (normalized === "dim" || normalized === "dimmed") {
|
|
120
|
+
fallback = "muted";
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const mapped = themeColorNameMap.get(normalized);
|
|
125
|
+
if (mapped) return mapped;
|
|
126
|
+
return token;
|
|
127
|
+
}
|
|
128
|
+
return fallback;
|
|
129
|
+
}
|
|
130
|
+
|
|
61
131
|
/**
|
|
62
132
|
* Colorize text using a theme color token or hex color.
|
|
63
|
-
* Non-hex values are passed directly to `theme.fg()
|
|
64
|
-
*
|
|
133
|
+
* Non-hex values are passed directly to `theme.fg()`; invalid tokens fall back
|
|
134
|
+
* to unstyled text so a config typo does not break rendering.
|
|
65
135
|
*/
|
|
66
136
|
export function colorize(theme: ThemeLike, color: ColorSpec, text: string): string {
|
|
67
137
|
if (isHexColor(color)) {
|
|
68
138
|
return `${hexToAnsi(color)}${text}\x1b[39m`;
|
|
69
139
|
}
|
|
70
|
-
return theme
|
|
140
|
+
return themeFg(theme, color, text);
|
|
71
141
|
}
|
|
72
142
|
|
|
73
143
|
/**
|
|
74
|
-
* Render text with Starship-style terminal styling strings (e.g. "bold red", "fg:202"
|
|
144
|
+
* Render text with Starship-style terminal styling strings (e.g. "bold red", "fg:202",
|
|
145
|
+
* "bg:blue", "underline bg:#bf5700").
|
|
75
146
|
*/
|
|
76
147
|
export function renderTerminalStyle(style: string, text: string): string {
|
|
77
148
|
const codes: string[] = [];
|
|
@@ -85,10 +156,66 @@ export function renderTerminalStyle(style: string, text: string): string {
|
|
|
85
156
|
continue;
|
|
86
157
|
}
|
|
87
158
|
|
|
88
|
-
const
|
|
89
|
-
const
|
|
159
|
+
const isForeground = normalized.startsWith("fg:");
|
|
160
|
+
const isBackground = normalized.startsWith("bg:");
|
|
161
|
+
const colorName = isForeground || isBackground ? normalized.slice(3) : normalized;
|
|
162
|
+
const color = terminalColorToAnsi(colorName, isBackground);
|
|
90
163
|
if (color) codes.push(color);
|
|
91
164
|
}
|
|
92
165
|
|
|
93
166
|
return codes.length ? `\x1b[${codes.join(";")}m${text}\x1b[0m` : text;
|
|
94
167
|
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Apply Starship-style terminal styling first, falling back to Pi theme tokens for
|
|
171
|
+
* legacy config values such as "accent" or "syntaxKeyword".
|
|
172
|
+
*/
|
|
173
|
+
export function renderStyle(theme: ThemeLike, style: ColorSpec, text: string): string {
|
|
174
|
+
if (style.trim() === "") return text;
|
|
175
|
+
const styled = renderTerminalStyle(style, text);
|
|
176
|
+
return styled === text ? colorize(theme, style, text) : styled;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function renderThemeStyle(theme: ThemeLike, style: ColorSpec, text: string): string {
|
|
180
|
+
const trimmed = style.trim();
|
|
181
|
+
if (trimmed === "") return text;
|
|
182
|
+
|
|
183
|
+
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
184
|
+
if (tokens.some(isExplicitTerminalColorToken)) return renderTerminalStyle(style, text);
|
|
185
|
+
|
|
186
|
+
const color = mapThemeColor(tokens) ?? "text";
|
|
187
|
+
return themeFg(theme, color, applyThemeModifiers(theme, tokens, text));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function renderStyleForSource(
|
|
191
|
+
theme: ThemeLike,
|
|
192
|
+
source: ColorSource,
|
|
193
|
+
style: ColorSpec,
|
|
194
|
+
text: string,
|
|
195
|
+
): string {
|
|
196
|
+
return source === "terminal"
|
|
197
|
+
? renderStyle(theme, style, text)
|
|
198
|
+
: renderThemeStyle(theme, style, text);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function renderEditorAccent(text: string): string {
|
|
202
|
+
return renderTerminalStyle(EDITOR_ACCENT_STYLE, text);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function renderEditorBorder(text: string): string {
|
|
206
|
+
return renderTerminalStyle(EDITOR_BORDER_STYLE, text);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function renderAccentLine(theme: ThemeLike, source: ColorSource, text: string): string {
|
|
210
|
+
return source === "terminal" ? renderEditorAccent(text) : themeFg(theme, "accent", text);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function renderChromeBorder(
|
|
214
|
+
theme: ThemeLike,
|
|
215
|
+
source: ColorSource,
|
|
216
|
+
terminalFallbackStyle: ColorSpec,
|
|
217
|
+
text: string,
|
|
218
|
+
): string {
|
|
219
|
+
if (source === "terminal") return renderTerminalStyle(terminalFallbackStyle, text);
|
|
220
|
+
return themeFg(theme, "borderMuted", text);
|
|
221
|
+
}
|
package/extensions/zentui/ui.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
truncateToWidth,
|
|
7
7
|
visibleWidth,
|
|
8
8
|
} from "@earendil-works/pi-tui";
|
|
9
|
+
import type { PolishedTuiConfig } from "./config";
|
|
10
|
+
import { EDITOR_BORDER_STYLE, renderAccentLine, renderChromeBorder } from "./style";
|
|
9
11
|
|
|
10
12
|
type AutocompleteEditorInternals = {
|
|
11
13
|
autocompleteList?: Pick<Component, "render">;
|
|
@@ -20,6 +22,7 @@ function clampRenderedLines(lines: string[], width: number): string[] {
|
|
|
20
22
|
export class PolishedEditor extends CustomEditor {
|
|
21
23
|
private readonly getModelMeta: () => string;
|
|
22
24
|
private readonly getThinkingLevel: () => string | undefined;
|
|
25
|
+
private readonly getConfig: () => PolishedTuiConfig;
|
|
23
26
|
private readonly uiTheme: Theme;
|
|
24
27
|
private readonly reset = "\x1b[0m";
|
|
25
28
|
|
|
@@ -28,12 +31,14 @@ export class PolishedEditor extends CustomEditor {
|
|
|
28
31
|
theme: EditorTheme,
|
|
29
32
|
keybindings: KeybindingsManager,
|
|
30
33
|
uiTheme: Theme,
|
|
34
|
+
getConfig: () => PolishedTuiConfig,
|
|
31
35
|
getModelMeta: () => string,
|
|
32
36
|
getThinkingLevel: () => string | undefined,
|
|
33
37
|
) {
|
|
34
38
|
super(tui, theme, keybindings, { paddingX: 0 });
|
|
35
39
|
this.borderColor = (text: string) => uiTheme.fg("border", text);
|
|
36
40
|
this.uiTheme = uiTheme;
|
|
41
|
+
this.getConfig = getConfig;
|
|
37
42
|
this.getModelMeta = getModelMeta;
|
|
38
43
|
this.getThinkingLevel = getThinkingLevel;
|
|
39
44
|
}
|
|
@@ -87,9 +92,20 @@ export class PolishedEditor extends CustomEditor {
|
|
|
87
92
|
}
|
|
88
93
|
const meta = metaParts.filter(Boolean).join(this.uiTheme.fg("border", " "));
|
|
89
94
|
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
95
|
+
const colorSource = this.getConfig().colorSources.editor;
|
|
96
|
+
const rail = `${renderAccentLine(this.uiTheme, colorSource, "│")}${this.reset} `;
|
|
97
|
+
const top = renderChromeBorder(
|
|
98
|
+
this.uiTheme,
|
|
99
|
+
colorSource,
|
|
100
|
+
EDITOR_BORDER_STYLE,
|
|
101
|
+
"─".repeat(width),
|
|
102
|
+
);
|
|
103
|
+
const bottom = renderChromeBorder(
|
|
104
|
+
this.uiTheme,
|
|
105
|
+
colorSource,
|
|
106
|
+
EDITOR_BORDER_STYLE,
|
|
107
|
+
"─".repeat(width),
|
|
108
|
+
);
|
|
93
109
|
const lines = ["", ...editorLines, "", meta];
|
|
94
110
|
const renderedLines = [
|
|
95
111
|
top,
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
truncateToWidth,
|
|
6
6
|
visibleWidth,
|
|
7
7
|
} from "@earendil-works/pi-tui";
|
|
8
|
+
import type { PolishedTuiConfig } from "./config";
|
|
9
|
+
import { EDITOR_BORDER_STYLE, renderAccentLine, renderChromeBorder } from "./style";
|
|
8
10
|
|
|
9
11
|
const OSC133_ZONE_START = "\x1b]133;A\x07";
|
|
10
12
|
const OSC133_ZONE_END = "\x1b]133;B\x07";
|
|
@@ -17,9 +19,14 @@ type PatchableUserMessagePrototype = {
|
|
|
17
19
|
children?: unknown[];
|
|
18
20
|
__zentuiUserMessageOriginalRender?: RenderFn;
|
|
19
21
|
__zentuiUserMessagePatched?: boolean;
|
|
22
|
+
__zentuiUserMessageWrapper?: RenderFn;
|
|
23
|
+
__zentuiUserMessageActive?: boolean;
|
|
20
24
|
__zentuiUserMessageGetTheme?: () => Theme | undefined;
|
|
25
|
+
__zentuiUserMessageGetConfig?: () => PolishedTuiConfig;
|
|
21
26
|
};
|
|
22
27
|
|
|
28
|
+
type Cleanup = () => void;
|
|
29
|
+
|
|
23
30
|
type MarkdownLike = {
|
|
24
31
|
text?: unknown;
|
|
25
32
|
};
|
|
@@ -73,9 +80,14 @@ function fillLine(content: string, width: number): string {
|
|
|
73
80
|
return `${truncated}${pad}`;
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
function renderPromptBoxLine(
|
|
83
|
+
function renderPromptBoxLine(
|
|
84
|
+
line: string,
|
|
85
|
+
width: number,
|
|
86
|
+
theme: Theme | undefined,
|
|
87
|
+
config: PolishedTuiConfig,
|
|
88
|
+
): string {
|
|
77
89
|
if (width <= 0) return "";
|
|
78
|
-
const rail = `${
|
|
90
|
+
const rail = `${theme ? renderAccentLine(theme, config.colorSources.userMessages, "│") : "│"} `;
|
|
79
91
|
const contentWidth = Math.max(0, width - visibleWidth(rail));
|
|
80
92
|
return truncateToWidth(`${rail}${fillLine(line, contentWidth)}`, width, "");
|
|
81
93
|
}
|
|
@@ -84,42 +96,70 @@ function renderZentuiUserMessage(
|
|
|
84
96
|
instance: PatchableUserMessagePrototype,
|
|
85
97
|
width: number,
|
|
86
98
|
theme: Theme | undefined,
|
|
99
|
+
config: PolishedTuiConfig,
|
|
87
100
|
): string[] | undefined {
|
|
88
101
|
const text = findMarkdownText(instance);
|
|
89
102
|
if (text === undefined) return undefined;
|
|
90
103
|
if (width <= 0) return [""];
|
|
91
104
|
|
|
92
|
-
const railWidth = visibleWidth(
|
|
105
|
+
const railWidth = visibleWidth(
|
|
106
|
+
`${theme ? renderAccentLine(theme, config.colorSources.userMessages, "│") : "│"} `,
|
|
107
|
+
);
|
|
93
108
|
const contentWidth = Math.max(1, width - railWidth);
|
|
94
109
|
const renderer = new Markdown(text, 0, 0, makeMarkdownTheme(theme), {
|
|
95
110
|
color: (content) => themeFg(theme, "userMessageText", content),
|
|
96
111
|
});
|
|
97
112
|
const body = renderer.render(contentWidth);
|
|
98
113
|
const contentLines = body.length > 0 ? body : [""];
|
|
99
|
-
const border =
|
|
114
|
+
const border = theme
|
|
115
|
+
? renderChromeBorder(
|
|
116
|
+
theme,
|
|
117
|
+
config.colorSources.userMessages,
|
|
118
|
+
EDITOR_BORDER_STYLE,
|
|
119
|
+
"─".repeat(width),
|
|
120
|
+
)
|
|
121
|
+
: "─".repeat(width);
|
|
100
122
|
|
|
101
123
|
return [
|
|
102
124
|
truncateToWidth(border, width, ""),
|
|
103
|
-
renderPromptBoxLine("", width, theme),
|
|
104
|
-
...contentLines.map((line) => renderPromptBoxLine(line, width, theme)),
|
|
105
|
-
renderPromptBoxLine("", width, theme),
|
|
125
|
+
renderPromptBoxLine("", width, theme, config),
|
|
126
|
+
...contentLines.map((line) => renderPromptBoxLine(line, width, theme, config)),
|
|
127
|
+
renderPromptBoxLine("", width, theme, config),
|
|
106
128
|
truncateToWidth(border, width, ""),
|
|
107
129
|
];
|
|
108
130
|
}
|
|
109
131
|
|
|
110
|
-
export function installUserMessageStyle(
|
|
132
|
+
export function installUserMessageStyle(
|
|
133
|
+
getTheme: () => Theme | undefined,
|
|
134
|
+
getConfig: () => PolishedTuiConfig,
|
|
135
|
+
): Cleanup {
|
|
111
136
|
const prototype = UserMessageComponent.prototype as unknown as PatchableUserMessagePrototype;
|
|
112
137
|
prototype.__zentuiUserMessageGetTheme = getTheme;
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
prototype.__zentuiUserMessageGetConfig = getConfig;
|
|
139
|
+
prototype.__zentuiUserMessageActive = true;
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
prototype.__zentuiUserMessagePatched &&
|
|
143
|
+
prototype.render === prototype.__zentuiUserMessageWrapper
|
|
144
|
+
) {
|
|
145
|
+
return () => {
|
|
146
|
+
prototype.__zentuiUserMessageActive = false;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
115
149
|
|
|
116
150
|
prototype.__zentuiUserMessageOriginalRender = prototype.render;
|
|
117
|
-
|
|
151
|
+
const wrapper = function renderWithZentuiUserMessage(this: unknown, width: number): string[] {
|
|
118
152
|
const original = prototype.__zentuiUserMessageOriginalRender ?? prototype.render;
|
|
153
|
+
if (!prototype.__zentuiUserMessageActive) return original.call(this, width);
|
|
154
|
+
|
|
155
|
+
const config = prototype.__zentuiUserMessageGetConfig?.();
|
|
156
|
+
if (!config) return original.call(this, width);
|
|
157
|
+
|
|
119
158
|
const lines = renderZentuiUserMessage(
|
|
120
159
|
this as PatchableUserMessagePrototype,
|
|
121
160
|
width,
|
|
122
161
|
prototype.__zentuiUserMessageGetTheme?.(),
|
|
162
|
+
config,
|
|
123
163
|
);
|
|
124
164
|
|
|
125
165
|
if (!lines) return original.call(this, width);
|
|
@@ -129,5 +169,11 @@ export function installUserMessageStyle(getTheme: () => Theme | undefined): void
|
|
|
129
169
|
lines[lines.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + lines[lines.length - 1];
|
|
130
170
|
return lines;
|
|
131
171
|
};
|
|
172
|
+
prototype.__zentuiUserMessageWrapper = wrapper;
|
|
173
|
+
prototype.render = wrapper;
|
|
132
174
|
prototype.__zentuiUserMessagePatched = true;
|
|
175
|
+
|
|
176
|
+
return () => {
|
|
177
|
+
prototype.__zentuiUserMessageActive = false;
|
|
178
|
+
};
|
|
133
179
|
}
|