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 CHANGED
@@ -200,7 +200,8 @@ Default config values — copy this and change any value you want:
200
200
  },
201
201
  "extensionStatuses": {
202
202
  "defaultPlacement": "right",
203
- "placements": {}
203
+ "placements": {},
204
+ "colorModes": {}
204
205
  }
205
206
  }
206
207
  ```
@@ -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 { ExtensionStatusPlacement, PolishedTuiConfig } from "./config";
3
- import { getExtensionStatusPlacement } from "./config";
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
- export function sanitizeExtensionStatusText(value: string): string {
22
- return stripVTControlCharacters(value)
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 text = sanitizeExtensionStatusText(value);
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 = (text: string) =>
222
- renderStyleForSource(theme, colorSource, config.colors.extensionStatus, text);
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((segment) => renderExtensionStatus(segment.text)),
227
- extensionStatuses.middle.map((segment) => renderExtensionStatus(segment.text)),
228
- extensionStatuses.right.map((segment) => renderExtensionStatus(segment.text)),
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 && isZentuiEditorFactory(currentFactory)) {
171
+ if (currentFactory && currentFactory === installedEditorFactory) {
157
172
  editorInstalled = true;
158
173
  return true;
159
174
  }
160
175
 
161
176
  installPrototypePatches();
162
- if (currentFactory) {
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
- ctx.ui.setEditorComponent(makeWrappedEditorFactory(ctx, currentFactory));
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
- ctx.ui.setEditorComponent(makeEditorFactory(ctx));
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.hasUI) return result;
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.hasUI) return;
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.hasUI || !currentConfig.features.editor) return;
283
+ if (!isTuiContext(ctx) || !currentConfig.features.editor) return;
254
284
  const currentFactory = ctx.ui.getEditorComponent();
255
- if (currentFactory && !isZentuiEditorFactory(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?.hasUI) {
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
- editorInstallMode === "wrapper" && wrappedEditorFactory
273
- ? wrappedEditorFactory
274
- : undefined,
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.map(([key, value]) => {
247
+ return statuses.flatMap(([key, value]) => {
224
248
  const sanitizedText = sanitizeExtensionStatusText(value);
225
- return {
226
- id: `${thirdPartyStatusSettingPrefix}${key}`,
227
- label: key,
228
- description: sanitizedText ? `Current status: ${sanitizedText}` : undefined,
229
- currentValue: getExtensionStatusPlacement(config, key),
230
- values: extensionStatusPlacementValues,
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
- if (!ctx.hasUI) return;
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 thirdPartyStatusKey = thirdPartyStatusKeyFromSettingId(id);
359
- if (thirdPartyStatusKey && isExtensionStatusPlacement(newValue)) {
360
- deps.setExtensionStatusPlacement(thirdPartyStatusKey, newValue);
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(`Third-party status ${thirdPartyStatusKey}: ${newValue}`, "info");
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) {
@@ -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 editorLines = editorFrame.slice(1, -1);
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 MarkdownLike = {
35
- text?: unknown;
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
- const text = findMarkdownText(instance);
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
- lines[0] = OSC133_ZONE_START + lines[0];
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-zentui",
3
- "version": "0.2.2",
3
+ "version": "0.2.6",
4
4
  "description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",