pi-zentui 0.2.2 → 0.2.6
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 +2 -1
- package/extensions/zentui/config.ts +54 -0
- package/extensions/zentui/extension-status.ts +41 -6
- package/extensions/zentui/footer.ts +8 -6
- package/extensions/zentui/index.ts +47 -12
- package/extensions/zentui/settings-command.ts +69 -19
- package/extensions/zentui/ui.ts +59 -2
- package/extensions/zentui/user-message.ts +94 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,10 +19,14 @@ export type UiFeaturesConfig = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
export type ExtensionStatusPlacement = "off" | "left" | "middle" | "right";
|
|
22
|
+
export type ExtensionStatusColorMode = "zentui" | "original";
|
|
23
|
+
|
|
24
|
+
const DEFAULT_EXTENSION_STATUS_COLOR_MODE: ExtensionStatusColorMode = "zentui";
|
|
22
25
|
|
|
23
26
|
export type ExtensionStatusesConfig = {
|
|
24
27
|
defaultPlacement: ExtensionStatusPlacement;
|
|
25
28
|
placements: Record<string, ExtensionStatusPlacement>;
|
|
29
|
+
colorModes: Record<string, ExtensionStatusColorMode>;
|
|
26
30
|
};
|
|
27
31
|
|
|
28
32
|
const DEFAULT_PROJECT_REFRESH_INTERVAL_MS = 30_000;
|
|
@@ -123,6 +127,7 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
123
127
|
extensionStatuses: {
|
|
124
128
|
defaultPlacement: "right",
|
|
125
129
|
placements: {},
|
|
130
|
+
colorModes: {},
|
|
126
131
|
},
|
|
127
132
|
};
|
|
128
133
|
|
|
@@ -251,6 +256,10 @@ export function isExtensionStatusPlacement(value: unknown): value is ExtensionSt
|
|
|
251
256
|
return value === "off" || value === "left" || value === "middle" || value === "right";
|
|
252
257
|
}
|
|
253
258
|
|
|
259
|
+
export function isExtensionStatusColorMode(value: unknown): value is ExtensionStatusColorMode {
|
|
260
|
+
return value === "zentui" || value === "original";
|
|
261
|
+
}
|
|
262
|
+
|
|
254
263
|
function normalizeExtensionStatuses(record: Record<string, unknown>): ExtensionStatusesConfig {
|
|
255
264
|
const defaultPlacement = isExtensionStatusPlacement(record.defaultPlacement)
|
|
256
265
|
? record.defaultPlacement
|
|
@@ -263,10 +272,19 @@ function normalizeExtensionStatuses(record: Record<string, unknown>): ExtensionS
|
|
|
263
272
|
),
|
|
264
273
|
)
|
|
265
274
|
: {};
|
|
275
|
+
const colorModes = isRecord(record.colorModes)
|
|
276
|
+
? Object.fromEntries(
|
|
277
|
+
Object.entries(record.colorModes).filter(
|
|
278
|
+
(entry): entry is [string, ExtensionStatusColorMode] =>
|
|
279
|
+
isExtensionStatusColorMode(entry[1]),
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
: {};
|
|
266
283
|
|
|
267
284
|
return {
|
|
268
285
|
defaultPlacement,
|
|
269
286
|
placements,
|
|
287
|
+
colorModes,
|
|
270
288
|
};
|
|
271
289
|
}
|
|
272
290
|
|
|
@@ -345,6 +363,7 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
345
363
|
extensionStatuses: {
|
|
346
364
|
defaultPlacement: extensionStatuses.defaultPlacement,
|
|
347
365
|
placements: { ...extensionStatuses.placements },
|
|
366
|
+
colorModes: { ...extensionStatuses.colorModes },
|
|
348
367
|
},
|
|
349
368
|
};
|
|
350
369
|
}
|
|
@@ -356,6 +375,13 @@ export function getExtensionStatusPlacement(
|
|
|
356
375
|
return config.extensionStatuses.placements[key] ?? config.extensionStatuses.defaultPlacement;
|
|
357
376
|
}
|
|
358
377
|
|
|
378
|
+
export function getExtensionStatusColorMode(
|
|
379
|
+
config: PolishedTuiConfig,
|
|
380
|
+
key: string,
|
|
381
|
+
): ExtensionStatusColorMode {
|
|
382
|
+
return config.extensionStatuses.colorModes[key] ?? DEFAULT_EXTENSION_STATUS_COLOR_MODE;
|
|
383
|
+
}
|
|
384
|
+
|
|
359
385
|
export function loadConfig(): PolishedTuiConfig {
|
|
360
386
|
try {
|
|
361
387
|
if (!existsSync(configPath)) return mergeConfig({});
|
|
@@ -424,3 +450,31 @@ export function saveExtensionStatusPlacement(
|
|
|
424
450
|
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
425
451
|
return mergeConfig(record);
|
|
426
452
|
}
|
|
453
|
+
|
|
454
|
+
export function saveExtensionStatusColorMode(
|
|
455
|
+
key: string,
|
|
456
|
+
colorMode: ExtensionStatusColorMode,
|
|
457
|
+
path = configPath,
|
|
458
|
+
): PolishedTuiConfig {
|
|
459
|
+
const record = readConfigRecord(path);
|
|
460
|
+
const existingExtensionStatuses = isRecord(record.extensionStatuses)
|
|
461
|
+
? { ...(record.extensionStatuses as Record<string, unknown>) }
|
|
462
|
+
: {};
|
|
463
|
+
const existingColorModes = isRecord(existingExtensionStatuses.colorModes)
|
|
464
|
+
? { ...(existingExtensionStatuses.colorModes as Record<string, unknown>) }
|
|
465
|
+
: {};
|
|
466
|
+
|
|
467
|
+
Object.defineProperty(existingColorModes, key, {
|
|
468
|
+
value: colorMode,
|
|
469
|
+
enumerable: true,
|
|
470
|
+
configurable: true,
|
|
471
|
+
writable: true,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
record.extensionStatuses = {
|
|
475
|
+
...existingExtensionStatuses,
|
|
476
|
+
colorModes: existingColorModes,
|
|
477
|
+
};
|
|
478
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
479
|
+
return mergeConfig(record);
|
|
480
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { stripVTControlCharacters } from "node:util";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
2
|
+
import type {
|
|
3
|
+
ExtensionStatusColorMode,
|
|
4
|
+
ExtensionStatusPlacement,
|
|
5
|
+
PolishedTuiConfig,
|
|
6
|
+
} from "./config";
|
|
7
|
+
import { getExtensionStatusColorMode, getExtensionStatusPlacement } from "./config";
|
|
4
8
|
|
|
5
9
|
export type ExtensionStatusSegment = {
|
|
6
10
|
key: string;
|
|
7
11
|
text: string;
|
|
8
12
|
placement: ExtensionStatusPlacement;
|
|
13
|
+
colorMode: ExtensionStatusColorMode;
|
|
9
14
|
};
|
|
10
15
|
|
|
11
16
|
export type ExtensionStatusSegmentsByPlacement = {
|
|
@@ -14,18 +19,44 @@ export type ExtensionStatusSegmentsByPlacement = {
|
|
|
14
19
|
right: ExtensionStatusSegment[];
|
|
15
20
|
};
|
|
16
21
|
|
|
22
|
+
const safeSgrPattern = /\x1b\[[0-9;:]*m/g;
|
|
23
|
+
const sgrPlaceholderPattern = /__ZENTUI_SGR_(\d+)__/g;
|
|
24
|
+
|
|
17
25
|
function compareKeys(a: ExtensionStatusSegment, b: ExtensionStatusSegment): number {
|
|
18
26
|
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
return
|
|
29
|
+
function normalizeStatusWhitespace(value: string): string {
|
|
30
|
+
return value
|
|
23
31
|
.replace(/[\r\n\t\f\v]+/g, " ")
|
|
24
32
|
.replace(/[\u0000-\u001f\u007f-\u009f]/g, "")
|
|
25
33
|
.replace(/\s+/g, " ")
|
|
26
34
|
.trim();
|
|
27
35
|
}
|
|
28
36
|
|
|
37
|
+
export function sanitizeExtensionStatusText(value: string): string {
|
|
38
|
+
return normalizeStatusWhitespace(stripVTControlCharacters(value));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasVisibleStatusText(value: string): boolean {
|
|
42
|
+
return sanitizeExtensionStatusText(value).length > 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sanitizeExtensionStatusOriginalText(value: string): string {
|
|
46
|
+
const safeSequences: string[] = [];
|
|
47
|
+
const protectedValue = value.replace(safeSgrPattern, (sequence) => {
|
|
48
|
+
const index = safeSequences.push(sequence) - 1;
|
|
49
|
+
return `__ZENTUI_SGR_${index}__`;
|
|
50
|
+
});
|
|
51
|
+
const cleaned = normalizeStatusWhitespace(stripVTControlCharacters(protectedValue));
|
|
52
|
+
const restored = cleaned.replace(sgrPlaceholderPattern, (_match, indexText: string) => {
|
|
53
|
+
const index = Number.parseInt(indexText, 10);
|
|
54
|
+
return safeSequences[index] ?? "";
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return hasVisibleStatusText(restored) ? restored : "";
|
|
58
|
+
}
|
|
59
|
+
|
|
29
60
|
export function collectExtensionStatusSegments(
|
|
30
61
|
statuses: ReadonlyMap<string, string>,
|
|
31
62
|
config: PolishedTuiConfig,
|
|
@@ -40,10 +71,14 @@ export function collectExtensionStatusSegments(
|
|
|
40
71
|
const placement = getExtensionStatusPlacement(config, key);
|
|
41
72
|
if (placement === "off") continue;
|
|
42
73
|
|
|
43
|
-
const
|
|
74
|
+
const colorMode = getExtensionStatusColorMode(config, key);
|
|
75
|
+
const text =
|
|
76
|
+
colorMode === "original"
|
|
77
|
+
? sanitizeExtensionStatusOriginalText(value)
|
|
78
|
+
: sanitizeExtensionStatusText(value);
|
|
44
79
|
if (!text) continue;
|
|
45
80
|
|
|
46
|
-
segments[placement].push({ key, text, placement });
|
|
81
|
+
segments[placement].push({ key, text, placement, colorMode });
|
|
47
82
|
}
|
|
48
83
|
|
|
49
84
|
segments.left.sort(compareKeys);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { PolishedTuiConfig } from "./config";
|
|
4
|
-
import { collectExtensionStatusSegments } from "./extension-status";
|
|
4
|
+
import { type ExtensionStatusSegment, collectExtensionStatusSegments } from "./extension-status";
|
|
5
5
|
import { formatCwdLabel, formatRuntimeSegment } from "./format";
|
|
6
6
|
import type { FooterState } from "./state";
|
|
7
7
|
import { renderStyleForSource } from "./style";
|
|
@@ -218,14 +218,16 @@ export function installFooter(
|
|
|
218
218
|
footerData.getExtensionStatuses(),
|
|
219
219
|
config,
|
|
220
220
|
);
|
|
221
|
-
const renderExtensionStatus = (
|
|
222
|
-
|
|
221
|
+
const renderExtensionStatus = (segment: ExtensionStatusSegment) =>
|
|
222
|
+
segment.colorMode === "original"
|
|
223
|
+
? segment.text
|
|
224
|
+
: renderStyleForSource(theme, colorSource, config.colors.extensionStatus, segment.text);
|
|
223
225
|
const content = composeFooterContent(
|
|
224
226
|
left,
|
|
225
227
|
right,
|
|
226
|
-
extensionStatuses.left.map(
|
|
227
|
-
extensionStatuses.middle.map(
|
|
228
|
-
extensionStatuses.right.map(
|
|
228
|
+
extensionStatuses.left.map(renderExtensionStatus),
|
|
229
|
+
extensionStatuses.middle.map(renderExtensionStatus),
|
|
230
|
+
extensionStatuses.right.map(renderExtensionStatus),
|
|
229
231
|
separator,
|
|
230
232
|
innerWidth,
|
|
231
233
|
);
|
|
@@ -7,12 +7,14 @@ import type {
|
|
|
7
7
|
import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
|
|
8
8
|
import {
|
|
9
9
|
type ColorSourcesConfig,
|
|
10
|
+
type ExtensionStatusColorMode,
|
|
10
11
|
type ExtensionStatusPlacement,
|
|
11
12
|
type PolishedTuiConfig,
|
|
12
13
|
type UiFeaturesConfig,
|
|
13
14
|
ensureConfigExists,
|
|
14
15
|
loadConfig,
|
|
15
16
|
saveColorSourcesPatch,
|
|
17
|
+
saveExtensionStatusColorMode,
|
|
16
18
|
saveExtensionStatusPlacement,
|
|
17
19
|
saveUiFeaturesPatch,
|
|
18
20
|
} from "./config";
|
|
@@ -32,11 +34,13 @@ import { PolishedEditor, WrappedPolishedEditor } from "./ui";
|
|
|
32
34
|
import { installUserMessageStyle } from "./user-message";
|
|
33
35
|
|
|
34
36
|
const ZENTUI_EDITOR_FACTORY = Symbol.for("pi-zentui.editor-factory");
|
|
37
|
+
const ZENTUI_EDITOR_BASE_FACTORY = Symbol.for("pi-zentui.editor-base-factory");
|
|
35
38
|
|
|
36
39
|
type EditorFactory = NonNullable<Parameters<ExtensionContext["ui"]["setEditorComponent"]>[0]>;
|
|
37
40
|
|
|
38
41
|
type ZentuiEditorFactory = EditorFactory & {
|
|
39
42
|
[ZENTUI_EDITOR_FACTORY]?: true;
|
|
43
|
+
[ZENTUI_EDITOR_BASE_FACTORY]?: EditorFactory;
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
type ApplyUiResult = {
|
|
@@ -49,6 +53,15 @@ function isZentuiEditorFactory(factory: EditorFactory | undefined): boolean {
|
|
|
49
53
|
return Boolean((factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_FACTORY]);
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
function getZentuiEditorBaseFactory(factory: EditorFactory | undefined): EditorFactory | undefined {
|
|
57
|
+
return (factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_BASE_FACTORY];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isTuiContext(ctx: ExtensionContext): boolean {
|
|
61
|
+
const mode = (ctx as ExtensionContext & { mode?: string }).mode;
|
|
62
|
+
return ctx.hasUI && (mode === undefined || mode === "tui");
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
export default function (pi: ExtensionAPI) {
|
|
53
66
|
const state: FooterState = createInitialState(emptyGitStatus());
|
|
54
67
|
|
|
@@ -61,6 +74,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
61
74
|
let footerInstalled = false;
|
|
62
75
|
let editorInstalled = false;
|
|
63
76
|
let editorInstallMode: EditorInstallMode = "none";
|
|
77
|
+
let installedEditorFactory: EditorFactory | undefined;
|
|
64
78
|
let wrappedEditorFactory: EditorFactory | undefined;
|
|
65
79
|
let prototypePatchesInstalled = false;
|
|
66
80
|
|
|
@@ -148,24 +162,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
148
162
|
getThinkingLevel,
|
|
149
163
|
)) as ZentuiEditorFactory;
|
|
150
164
|
factory[ZENTUI_EDITOR_FACTORY] = true;
|
|
165
|
+
factory[ZENTUI_EDITOR_BASE_FACTORY] = baseFactory;
|
|
151
166
|
return factory;
|
|
152
167
|
};
|
|
153
168
|
|
|
154
169
|
const installEditor = (ctx: ExtensionContext): boolean => {
|
|
155
170
|
const currentFactory = ctx.ui.getEditorComponent();
|
|
156
|
-
if (currentFactory &&
|
|
171
|
+
if (currentFactory && currentFactory === installedEditorFactory) {
|
|
157
172
|
editorInstalled = true;
|
|
158
173
|
return true;
|
|
159
174
|
}
|
|
160
175
|
|
|
161
176
|
installPrototypePatches();
|
|
162
|
-
|
|
177
|
+
const currentZentuiBaseFactory = getZentuiEditorBaseFactory(currentFactory);
|
|
178
|
+
if (currentFactory && isZentuiEditorFactory(currentFactory)) {
|
|
179
|
+
wrappedEditorFactory = currentZentuiBaseFactory;
|
|
180
|
+
const nextFactory = currentZentuiBaseFactory
|
|
181
|
+
? makeWrappedEditorFactory(ctx, currentZentuiBaseFactory)
|
|
182
|
+
: makeEditorFactory(ctx);
|
|
183
|
+
ctx.ui.setEditorComponent(nextFactory);
|
|
184
|
+
installedEditorFactory = nextFactory;
|
|
185
|
+
editorInstallMode = currentZentuiBaseFactory ? "wrapper" : "standalone";
|
|
186
|
+
} else if (currentFactory) {
|
|
163
187
|
wrappedEditorFactory = currentFactory;
|
|
164
|
-
|
|
188
|
+
const nextFactory = makeWrappedEditorFactory(ctx, currentFactory);
|
|
189
|
+
ctx.ui.setEditorComponent(nextFactory);
|
|
190
|
+
installedEditorFactory = nextFactory;
|
|
165
191
|
editorInstallMode = "wrapper";
|
|
166
192
|
} else {
|
|
167
193
|
wrappedEditorFactory = undefined;
|
|
168
|
-
|
|
194
|
+
const nextFactory = makeEditorFactory(ctx);
|
|
195
|
+
ctx.ui.setEditorComponent(nextFactory);
|
|
196
|
+
installedEditorFactory = nextFactory;
|
|
169
197
|
editorInstallMode = "standalone";
|
|
170
198
|
}
|
|
171
199
|
editorInstalled = true;
|
|
@@ -181,6 +209,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
181
209
|
editorInstallMode === "wrapper" && wrappedEditorFactory ? wrappedEditorFactory : undefined,
|
|
182
210
|
);
|
|
183
211
|
wrappedEditorFactory = undefined;
|
|
212
|
+
installedEditorFactory = undefined;
|
|
184
213
|
editorInstallMode = "none";
|
|
185
214
|
editorInstalled = false;
|
|
186
215
|
return true;
|
|
@@ -216,7 +245,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
216
245
|
|
|
217
246
|
const applyConfiguredUi = (ctx: ExtensionContext): ApplyUiResult => {
|
|
218
247
|
const result: ApplyUiResult = { editorBlocked: false };
|
|
219
|
-
if (!ctx
|
|
248
|
+
if (!isTuiContext(ctx)) return result;
|
|
220
249
|
activeTheme = ctx.ui.theme;
|
|
221
250
|
if (currentConfig.features.editor) {
|
|
222
251
|
const currentFactory = ctx.ui.getEditorComponent();
|
|
@@ -235,11 +264,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
235
264
|
};
|
|
236
265
|
|
|
237
266
|
const installUi = (ctx: ExtensionContext) => {
|
|
238
|
-
if (!ctx
|
|
267
|
+
if (!isTuiContext(ctx)) return;
|
|
239
268
|
activeTheme = ctx.ui.theme;
|
|
240
269
|
uninstallPrototypePatches();
|
|
241
270
|
footerInstalled = false;
|
|
242
271
|
editorInstalled = false;
|
|
272
|
+
installedEditorFactory = undefined;
|
|
243
273
|
ensureConfigExists();
|
|
244
274
|
currentConfig = loadConfig();
|
|
245
275
|
syncFooterState(ctx);
|
|
@@ -250,9 +280,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
250
280
|
|
|
251
281
|
const scheduleEditorReconciliation = (ctx: ExtensionContext) => {
|
|
252
282
|
setTimeout(() => {
|
|
253
|
-
if (!ctx
|
|
283
|
+
if (!isTuiContext(ctx) || !currentConfig.features.editor) return;
|
|
254
284
|
const currentFactory = ctx.ui.getEditorComponent();
|
|
255
|
-
if (currentFactory &&
|
|
285
|
+
if (currentFactory && currentFactory !== installedEditorFactory) {
|
|
256
286
|
applyConfiguredUi(ctx);
|
|
257
287
|
refresh();
|
|
258
288
|
}
|
|
@@ -264,18 +294,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
264
294
|
stopProjectRefresh();
|
|
265
295
|
requestFooterRender = undefined;
|
|
266
296
|
getActiveExtensionStatuses = () => new Map();
|
|
267
|
-
if (ctx
|
|
297
|
+
if (ctx && isTuiContext(ctx)) {
|
|
268
298
|
ctx.ui.setFooter(undefined);
|
|
269
299
|
const currentFactory = ctx.ui.getEditorComponent();
|
|
270
300
|
if (!currentFactory || isZentuiEditorFactory(currentFactory)) {
|
|
271
301
|
ctx.ui.setEditorComponent(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
302
|
+
getZentuiEditorBaseFactory(currentFactory) ??
|
|
303
|
+
(editorInstallMode === "wrapper" && wrappedEditorFactory
|
|
304
|
+
? wrappedEditorFactory
|
|
305
|
+
: undefined),
|
|
275
306
|
);
|
|
276
307
|
}
|
|
277
308
|
}
|
|
278
309
|
wrappedEditorFactory = undefined;
|
|
310
|
+
installedEditorFactory = undefined;
|
|
279
311
|
editorInstallMode = "none";
|
|
280
312
|
footerInstalled = false;
|
|
281
313
|
editorInstalled = false;
|
|
@@ -315,6 +347,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
315
347
|
setExtensionStatusPlacement(key: string, placement: ExtensionStatusPlacement) {
|
|
316
348
|
currentConfig = saveExtensionStatusPlacement(key, placement);
|
|
317
349
|
},
|
|
350
|
+
setExtensionStatusColorMode(key: string, colorMode: ExtensionStatusColorMode) {
|
|
351
|
+
currentConfig = saveExtensionStatusColorMode(key, colorMode);
|
|
352
|
+
},
|
|
318
353
|
requestRender() {
|
|
319
354
|
refresh();
|
|
320
355
|
},
|
|
@@ -10,10 +10,13 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
type ColorSource,
|
|
12
12
|
type ColorSourcesConfig,
|
|
13
|
+
type ExtensionStatusColorMode,
|
|
13
14
|
type ExtensionStatusPlacement,
|
|
14
15
|
type PolishedTuiConfig,
|
|
15
16
|
type UiFeaturesConfig,
|
|
17
|
+
getExtensionStatusColorMode,
|
|
16
18
|
getExtensionStatusPlacement,
|
|
19
|
+
isExtensionStatusColorMode,
|
|
17
20
|
isExtensionStatusPlacement,
|
|
18
21
|
} from "./config";
|
|
19
22
|
import { sanitizeExtensionStatusText } from "./extension-status";
|
|
@@ -26,6 +29,7 @@ const extensionStatusPlacementValues: ExtensionStatusPlacement[] = [
|
|
|
26
29
|
"middle",
|
|
27
30
|
"right",
|
|
28
31
|
];
|
|
32
|
+
const extensionStatusColorModeValues: ExtensionStatusColorMode[] = ["zentui", "original"];
|
|
29
33
|
type FeatureState = "enabled" | "disabled";
|
|
30
34
|
|
|
31
35
|
const featureStateValues: FeatureState[] = ["enabled", "disabled"];
|
|
@@ -44,6 +48,7 @@ type SettingsCommandDeps = {
|
|
|
44
48
|
) => { applied: boolean; reason?: string };
|
|
45
49
|
getActiveExtensionStatuses: () => ReadonlyMap<string, string>;
|
|
46
50
|
setExtensionStatusPlacement: (key: string, placement: ExtensionStatusPlacement) => void;
|
|
51
|
+
setExtensionStatusColorMode: (key: string, colorMode: ExtensionStatusColorMode) => void;
|
|
47
52
|
requestRender: () => void;
|
|
48
53
|
settingsListTheme?: SettingsListTheme;
|
|
49
54
|
};
|
|
@@ -93,6 +98,7 @@ const sectionLabels: Record<SettingsSection, string> = {
|
|
|
93
98
|
};
|
|
94
99
|
|
|
95
100
|
const thirdPartyStatusSettingPrefix = "thirdPartyStatus:";
|
|
101
|
+
type ThirdPartyStatusSettingKind = "placement" | "colorMode";
|
|
96
102
|
|
|
97
103
|
function isColorSource(value: string): value is ColorSource {
|
|
98
104
|
return value === "theme" || value === "terminal";
|
|
@@ -180,6 +186,24 @@ function argumentCompletions(prefix: string): AutocompleteItem[] | null {
|
|
|
180
186
|
return matches.length > 0 ? matches : null;
|
|
181
187
|
}
|
|
182
188
|
|
|
189
|
+
function thirdPartyStatusSettingId(key: string, kind: ThirdPartyStatusSettingKind): string {
|
|
190
|
+
return `${thirdPartyStatusSettingPrefix}${kind}:${key}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function thirdPartyStatusSettingFromId(
|
|
194
|
+
id: string,
|
|
195
|
+
): { kind: ThirdPartyStatusSettingKind; key: string } | undefined {
|
|
196
|
+
if (!id.startsWith(thirdPartyStatusSettingPrefix)) return undefined;
|
|
197
|
+
const rest = id.slice(thirdPartyStatusSettingPrefix.length);
|
|
198
|
+
const separatorIndex = rest.indexOf(":");
|
|
199
|
+
if (separatorIndex < 0) return undefined;
|
|
200
|
+
|
|
201
|
+
const kind = rest.slice(0, separatorIndex);
|
|
202
|
+
if (kind !== "placement" && kind !== "colorMode") return undefined;
|
|
203
|
+
|
|
204
|
+
return { kind, key: rest.slice(separatorIndex + 1) };
|
|
205
|
+
}
|
|
206
|
+
|
|
183
207
|
function buildItems(
|
|
184
208
|
section: SettingsSection,
|
|
185
209
|
config: PolishedTuiConfig,
|
|
@@ -220,24 +244,28 @@ function buildItems(
|
|
|
220
244
|
];
|
|
221
245
|
}
|
|
222
246
|
|
|
223
|
-
return statuses.
|
|
247
|
+
return statuses.flatMap(([key, value]) => {
|
|
224
248
|
const sanitizedText = sanitizeExtensionStatusText(value);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
249
|
+
const description = sanitizedText ? `Current status: ${sanitizedText}` : undefined;
|
|
250
|
+
return [
|
|
251
|
+
{
|
|
252
|
+
id: thirdPartyStatusSettingId(key, "placement"),
|
|
253
|
+
label: `${key} placement`,
|
|
254
|
+
description,
|
|
255
|
+
currentValue: getExtensionStatusPlacement(config, key),
|
|
256
|
+
values: extensionStatusPlacementValues,
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: thirdPartyStatusSettingId(key, "colorMode"),
|
|
260
|
+
label: `${key} color`,
|
|
261
|
+
description,
|
|
262
|
+
currentValue: getExtensionStatusColorMode(config, key),
|
|
263
|
+
values: extensionStatusColorModeValues,
|
|
264
|
+
},
|
|
265
|
+
];
|
|
232
266
|
});
|
|
233
267
|
}
|
|
234
268
|
|
|
235
|
-
function thirdPartyStatusKeyFromSettingId(id: string): string | undefined {
|
|
236
|
-
return id.startsWith(thirdPartyStatusSettingPrefix)
|
|
237
|
-
? id.slice(thirdPartyStatusSettingPrefix.length)
|
|
238
|
-
: undefined;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
269
|
function nextSection(section: SettingsSection): SettingsSection {
|
|
242
270
|
const currentIndex = settingsSections.indexOf(section);
|
|
243
271
|
return settingsSections[(currentIndex + 1) % settingsSections.length] ?? "coloring";
|
|
@@ -305,7 +333,8 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
|
|
|
305
333
|
return;
|
|
306
334
|
}
|
|
307
335
|
|
|
308
|
-
|
|
336
|
+
const mode = (ctx as typeof ctx & { mode?: string }).mode;
|
|
337
|
+
if (!ctx.hasUI || (mode !== undefined && mode !== "tui")) return;
|
|
309
338
|
|
|
310
339
|
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
311
340
|
const settingsListTheme = deps.settingsListTheme ?? getSettingsListTheme();
|
|
@@ -355,12 +384,33 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
|
|
|
355
384
|
return;
|
|
356
385
|
}
|
|
357
386
|
|
|
358
|
-
const
|
|
359
|
-
if (
|
|
360
|
-
|
|
387
|
+
const thirdPartyStatusSetting = thirdPartyStatusSettingFromId(id);
|
|
388
|
+
if (
|
|
389
|
+
thirdPartyStatusSetting?.kind === "placement" &&
|
|
390
|
+
isExtensionStatusPlacement(newValue)
|
|
391
|
+
) {
|
|
392
|
+
deps.setExtensionStatusPlacement(thirdPartyStatusSetting.key, newValue);
|
|
393
|
+
settingsList.updateValue(id, newValue);
|
|
394
|
+
deps.requestRender();
|
|
395
|
+
ctx.ui.notify(
|
|
396
|
+
`Third-party status ${thirdPartyStatusSetting.key} placement: ${newValue}`,
|
|
397
|
+
"info",
|
|
398
|
+
);
|
|
399
|
+
tui.requestRender();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (
|
|
404
|
+
thirdPartyStatusSetting?.kind === "colorMode" &&
|
|
405
|
+
isExtensionStatusColorMode(newValue)
|
|
406
|
+
) {
|
|
407
|
+
deps.setExtensionStatusColorMode(thirdPartyStatusSetting.key, newValue);
|
|
361
408
|
settingsList.updateValue(id, newValue);
|
|
362
409
|
deps.requestRender();
|
|
363
|
-
ctx.ui.notify(
|
|
410
|
+
ctx.ui.notify(
|
|
411
|
+
`Third-party status ${thirdPartyStatusSetting.key} color: ${newValue}`,
|
|
412
|
+
"info",
|
|
413
|
+
);
|
|
364
414
|
tui.requestRender();
|
|
365
415
|
}
|
|
366
416
|
} catch (error) {
|
package/extensions/zentui/ui.ts
CHANGED
|
@@ -132,6 +132,60 @@ function composeMetadataLine(left: string, right: string | undefined, width: num
|
|
|
132
132
|
return `${leftText}${gap}${right}`;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
function plainRenderedText(line: string): string {
|
|
136
|
+
return line
|
|
137
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
138
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
139
|
+
.replace(/\[[/?][^\]]+\]/g, "");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isHorizontalBorder(line: string): boolean {
|
|
143
|
+
const plain = plainRenderedText(line).trim();
|
|
144
|
+
return plain.length > 0 && /^─+$/.test(plain);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isRenderedModelMetaLine(line: string, modelMeta: EditorMeta): boolean {
|
|
148
|
+
const plain = plainRenderedText(line);
|
|
149
|
+
return plain.includes(modelMeta.modelLabel) && plain.includes(modelMeta.providerLabel);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function hasRenderedModelMetaLine(lines: string[], modelMeta: EditorMeta): boolean {
|
|
153
|
+
return lines.some((line) => isRenderedModelMetaLine(line, modelMeta));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isAlreadyPolishedFrame(lines: string[], modelMeta: EditorMeta): boolean {
|
|
157
|
+
return (
|
|
158
|
+
lines.length >= 3 &&
|
|
159
|
+
isHorizontalBorder(lines[0] ?? "") &&
|
|
160
|
+
isHorizontalBorder(lines.at(-1) ?? "") &&
|
|
161
|
+
hasRenderedModelMetaLine(lines.slice(1, -1), modelMeta)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function removeRenderedModelMetaLines(lines: string[], modelMeta: EditorMeta): string[] {
|
|
166
|
+
const result: string[] = [];
|
|
167
|
+
for (let index = 0; index < lines.length; index++) {
|
|
168
|
+
const line = lines[index] ?? "";
|
|
169
|
+
if (isRenderedModelMetaLine(line, modelMeta)) continue;
|
|
170
|
+
|
|
171
|
+
const plain = plainRenderedText(line).trim();
|
|
172
|
+
const previousWasMeta = index > 0 && isRenderedModelMetaLine(lines[index - 1] ?? "", modelMeta);
|
|
173
|
+
const nextIsMeta =
|
|
174
|
+
index < lines.length - 1 && isRenderedModelMetaLine(lines[index + 1] ?? "", modelMeta);
|
|
175
|
+
if (!plain && (previousWasMeta || nextIsMeta)) continue;
|
|
176
|
+
|
|
177
|
+
result.push(line);
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function removeStalePolishedLeadingSpacer(lines: string[], shouldRemove: boolean): string[] {
|
|
183
|
+
if (!shouldRemove || lines.length === 0) return lines;
|
|
184
|
+
const firstLine = lines[0] ?? "";
|
|
185
|
+
if (plainRenderedText(firstLine).trim()) return lines;
|
|
186
|
+
return lines.slice(1);
|
|
187
|
+
}
|
|
188
|
+
|
|
135
189
|
function vimModeColor(mode: string): string {
|
|
136
190
|
switch (mode.toLowerCase()) {
|
|
137
191
|
case "insert":
|
|
@@ -195,10 +249,13 @@ function renderPolishedFrame({
|
|
|
195
249
|
autocompleteCount > 0 && autocompleteCount < baseRendered.length
|
|
196
250
|
? baseRendered.slice(-autocompleteCount)
|
|
197
251
|
: [];
|
|
198
|
-
|
|
199
252
|
if (editorFrame.length < 2) return clampRenderedLines(baseRendered, width);
|
|
200
253
|
|
|
201
|
-
const
|
|
254
|
+
const stalePolishedFrame = isAlreadyPolishedFrame(editorFrame, modelMeta);
|
|
255
|
+
const editorLines = removeStalePolishedLeadingSpacer(
|
|
256
|
+
removeRenderedModelMetaLines(editorFrame.slice(1, -1), modelMeta),
|
|
257
|
+
stalePolishedFrame,
|
|
258
|
+
);
|
|
202
259
|
const model = renderStyleForSourceOrFallback(
|
|
203
260
|
uiTheme,
|
|
204
261
|
colorSource,
|
|
@@ -17,13 +17,18 @@ const OSC133_ZONE_END = "\x1b]133;B\x07";
|
|
|
17
17
|
const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
|
|
18
18
|
|
|
19
19
|
type RenderFn = (width: number) => string[];
|
|
20
|
+
type InvalidateFn = () => void;
|
|
20
21
|
|
|
21
22
|
type PatchableUserMessagePrototype = {
|
|
22
23
|
render: RenderFn;
|
|
24
|
+
invalidate: InvalidateFn;
|
|
23
25
|
children?: unknown[];
|
|
24
26
|
__zentuiUserMessageOriginalRender?: RenderFn;
|
|
27
|
+
__zentuiUserMessageOriginalInvalidate?: InvalidateFn;
|
|
25
28
|
__zentuiUserMessagePatched?: boolean;
|
|
29
|
+
__zentuiUserMessageInvalidatePatched?: boolean;
|
|
26
30
|
__zentuiUserMessageWrapper?: RenderFn;
|
|
31
|
+
__zentuiUserMessageInvalidateWrapper?: InvalidateFn;
|
|
27
32
|
__zentuiUserMessageActive?: boolean;
|
|
28
33
|
__zentuiUserMessageGetTheme?: () => Theme | undefined;
|
|
29
34
|
__zentuiUserMessageGetConfig?: () => PolishedTuiConfig;
|
|
@@ -31,22 +36,32 @@ type PatchableUserMessagePrototype = {
|
|
|
31
36
|
|
|
32
37
|
type Cleanup = () => void;
|
|
33
38
|
|
|
34
|
-
type
|
|
35
|
-
|
|
39
|
+
type UserMessageRenderCache = {
|
|
40
|
+
hasMarkdownText: boolean;
|
|
41
|
+
text?: string;
|
|
42
|
+
width?: number;
|
|
43
|
+
theme?: Theme;
|
|
44
|
+
configKey?: string;
|
|
45
|
+
renderedLines?: string[];
|
|
36
46
|
};
|
|
37
47
|
|
|
48
|
+
const userMessageRenderCache = new WeakMap<object, UserMessageRenderCache>();
|
|
49
|
+
|
|
50
|
+
function isObject(value: unknown): value is object {
|
|
51
|
+
return (typeof value === "object" && value !== null) || typeof value === "function";
|
|
52
|
+
}
|
|
53
|
+
|
|
38
54
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
39
55
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
function findMarkdownText(value: unknown): string | undefined {
|
|
43
|
-
if (isRecord(value) && typeof (value as MarkdownLike).text === "string") {
|
|
44
|
-
return (value as { text: string }).text;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
59
|
if (!isRecord(value)) return undefined;
|
|
60
|
+
if (typeof value.text === "string") return value.text;
|
|
61
|
+
|
|
62
|
+
const children = value.children;
|
|
63
|
+
if (!Array.isArray(children)) return undefined;
|
|
48
64
|
|
|
49
|
-
const children = Array.isArray(value.children) ? value.children : [];
|
|
50
65
|
for (const child of children) {
|
|
51
66
|
const text = findMarkdownText(child);
|
|
52
67
|
if (text !== undefined) return text;
|
|
@@ -55,6 +70,26 @@ function findMarkdownText(value: unknown): string | undefined {
|
|
|
55
70
|
return undefined;
|
|
56
71
|
}
|
|
57
72
|
|
|
73
|
+
function getCachedMarkdownText(instance: object): string | undefined {
|
|
74
|
+
const cached = userMessageRenderCache.get(instance);
|
|
75
|
+
if (cached?.hasMarkdownText) return cached.text;
|
|
76
|
+
|
|
77
|
+
const text = findMarkdownText(instance);
|
|
78
|
+
if (text !== undefined) {
|
|
79
|
+
userMessageRenderCache.set(instance, { ...cached, hasMarkdownText: true, text });
|
|
80
|
+
}
|
|
81
|
+
return text;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getUserMessageConfigKey(config: PolishedTuiConfig): string {
|
|
85
|
+
return [
|
|
86
|
+
config.features.copyFriendly ? "copy" : "chrome",
|
|
87
|
+
config.colorSources.userMessages,
|
|
88
|
+
config.colors.editorAccent ?? "",
|
|
89
|
+
config.colors.editorBorder ?? "",
|
|
90
|
+
].join("\0");
|
|
91
|
+
}
|
|
92
|
+
|
|
58
93
|
function themeFg(theme: Theme | undefined, color: ThemeColor, text: string): string {
|
|
59
94
|
if (!theme) return text;
|
|
60
95
|
try {
|
|
@@ -126,10 +161,24 @@ function renderZentuiUserMessage(
|
|
|
126
161
|
theme: Theme | undefined,
|
|
127
162
|
config: PolishedTuiConfig,
|
|
128
163
|
): string[] | undefined {
|
|
129
|
-
|
|
164
|
+
if (!isRecord(instance)) return undefined;
|
|
165
|
+
|
|
166
|
+
const text = getCachedMarkdownText(instance);
|
|
130
167
|
if (text === undefined) return undefined;
|
|
131
168
|
if (width <= 0) return [""];
|
|
132
169
|
|
|
170
|
+
const configKey = getUserMessageConfigKey(config);
|
|
171
|
+
const cached = userMessageRenderCache.get(instance);
|
|
172
|
+
if (
|
|
173
|
+
cached?.hasMarkdownText &&
|
|
174
|
+
cached.width === width &&
|
|
175
|
+
cached.theme === theme &&
|
|
176
|
+
cached.configKey === configKey &&
|
|
177
|
+
cached.renderedLines
|
|
178
|
+
) {
|
|
179
|
+
return cached.renderedLines;
|
|
180
|
+
}
|
|
181
|
+
|
|
133
182
|
const railWidth = visibleWidth(renderPromptBoxRail(theme, config));
|
|
134
183
|
const contentWidth = Math.max(1, width - railWidth);
|
|
135
184
|
const renderer = new Markdown(text, 0, 0, makeMarkdownTheme(theme), {
|
|
@@ -146,14 +195,31 @@ function renderZentuiUserMessage(
|
|
|
146
195
|
"─".repeat(width),
|
|
147
196
|
)
|
|
148
197
|
: "─".repeat(width);
|
|
149
|
-
|
|
150
|
-
return [
|
|
198
|
+
const lines = [
|
|
151
199
|
truncateToWidth(border, width, ""),
|
|
152
200
|
renderPromptBoxLine("", width, theme, config),
|
|
153
201
|
...contentLines.map((line) => renderPromptBoxLine(line, width, theme, config)),
|
|
154
202
|
renderPromptBoxLine("", width, theme, config),
|
|
155
203
|
truncateToWidth(border, width, ""),
|
|
156
204
|
];
|
|
205
|
+
|
|
206
|
+
userMessageRenderCache.set(instance, {
|
|
207
|
+
hasMarkdownText: true,
|
|
208
|
+
text,
|
|
209
|
+
width,
|
|
210
|
+
theme,
|
|
211
|
+
configKey,
|
|
212
|
+
renderedLines: lines,
|
|
213
|
+
});
|
|
214
|
+
return lines;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function withPromptZoneMarkers(lines: string[]): string[] {
|
|
218
|
+
const markedLines = [...lines];
|
|
219
|
+
markedLines[0] = OSC133_ZONE_START + markedLines[0];
|
|
220
|
+
markedLines[markedLines.length - 1] =
|
|
221
|
+
OSC133_ZONE_END + OSC133_ZONE_FINAL + markedLines[markedLines.length - 1];
|
|
222
|
+
return markedLines;
|
|
157
223
|
}
|
|
158
224
|
|
|
159
225
|
export function installUserMessageStyle(
|
|
@@ -165,6 +231,23 @@ export function installUserMessageStyle(
|
|
|
165
231
|
prototype.__zentuiUserMessageGetConfig = getConfig;
|
|
166
232
|
prototype.__zentuiUserMessageActive = true;
|
|
167
233
|
|
|
234
|
+
if (
|
|
235
|
+
!(
|
|
236
|
+
prototype.__zentuiUserMessageInvalidatePatched &&
|
|
237
|
+
prototype.invalidate === prototype.__zentuiUserMessageInvalidateWrapper
|
|
238
|
+
)
|
|
239
|
+
) {
|
|
240
|
+
prototype.__zentuiUserMessageOriginalInvalidate = prototype.invalidate;
|
|
241
|
+
const invalidateWrapper = function invalidateWithZentuiUserMessage(this: unknown): void {
|
|
242
|
+
if (isObject(this)) userMessageRenderCache.delete(this);
|
|
243
|
+
const originalInvalidate = prototype.__zentuiUserMessageOriginalInvalidate;
|
|
244
|
+
originalInvalidate?.call(this);
|
|
245
|
+
};
|
|
246
|
+
prototype.__zentuiUserMessageInvalidateWrapper = invalidateWrapper;
|
|
247
|
+
prototype.invalidate = invalidateWrapper;
|
|
248
|
+
prototype.__zentuiUserMessageInvalidatePatched = true;
|
|
249
|
+
}
|
|
250
|
+
|
|
168
251
|
if (
|
|
169
252
|
prototype.__zentuiUserMessagePatched &&
|
|
170
253
|
prototype.render === prototype.__zentuiUserMessageWrapper
|
|
@@ -192,9 +275,7 @@ export function installUserMessageStyle(
|
|
|
192
275
|
if (!lines) return original.call(this, width);
|
|
193
276
|
if (lines.length === 0) return lines;
|
|
194
277
|
|
|
195
|
-
|
|
196
|
-
lines[lines.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + lines[lines.length - 1];
|
|
197
|
-
return lines;
|
|
278
|
+
return withPromptZoneMarkers(lines);
|
|
198
279
|
};
|
|
199
280
|
prototype.__zentuiUserMessageWrapper = wrapper;
|
|
200
281
|
prototype.render = wrapper;
|