pi-zentui 0.1.13 → 0.1.14
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 +19 -2
- package/extensions/zentui/config.ts +58 -0
- package/extensions/zentui/format.ts +31 -3
- package/extensions/zentui/index.ts +137 -34
- package/extensions/zentui/settings-command.ts +179 -16
- package/extensions/zentui/state.ts +2 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,7 +123,18 @@ pi install git:github.com/lmilojevicc/pi-zentui
|
|
|
123
123
|
|
|
124
124
|
## Config
|
|
125
125
|
|
|
126
|
-
User config lives at `~/.pi/agent/zentui.json`. The file is optional: missing or invalid known values fall back to Zentui defaults, unknown keys are ignored at runtime, and `/zentui` can patch color-source settings
|
|
126
|
+
User config lives at `~/.pi/agent/zentui.json`. The file is optional: missing or invalid known values fall back to Zentui defaults, unknown keys are ignored at runtime, and `/zentui` can patch color-source settings, UI feature toggles, and active third-party status placements.
|
|
127
|
+
|
|
128
|
+
Useful slash-command shortcuts:
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
/zentui editor enable
|
|
132
|
+
/zentui editor disable
|
|
133
|
+
/zentui statusline enable
|
|
134
|
+
/zentui statusline disable
|
|
135
|
+
/zentui editor toggle
|
|
136
|
+
/zentui statusline toggle
|
|
137
|
+
```
|
|
127
138
|
|
|
128
139
|
Default config values — copy this and change any value you want:
|
|
129
140
|
|
|
@@ -143,7 +154,8 @@ Default config values — copy this and change any value you want:
|
|
|
143
154
|
"staged": "+",
|
|
144
155
|
"renamed": "»",
|
|
145
156
|
"deleted": "✘",
|
|
146
|
-
"typechanged": "T"
|
|
157
|
+
"typechanged": "T",
|
|
158
|
+
"cacheHit": ""
|
|
147
159
|
},
|
|
148
160
|
"colors": {
|
|
149
161
|
"cwd": "bold cyan",
|
|
@@ -173,6 +185,10 @@ Default config values — copy this and change any value you want:
|
|
|
173
185
|
"editor": "theme",
|
|
174
186
|
"userMessages": "theme"
|
|
175
187
|
},
|
|
188
|
+
"features": {
|
|
189
|
+
"editor": true,
|
|
190
|
+
"statusLine": true
|
|
191
|
+
},
|
|
176
192
|
"extensionStatuses": {
|
|
177
193
|
"defaultPlacement": "right",
|
|
178
194
|
"placements": {}
|
|
@@ -184,6 +200,7 @@ Default config values — copy this and change any value you want:
|
|
|
184
200
|
- `projectRefreshIntervalMs`: project status polling interval; `0` disables polling.
|
|
185
201
|
- `icons`: every shown icon key is configurable; omit any key to use the Zentui default.
|
|
186
202
|
- `colorSources`: `theme` maps styles through Pi theme tokens; `terminal` emits terminal colors. `/zentui` switches these sources; manual JSON controls specific style values.
|
|
203
|
+
- `features`: `editor` enables Zentui's custom editor, selector borders, and previous-message chrome. `statusLine` enables Zentui's custom footer/status line. Both can be changed from `/zentui` or direct slash-command arguments.
|
|
187
204
|
- `extensionStatuses`: controls third-party statuses published by other Pi extensions through `ctx.ui.setStatus()`. `defaultPlacement` and each `placements` value can be `off`, `left`, `middle`, or `right`. `/zentui` lists only statuses that are currently active.
|
|
188
205
|
- The shown `editor*` values match the default `theme` source. Omit those keys to keep Zentui's source-aware defaults when switching between `theme` and `terminal`.
|
|
189
206
|
- `editorAccent` styles the active editor rail and previous user-message rail.
|
|
@@ -12,6 +12,11 @@ export type ColorSourcesConfig = {
|
|
|
12
12
|
userMessages: ColorSource;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
export type UiFeaturesConfig = {
|
|
16
|
+
editor: boolean;
|
|
17
|
+
statusLine: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
15
20
|
export type ExtensionStatusPlacement = "off" | "left" | "middle" | "right";
|
|
16
21
|
|
|
17
22
|
export type ExtensionStatusesConfig = {
|
|
@@ -38,6 +43,7 @@ export type PolishedTuiConfig = {
|
|
|
38
43
|
renamed: string;
|
|
39
44
|
deleted: string;
|
|
40
45
|
typechanged: string;
|
|
46
|
+
cacheHit: string;
|
|
41
47
|
};
|
|
42
48
|
colors: {
|
|
43
49
|
cwd: ColorSpec;
|
|
@@ -63,6 +69,7 @@ export type PolishedTuiConfig = {
|
|
|
63
69
|
editorThinkingXhigh?: ColorSpec;
|
|
64
70
|
};
|
|
65
71
|
colorSources: ColorSourcesConfig;
|
|
72
|
+
features: UiFeaturesConfig;
|
|
66
73
|
extensionStatuses: ExtensionStatusesConfig;
|
|
67
74
|
};
|
|
68
75
|
|
|
@@ -84,6 +91,7 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
84
91
|
renamed: "»",
|
|
85
92
|
deleted: "✘",
|
|
86
93
|
typechanged: "T",
|
|
94
|
+
cacheHit: "",
|
|
87
95
|
},
|
|
88
96
|
colors: {
|
|
89
97
|
cwd: "bold cyan",
|
|
@@ -103,6 +111,10 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
103
111
|
editor: "theme",
|
|
104
112
|
userMessages: "theme",
|
|
105
113
|
},
|
|
114
|
+
features: {
|
|
115
|
+
editor: true,
|
|
116
|
+
statusLine: true,
|
|
117
|
+
},
|
|
106
118
|
extensionStatuses: {
|
|
107
119
|
defaultPlacement: "right",
|
|
108
120
|
placements: {},
|
|
@@ -123,6 +135,7 @@ const iconKeys = [
|
|
|
123
135
|
"renamed",
|
|
124
136
|
"deleted",
|
|
125
137
|
"typechanged",
|
|
138
|
+
"cacheHit",
|
|
126
139
|
] as const satisfies readonly (keyof PolishedTuiConfig["icons"])[];
|
|
127
140
|
|
|
128
141
|
type ConfigRecord = Record<string, unknown>;
|
|
@@ -161,6 +174,11 @@ function colorSourceValue(
|
|
|
161
174
|
return value === "terminal" || value === "theme" ? value : defaultConfig.colorSources[key];
|
|
162
175
|
}
|
|
163
176
|
|
|
177
|
+
function booleanValue(record: Record<string, unknown>, key: keyof UiFeaturesConfig): boolean {
|
|
178
|
+
const value = record[key];
|
|
179
|
+
return typeof value === "boolean" ? value : defaultConfig.features[key];
|
|
180
|
+
}
|
|
181
|
+
|
|
164
182
|
function definedColors(
|
|
165
183
|
colors: Partial<Record<keyof PolishedTuiConfig["colors"], string | undefined>>,
|
|
166
184
|
): Partial<PolishedTuiConfig["colors"]> {
|
|
@@ -214,6 +232,13 @@ function normalizeColorSources(record: Record<string, unknown>): ColorSourcesCon
|
|
|
214
232
|
};
|
|
215
233
|
}
|
|
216
234
|
|
|
235
|
+
function normalizeUiFeatures(record: Record<string, unknown>): UiFeaturesConfig {
|
|
236
|
+
return {
|
|
237
|
+
editor: booleanValue(record, "editor"),
|
|
238
|
+
statusLine: booleanValue(record, "statusLine"),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
217
242
|
export function isExtensionStatusPlacement(value: unknown): value is ExtensionStatusPlacement {
|
|
218
243
|
return value === "off" || value === "left" || value === "middle" || value === "right";
|
|
219
244
|
}
|
|
@@ -241,6 +266,10 @@ function isColorSourceKey(value: string): value is keyof ColorSourcesConfig {
|
|
|
241
266
|
return value === "starship" || value === "editor" || value === "userMessages";
|
|
242
267
|
}
|
|
243
268
|
|
|
269
|
+
function isUiFeatureKey(value: string): value is keyof UiFeaturesConfig {
|
|
270
|
+
return value === "editor" || value === "statusLine";
|
|
271
|
+
}
|
|
272
|
+
|
|
244
273
|
function validColorSourceEntries(record: Record<string, unknown>): Partial<ColorSourcesConfig> {
|
|
245
274
|
return Object.fromEntries(
|
|
246
275
|
Object.entries(record).filter((entry): entry is [keyof ColorSourcesConfig, ColorSource] => {
|
|
@@ -250,6 +279,15 @@ function validColorSourceEntries(record: Record<string, unknown>): Partial<Color
|
|
|
250
279
|
) as Partial<ColorSourcesConfig>;
|
|
251
280
|
}
|
|
252
281
|
|
|
282
|
+
function validUiFeatureEntries(record: Record<string, unknown>): Partial<UiFeaturesConfig> {
|
|
283
|
+
return Object.fromEntries(
|
|
284
|
+
Object.entries(record).filter((entry): entry is [keyof UiFeaturesConfig, boolean] => {
|
|
285
|
+
const [key, value] = entry;
|
|
286
|
+
return isUiFeatureKey(key) && typeof value === "boolean";
|
|
287
|
+
}),
|
|
288
|
+
) as Partial<UiFeaturesConfig>;
|
|
289
|
+
}
|
|
290
|
+
|
|
253
291
|
function readConfigRecord(path = configPath): ConfigRecord {
|
|
254
292
|
try {
|
|
255
293
|
if (!existsSync(path)) return {};
|
|
@@ -278,6 +316,9 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
278
316
|
const colorSources = isRecord(config.colorSources)
|
|
279
317
|
? normalizeColorSources(config.colorSources as Record<string, unknown>)
|
|
280
318
|
: defaultConfig.colorSources;
|
|
319
|
+
const features = isRecord(config.features)
|
|
320
|
+
? normalizeUiFeatures(config.features as Record<string, unknown>)
|
|
321
|
+
: defaultConfig.features;
|
|
281
322
|
const extensionStatuses = isRecord(config.extensionStatuses)
|
|
282
323
|
? normalizeExtensionStatuses(config.extensionStatuses as Record<string, unknown>)
|
|
283
324
|
: defaultConfig.extensionStatuses;
|
|
@@ -292,6 +333,7 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
292
333
|
...colors,
|
|
293
334
|
},
|
|
294
335
|
colorSources: { ...colorSources },
|
|
336
|
+
features: { ...features },
|
|
295
337
|
extensionStatuses: {
|
|
296
338
|
defaultPlacement: extensionStatuses.defaultPlacement,
|
|
297
339
|
placements: { ...extensionStatuses.placements },
|
|
@@ -331,6 +373,22 @@ export function saveColorSourcesPatch(
|
|
|
331
373
|
return mergeConfig(record);
|
|
332
374
|
}
|
|
333
375
|
|
|
376
|
+
export function saveUiFeaturesPatch(
|
|
377
|
+
patch: Partial<UiFeaturesConfig>,
|
|
378
|
+
path = configPath,
|
|
379
|
+
): PolishedTuiConfig {
|
|
380
|
+
const record = readConfigRecord(path);
|
|
381
|
+
const existing = isRecord(record.features)
|
|
382
|
+
? { ...(record.features as Record<string, unknown>) }
|
|
383
|
+
: {};
|
|
384
|
+
record.features = {
|
|
385
|
+
...existing,
|
|
386
|
+
...validUiFeatureEntries(patch),
|
|
387
|
+
};
|
|
388
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
389
|
+
return mergeConfig(record);
|
|
390
|
+
}
|
|
391
|
+
|
|
334
392
|
export function saveExtensionStatusPlacement(
|
|
335
393
|
key: string,
|
|
336
394
|
placement: ExtensionStatusPlacement,
|
|
@@ -7,6 +7,9 @@ import { renderStyleForSource } from "./style";
|
|
|
7
7
|
export type UsageTotals = {
|
|
8
8
|
input: number;
|
|
9
9
|
output: number;
|
|
10
|
+
cacheRead: number;
|
|
11
|
+
cacheWrite: number;
|
|
12
|
+
latestCacheHitRate?: number;
|
|
10
13
|
cost: number;
|
|
11
14
|
};
|
|
12
15
|
|
|
@@ -35,27 +38,52 @@ export function formatProviderLabel(provider: string | undefined): string {
|
|
|
35
38
|
);
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
function calculateCacheHitRate(
|
|
42
|
+
input: number,
|
|
43
|
+
cacheRead: number,
|
|
44
|
+
cacheWrite: number,
|
|
45
|
+
): number | undefined {
|
|
46
|
+
const promptTokens = input + cacheRead + cacheWrite;
|
|
47
|
+
return promptTokens > 0 ? (cacheRead / promptTokens) * 100 : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
export function getUsageTotals(ctx: ExtensionContext): UsageTotals {
|
|
39
51
|
let input = 0;
|
|
40
52
|
let output = 0;
|
|
53
|
+
let cacheRead = 0;
|
|
54
|
+
let cacheWrite = 0;
|
|
55
|
+
let latestCacheHitRate: number | undefined;
|
|
41
56
|
let cost = 0;
|
|
42
57
|
|
|
43
58
|
const entries = ctx.sessionManager.getEntries?.() ?? ctx.sessionManager.getBranch();
|
|
44
59
|
for (const entry of entries) {
|
|
45
60
|
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
46
61
|
const usage = (entry.message as AssistantMessage).usage;
|
|
47
|
-
|
|
62
|
+
const entryInput = usage?.input ?? 0;
|
|
63
|
+
const entryCacheRead = usage?.cacheRead ?? 0;
|
|
64
|
+
const entryCacheWrite = usage?.cacheWrite ?? 0;
|
|
65
|
+
|
|
66
|
+
input += entryInput;
|
|
48
67
|
output += usage?.output ?? 0;
|
|
68
|
+
cacheRead += entryCacheRead;
|
|
69
|
+
cacheWrite += entryCacheWrite;
|
|
49
70
|
cost += usage?.cost?.total ?? 0;
|
|
71
|
+
latestCacheHitRate = calculateCacheHitRate(entryInput, entryCacheRead, entryCacheWrite);
|
|
50
72
|
}
|
|
51
73
|
|
|
52
|
-
return { input, output, cost };
|
|
74
|
+
return { input, output, cacheRead, cacheWrite, latestCacheHitRate, cost };
|
|
53
75
|
}
|
|
54
76
|
|
|
55
|
-
export function buildTokenLabel(totals: UsageTotals): string {
|
|
77
|
+
export function buildTokenLabel(totals: UsageTotals, cacheHitIcon = ""): string {
|
|
56
78
|
const parts: string[] = [];
|
|
57
79
|
if (totals.input) parts.push(`↑${formatCount(totals.input)}`);
|
|
58
80
|
if (totals.output) parts.push(`↓${formatCount(totals.output)}`);
|
|
81
|
+
|
|
82
|
+
const hasCacheTokens = totals.cacheRead > 0 || totals.cacheWrite > 0;
|
|
83
|
+
if (hasCacheTokens && totals.latestCacheHitRate !== undefined) {
|
|
84
|
+
const cacheHitRate = `${totals.latestCacheHitRate.toFixed(1)}%`;
|
|
85
|
+
parts.push(cacheHitIcon ? `${cacheHitIcon} ${cacheHitRate}` : cacheHitRate);
|
|
86
|
+
}
|
|
59
87
|
return parts.length > 0 ? parts.join(" ") : "↑0 ↓0";
|
|
60
88
|
}
|
|
61
89
|
|
|
@@ -9,10 +9,12 @@ import {
|
|
|
9
9
|
type ColorSourcesConfig,
|
|
10
10
|
type ExtensionStatusPlacement,
|
|
11
11
|
type PolishedTuiConfig,
|
|
12
|
+
type UiFeaturesConfig,
|
|
12
13
|
ensureConfigExists,
|
|
13
14
|
loadConfig,
|
|
14
15
|
saveColorSourcesPatch,
|
|
15
16
|
saveExtensionStatusPlacement,
|
|
17
|
+
saveUiFeaturesPatch,
|
|
16
18
|
} from "./config";
|
|
17
19
|
import { installFooter } from "./footer";
|
|
18
20
|
import { emptyGitStatus, readGitStatus } from "./git";
|
|
@@ -24,6 +26,22 @@ import { type FooterState, createInitialState, syncState } from "./state";
|
|
|
24
26
|
import { PolishedEditor } from "./ui";
|
|
25
27
|
import { installUserMessageStyle } from "./user-message";
|
|
26
28
|
|
|
29
|
+
const ZENTUI_EDITOR_FACTORY = Symbol.for("pi-zentui.editor-factory");
|
|
30
|
+
|
|
31
|
+
type EditorFactory = NonNullable<Parameters<ExtensionContext["ui"]["setEditorComponent"]>[0]>;
|
|
32
|
+
|
|
33
|
+
type ZentuiEditorFactory = EditorFactory & {
|
|
34
|
+
[ZENTUI_EDITOR_FACTORY]?: true;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type ApplyUiResult = {
|
|
38
|
+
editorBlocked: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function isZentuiEditorFactory(factory: EditorFactory | undefined): boolean {
|
|
42
|
+
return Boolean((factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_FACTORY]);
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
export default function (pi: ExtensionAPI) {
|
|
28
46
|
const state: FooterState = createInitialState(emptyGitStatus());
|
|
29
47
|
|
|
@@ -33,6 +51,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
33
51
|
let getActiveExtensionStatuses: () => ReadonlyMap<string, string> = () => new Map();
|
|
34
52
|
let stopRefreshInterval: StopProjectRefreshInterval = () => {};
|
|
35
53
|
let cleanupPrototypePatches: () => void = () => {};
|
|
54
|
+
let footerInstalled = false;
|
|
55
|
+
let editorInstalled = false;
|
|
56
|
+
let prototypePatchesInstalled = false;
|
|
36
57
|
let projectRefreshInFlight = false;
|
|
37
58
|
let projectRefreshPending = false;
|
|
38
59
|
|
|
@@ -40,6 +61,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
40
61
|
const getActiveTheme = () => activeTheme;
|
|
41
62
|
const getCurrentConfig = () => currentConfig;
|
|
42
63
|
const getThinkingLevel = () => pi.getThinkingLevel();
|
|
64
|
+
const syncFooterState = (ctx: ExtensionContext) =>
|
|
65
|
+
syncState(state, ctx, currentConfig.icons.cacheHit);
|
|
43
66
|
|
|
44
67
|
const refreshProjectState = async (ctx: ExtensionContext) => {
|
|
45
68
|
const [gitStatus, runtime] = await Promise.all([
|
|
@@ -69,44 +92,75 @@ export default function (pi: ExtensionAPI) {
|
|
|
69
92
|
|
|
70
93
|
const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
|
|
71
94
|
if (!ctx.hasUI) return;
|
|
72
|
-
|
|
73
|
-
if (project) scheduleProjectRefresh(ctx);
|
|
95
|
+
syncFooterState(ctx);
|
|
96
|
+
if (project && currentConfig.features.statusLine) scheduleProjectRefresh(ctx);
|
|
74
97
|
refresh();
|
|
75
98
|
};
|
|
76
99
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
theme,
|
|
83
|
-
keybindings,
|
|
84
|
-
ctx.ui.theme,
|
|
85
|
-
getCurrentConfig,
|
|
86
|
-
() => ({
|
|
87
|
-
modelLabel: state.modelLabel,
|
|
88
|
-
providerLabel: state.providerLabel,
|
|
89
|
-
}),
|
|
90
|
-
getThinkingLevel,
|
|
91
|
-
),
|
|
92
|
-
);
|
|
100
|
+
const stopProjectRefresh = () => {
|
|
101
|
+
stopRefreshInterval();
|
|
102
|
+
stopRefreshInterval = () => {};
|
|
103
|
+
projectRefreshInFlight = false;
|
|
104
|
+
projectRefreshPending = false;
|
|
93
105
|
};
|
|
94
106
|
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
activeTheme = ctx.ui.theme;
|
|
98
|
-
cleanupPrototypePatches();
|
|
107
|
+
const installPrototypePatches = () => {
|
|
108
|
+
if (prototypePatchesInstalled) return;
|
|
99
109
|
const cleanupSelectorBorderStyle = installSelectorBorderStyle(getActiveTheme, getCurrentConfig);
|
|
100
110
|
const cleanupUserMessageStyle = installUserMessageStyle(getActiveTheme, getCurrentConfig);
|
|
101
111
|
cleanupPrototypePatches = () => {
|
|
102
112
|
cleanupSelectorBorderStyle();
|
|
103
113
|
cleanupUserMessageStyle();
|
|
104
114
|
};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
prototypePatchesInstalled = true;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const uninstallPrototypePatches = () => {
|
|
119
|
+
cleanupPrototypePatches();
|
|
120
|
+
cleanupPrototypePatches = () => {};
|
|
121
|
+
prototypePatchesInstalled = false;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const makeEditorFactory = (ctx: ExtensionContext): ZentuiEditorFactory => {
|
|
125
|
+
const factory = ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) =>
|
|
126
|
+
new PolishedEditor(
|
|
127
|
+
tui,
|
|
128
|
+
theme,
|
|
129
|
+
keybindings,
|
|
130
|
+
ctx.ui.theme,
|
|
131
|
+
getCurrentConfig,
|
|
132
|
+
() => ({
|
|
133
|
+
modelLabel: state.modelLabel,
|
|
134
|
+
providerLabel: state.providerLabel,
|
|
135
|
+
}),
|
|
136
|
+
getThinkingLevel,
|
|
137
|
+
)) as ZentuiEditorFactory;
|
|
138
|
+
factory[ZENTUI_EDITOR_FACTORY] = true;
|
|
139
|
+
return factory;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const installEditor = (ctx: ExtensionContext): boolean => {
|
|
143
|
+
const currentFactory = ctx.ui.getEditorComponent();
|
|
144
|
+
if (currentFactory && !isZentuiEditorFactory(currentFactory)) return false;
|
|
145
|
+
|
|
146
|
+
installPrototypePatches();
|
|
147
|
+
ctx.ui.setEditorComponent(makeEditorFactory(ctx));
|
|
148
|
+
editorInstalled = true;
|
|
149
|
+
return true;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const uninstallEditor = (ctx: ExtensionContext): boolean => {
|
|
153
|
+
const currentFactory = ctx.ui.getEditorComponent();
|
|
154
|
+
if (currentFactory && !isZentuiEditorFactory(currentFactory)) return false;
|
|
155
|
+
|
|
156
|
+
uninstallPrototypePatches();
|
|
157
|
+
ctx.ui.setEditorComponent(undefined);
|
|
158
|
+
editorInstalled = false;
|
|
159
|
+
return true;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const installStatusLine = (ctx: ExtensionContext) => {
|
|
163
|
+
if (footerInstalled) return;
|
|
110
164
|
installFooter(ctx, state, getCurrentConfig, {
|
|
111
165
|
setRequestRender: (fn) => {
|
|
112
166
|
requestFooterRender = fn;
|
|
@@ -116,7 +170,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
116
170
|
getActiveExtensionStatuses = fn ?? (() => new Map());
|
|
117
171
|
},
|
|
118
172
|
});
|
|
119
|
-
|
|
173
|
+
footerInstalled = true;
|
|
174
|
+
stopProjectRefresh();
|
|
120
175
|
stopRefreshInterval = startProjectRefreshInterval(currentConfig.projectRefreshIntervalMs, () =>
|
|
121
176
|
scheduleProjectRefresh(ctx),
|
|
122
177
|
);
|
|
@@ -124,19 +179,57 @@ export default function (pi: ExtensionAPI) {
|
|
|
124
179
|
refresh();
|
|
125
180
|
};
|
|
126
181
|
|
|
182
|
+
const uninstallStatusLine = (ctx: ExtensionContext) => {
|
|
183
|
+
stopProjectRefresh();
|
|
184
|
+
ctx.ui.setFooter(undefined);
|
|
185
|
+
footerInstalled = false;
|
|
186
|
+
requestFooterRender = undefined;
|
|
187
|
+
getActiveExtensionStatuses = () => new Map();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const applyConfiguredUi = (ctx: ExtensionContext): ApplyUiResult => {
|
|
191
|
+
const result: ApplyUiResult = { editorBlocked: false };
|
|
192
|
+
if (!ctx.hasUI) return result;
|
|
193
|
+
activeTheme = ctx.ui.theme;
|
|
194
|
+
if (currentConfig.features.editor) {
|
|
195
|
+
if (!editorInstalled) result.editorBlocked = !installEditor(ctx);
|
|
196
|
+
} else if (editorInstalled || prototypePatchesInstalled) {
|
|
197
|
+
result.editorBlocked = !uninstallEditor(ctx);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (currentConfig.features.statusLine) {
|
|
201
|
+
installStatusLine(ctx);
|
|
202
|
+
} else if (footerInstalled) {
|
|
203
|
+
uninstallStatusLine(ctx);
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const installUi = (ctx: ExtensionContext) => {
|
|
209
|
+
if (!ctx.hasUI) return;
|
|
210
|
+
activeTheme = ctx.ui.theme;
|
|
211
|
+
uninstallPrototypePatches();
|
|
212
|
+
footerInstalled = false;
|
|
213
|
+
editorInstalled = false;
|
|
214
|
+
ensureConfigExists();
|
|
215
|
+
currentConfig = loadConfig();
|
|
216
|
+
syncFooterState(ctx);
|
|
217
|
+
stopProjectRefresh();
|
|
218
|
+
applyConfiguredUi(ctx);
|
|
219
|
+
refresh();
|
|
220
|
+
};
|
|
221
|
+
|
|
127
222
|
const cleanupUi = (ctx?: ExtensionContext) => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
stopRefreshInterval();
|
|
131
|
-
stopRefreshInterval = () => {};
|
|
132
|
-
projectRefreshInFlight = false;
|
|
133
|
-
projectRefreshPending = false;
|
|
223
|
+
uninstallPrototypePatches();
|
|
224
|
+
stopProjectRefresh();
|
|
134
225
|
requestFooterRender = undefined;
|
|
135
226
|
getActiveExtensionStatuses = () => new Map();
|
|
136
227
|
if (ctx?.hasUI) {
|
|
137
228
|
ctx.ui.setFooter(undefined);
|
|
138
229
|
ctx.ui.setEditorComponent(undefined);
|
|
139
230
|
}
|
|
231
|
+
footerInstalled = false;
|
|
232
|
+
editorInstalled = false;
|
|
140
233
|
activeTheme = undefined;
|
|
141
234
|
};
|
|
142
235
|
|
|
@@ -156,6 +249,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
156
249
|
setColorSources(patch: Partial<ColorSourcesConfig>) {
|
|
157
250
|
currentConfig = saveColorSourcesPatch(patch);
|
|
158
251
|
},
|
|
252
|
+
setUiFeatures(patch: Partial<UiFeaturesConfig>, ctx: ExtensionContext) {
|
|
253
|
+
currentConfig = saveUiFeaturesPatch(patch);
|
|
254
|
+
const result = applyConfiguredUi(ctx);
|
|
255
|
+
return {
|
|
256
|
+
applied: !(patch.editor !== undefined && result.editorBlocked),
|
|
257
|
+
reason: result.editorBlocked
|
|
258
|
+
? "another extension is currently managing the editor; reload Pi to apply this change"
|
|
259
|
+
: undefined,
|
|
260
|
+
};
|
|
261
|
+
},
|
|
159
262
|
getActiveExtensionStatuses() {
|
|
160
263
|
return getActiveExtensionStatuses();
|
|
161
264
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import {
|
|
4
|
+
type AutocompleteItem,
|
|
4
5
|
type SettingItem,
|
|
5
6
|
SettingsList,
|
|
6
7
|
type SettingsListTheme,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
type ColorSourcesConfig,
|
|
12
13
|
type ExtensionStatusPlacement,
|
|
13
14
|
type PolishedTuiConfig,
|
|
15
|
+
type UiFeaturesConfig,
|
|
14
16
|
getExtensionStatusPlacement,
|
|
15
17
|
isExtensionStatusPlacement,
|
|
16
18
|
} from "./config";
|
|
@@ -24,58 +26,152 @@ const extensionStatusPlacementValues: ExtensionStatusPlacement[] = [
|
|
|
24
26
|
"middle",
|
|
25
27
|
"right",
|
|
26
28
|
];
|
|
29
|
+
type FeatureState = "enabled" | "disabled";
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
const featureStateValues: FeatureState[] = ["enabled", "disabled"];
|
|
32
|
+
|
|
33
|
+
type ColorSettingId = "starship" | "editorMessages";
|
|
34
|
+
type FeatureSettingId = keyof UiFeaturesConfig;
|
|
29
35
|
|
|
30
36
|
type SettingsCommandDeps = {
|
|
31
37
|
getConfig: () => PolishedTuiConfig;
|
|
32
38
|
setColorSources: (patch: Partial<ColorSourcesConfig>) => void;
|
|
39
|
+
setUiFeatures: (
|
|
40
|
+
patch: Partial<UiFeaturesConfig>,
|
|
41
|
+
ctx: ExtensionContext,
|
|
42
|
+
) => { applied: boolean; reason?: string };
|
|
33
43
|
getActiveExtensionStatuses: () => ReadonlyMap<string, string>;
|
|
34
44
|
setExtensionStatusPlacement: (key: string, placement: ExtensionStatusPlacement) => void;
|
|
35
45
|
requestRender: () => void;
|
|
36
46
|
settingsListTheme?: SettingsListTheme;
|
|
37
47
|
};
|
|
38
48
|
|
|
39
|
-
const
|
|
49
|
+
const colorSettingLabels: Record<ColorSettingId, string> = {
|
|
40
50
|
starship: "Starship/footer colors",
|
|
41
51
|
editorMessages: "Editor + previous messages",
|
|
42
52
|
};
|
|
43
53
|
|
|
44
|
-
const
|
|
54
|
+
const colorSettingDescriptions: Record<ColorSettingId, string> = {
|
|
45
55
|
starship:
|
|
46
56
|
"Choose whether footer runtime/git/context colors use Pi theme tokens or terminal palette styles.",
|
|
47
57
|
editorMessages:
|
|
48
58
|
"Choose whether editor and previous user-message borders/rails use Pi theme colors or terminal palette styles.",
|
|
49
59
|
};
|
|
50
60
|
|
|
61
|
+
const featureSettingLabels: Record<FeatureSettingId, string> = {
|
|
62
|
+
editor: "Editor",
|
|
63
|
+
statusLine: "Status line",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const featureSettingDescriptions: Record<FeatureSettingId, string> = {
|
|
67
|
+
editor:
|
|
68
|
+
"Enable or disable Zentui's custom editor, selector borders, and previous-message chrome.",
|
|
69
|
+
statusLine: "Enable or disable Zentui's custom footer/status line.",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const directCommandSuggestions = [
|
|
73
|
+
"editor enable",
|
|
74
|
+
"editor disable",
|
|
75
|
+
"editor toggle",
|
|
76
|
+
"statusline enable",
|
|
77
|
+
"statusline disable",
|
|
78
|
+
"statusline toggle",
|
|
79
|
+
];
|
|
80
|
+
|
|
51
81
|
function isColorSource(value: string): value is ColorSource {
|
|
52
82
|
return value === "theme" || value === "terminal";
|
|
53
83
|
}
|
|
54
84
|
|
|
55
|
-
function
|
|
85
|
+
function isColorSettingId(value: string): value is ColorSettingId {
|
|
56
86
|
return value === "starship" || value === "editorMessages";
|
|
57
87
|
}
|
|
58
88
|
|
|
89
|
+
function isFeatureSettingId(value: string): value is FeatureSettingId {
|
|
90
|
+
return value === "editor" || value === "statusLine";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isFeatureState(value: string): value is FeatureState {
|
|
94
|
+
return value === "enabled" || value === "disabled";
|
|
95
|
+
}
|
|
96
|
+
|
|
59
97
|
function editorMessageValue(config: PolishedTuiConfig): ColorSource | "mixed" {
|
|
60
98
|
return config.colorSources.editor === config.colorSources.userMessages
|
|
61
99
|
? config.colorSources.editor
|
|
62
100
|
: "mixed";
|
|
63
101
|
}
|
|
64
102
|
|
|
65
|
-
function patchForSetting(id:
|
|
103
|
+
function patchForSetting(id: ColorSettingId, value: ColorSource): Partial<ColorSourcesConfig> {
|
|
66
104
|
return id === "starship" ? { starship: value } : { editor: value, userMessages: value };
|
|
67
105
|
}
|
|
68
106
|
|
|
107
|
+
function featureValue(enabled: boolean): FeatureState {
|
|
108
|
+
return enabled ? "enabled" : "disabled";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function featurePatch(id: FeatureSettingId, value: FeatureState): Partial<UiFeaturesConfig> {
|
|
112
|
+
return { [id]: value === "enabled" } as Partial<UiFeaturesConfig>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function usageText(): string {
|
|
116
|
+
return "Usage: /zentui [editor|statusline] [enable|disable|toggle]";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function featureNotification(
|
|
120
|
+
feature: FeatureSettingId,
|
|
121
|
+
value: FeatureState,
|
|
122
|
+
result: { applied: boolean; reason?: string },
|
|
123
|
+
): string {
|
|
124
|
+
const base = `${featureSettingLabels[feature]}: ${value}`;
|
|
125
|
+
return result.applied ? base : `${base} (${result.reason ?? "reload Pi to apply this change"})`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseDirectFeatureCommand(
|
|
129
|
+
args: string,
|
|
130
|
+
config: PolishedTuiConfig,
|
|
131
|
+
): { feature: FeatureSettingId; enabled: boolean } | undefined {
|
|
132
|
+
const normalized = args.trim().toLowerCase().replaceAll(/[_-]+/g, " ");
|
|
133
|
+
if (!normalized) return undefined;
|
|
134
|
+
|
|
135
|
+
const words = normalized.split(/\s+/g).filter(Boolean);
|
|
136
|
+
const hasWord = (value: string) => words.includes(value);
|
|
137
|
+
const feature = hasWord("editor")
|
|
138
|
+
? "editor"
|
|
139
|
+
: hasWord("footer") || hasWord("statusline") || hasWord("status")
|
|
140
|
+
? "statusLine"
|
|
141
|
+
: undefined;
|
|
142
|
+
const action = hasWord("toggle")
|
|
143
|
+
? "toggle"
|
|
144
|
+
: hasWord("enable") || hasWord("enabled") || hasWord("on")
|
|
145
|
+
? "enable"
|
|
146
|
+
: hasWord("disable") || hasWord("disabled") || hasWord("off")
|
|
147
|
+
? "disable"
|
|
148
|
+
: undefined;
|
|
149
|
+
|
|
150
|
+
if (!feature || !action) return undefined;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
feature,
|
|
154
|
+
enabled: action === "toggle" ? !config.features[feature] : action === "enable",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function argumentCompletions(prefix: string): AutocompleteItem[] | null {
|
|
159
|
+
const trimmedPrefix = prefix.trimStart().toLowerCase();
|
|
160
|
+
const items = directCommandSuggestions.map((value) => ({ value, label: value }));
|
|
161
|
+
const matches = items.filter((item) => item.value.startsWith(trimmedPrefix));
|
|
162
|
+
return matches.length > 0 ? matches : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
69
165
|
function buildItems(
|
|
70
166
|
config: PolishedTuiConfig,
|
|
71
167
|
activeStatusCount: number,
|
|
72
168
|
thirdPartyStatusesSubmenu: SettingItem["submenu"],
|
|
73
169
|
): SettingItem[] {
|
|
74
170
|
return [
|
|
75
|
-
...(Object.keys(
|
|
171
|
+
...(Object.keys(colorSettingLabels) as ColorSettingId[]).map((key) => ({
|
|
76
172
|
id: key,
|
|
77
|
-
label:
|
|
78
|
-
description:
|
|
173
|
+
label: colorSettingLabels[key],
|
|
174
|
+
description: colorSettingDescriptions[key],
|
|
79
175
|
currentValue: key === "starship" ? config.colorSources.starship : editorMessageValue(config),
|
|
80
176
|
values: colorSourceValues,
|
|
81
177
|
})),
|
|
@@ -87,17 +183,62 @@ function buildItems(
|
|
|
87
183
|
currentValue: `${activeStatusCount} active`,
|
|
88
184
|
submenu: thirdPartyStatusesSubmenu,
|
|
89
185
|
},
|
|
186
|
+
...(Object.keys(featureSettingLabels) as FeatureSettingId[]).map((key) => ({
|
|
187
|
+
id: key,
|
|
188
|
+
label: featureSettingLabels[key],
|
|
189
|
+
description: featureSettingDescriptions[key],
|
|
190
|
+
currentValue: featureValue(config.features[key]),
|
|
191
|
+
values: featureStateValues,
|
|
192
|
+
})),
|
|
90
193
|
];
|
|
91
194
|
}
|
|
92
195
|
|
|
93
196
|
export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCommandDeps): void {
|
|
94
197
|
pi.registerCommand("zentui", {
|
|
95
198
|
description: "Configure Zentui",
|
|
199
|
+
getArgumentCompletions: argumentCompletions,
|
|
96
200
|
handler: async (_args, ctx) => {
|
|
201
|
+
const args = typeof _args === "string" ? _args : "";
|
|
202
|
+
const directCommand = parseDirectFeatureCommand(args, deps.getConfig());
|
|
203
|
+
if (directCommand) {
|
|
204
|
+
try {
|
|
205
|
+
const result = deps.setUiFeatures(
|
|
206
|
+
{ [directCommand.feature]: directCommand.enabled },
|
|
207
|
+
ctx,
|
|
208
|
+
);
|
|
209
|
+
deps.requestRender();
|
|
210
|
+
if (ctx.hasUI) {
|
|
211
|
+
ctx.ui.notify(
|
|
212
|
+
featureNotification(
|
|
213
|
+
directCommand.feature,
|
|
214
|
+
featureValue(directCommand.enabled),
|
|
215
|
+
result,
|
|
216
|
+
),
|
|
217
|
+
"info",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
} catch (error) {
|
|
221
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
222
|
+
if (ctx.hasUI) ctx.ui.notify(`Could not update Zentui settings: ${message}`, "error");
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (args.trim()) {
|
|
228
|
+
if (ctx.hasUI) ctx.ui.notify(usageText(), "warning");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
97
232
|
if (!ctx.hasUI) return;
|
|
98
233
|
|
|
99
234
|
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
100
235
|
const settingsListTheme = deps.settingsListTheme ?? getSettingsListTheme();
|
|
236
|
+
const applyFeatureChange = (id: FeatureSettingId, newValue: FeatureState) => {
|
|
237
|
+
const result = deps.setUiFeatures(featurePatch(id, newValue), ctx);
|
|
238
|
+
deps.requestRender();
|
|
239
|
+
ctx.ui.notify(featureNotification(id, newValue, result), "info");
|
|
240
|
+
tui.requestRender();
|
|
241
|
+
};
|
|
101
242
|
const makeThirdPartyStatusesSubmenu: SettingItem["submenu"] = (_currentValue, close) => {
|
|
102
243
|
const activeStatuses = Array.from(deps.getActiveExtensionStatuses().entries()).sort(
|
|
103
244
|
([a], [b]) => (a < b ? -1 : a > b ? 1 : 0),
|
|
@@ -175,14 +316,36 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
|
|
|
175
316
|
5,
|
|
176
317
|
settingsListTheme,
|
|
177
318
|
(id, newValue) => {
|
|
178
|
-
if (!isSettingId(id) || !isColorSource(newValue)) return;
|
|
179
|
-
|
|
180
319
|
try {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
320
|
+
if (isColorSettingId(id) && isColorSource(newValue)) {
|
|
321
|
+
deps.setColorSources(patchForSetting(id, newValue));
|
|
322
|
+
settingsList.updateValue(id, newValue);
|
|
323
|
+
deps.requestRender();
|
|
324
|
+
ctx.ui.notify(`${colorSettingLabels[id]}: ${newValue}`, "info");
|
|
325
|
+
tui.requestRender();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (isFeatureSettingId(id) && isFeatureState(newValue)) {
|
|
330
|
+
settingsList.updateValue(id, newValue);
|
|
331
|
+
if (id === "editor") {
|
|
332
|
+
done(undefined);
|
|
333
|
+
// Changing the editor component while ctx.ui.custom() is active clears the
|
|
334
|
+
// custom component without resolving it, leaving Pi's input loop stuck.
|
|
335
|
+
// Close the settings UI first, then apply the editor swap on the next tick.
|
|
336
|
+
setTimeout(() => {
|
|
337
|
+
try {
|
|
338
|
+
applyFeatureChange(id, newValue);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
341
|
+
ctx.ui.notify(`Could not update Zentui settings: ${message}`, "error");
|
|
342
|
+
}
|
|
343
|
+
}, 0);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
applyFeatureChange(id, newValue);
|
|
348
|
+
}
|
|
186
349
|
} catch (error) {
|
|
187
350
|
const message = error instanceof Error ? error.message : String(error);
|
|
188
351
|
ctx.ui.notify(`Could not update Zentui settings: ${message}`, "error");
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
|
-
type UsageTotals,
|
|
4
3
|
buildContextLabel,
|
|
5
4
|
buildCostLabel,
|
|
6
5
|
buildTokenLabel,
|
|
@@ -31,11 +30,11 @@ export function createInitialState(gitDefaults: GitStatusSummary): FooterState {
|
|
|
31
30
|
};
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
export function syncState(state: FooterState, ctx: ExtensionContext): void {
|
|
33
|
+
export function syncState(state: FooterState, ctx: ExtensionContext, cacheHitIcon: string): void {
|
|
35
34
|
const totals = getUsageTotals(ctx);
|
|
36
35
|
state.modelLabel = ctx.model?.id ?? "no-model";
|
|
37
36
|
state.providerLabel = formatProviderLabel(ctx.model?.provider);
|
|
38
37
|
state.contextLabel = buildContextLabel(ctx);
|
|
39
|
-
state.tokenLabel = buildTokenLabel(totals);
|
|
38
|
+
state.tokenLabel = buildTokenLabel(totals, cacheHitIcon);
|
|
40
39
|
state.costLabel = buildCostLabel(totals);
|
|
41
40
|
}
|