pi-one-ui 0.1.1

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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/extensions/app/config/renderer.ts +452 -0
  4. package/extensions/app/config/shell.ts +2484 -0
  5. package/extensions/app/ownership.ts +18 -0
  6. package/extensions/app/panel.ts +533 -0
  7. package/extensions/app/presets.ts +36 -0
  8. package/extensions/features/agent-summary/core.ts +194 -0
  9. package/extensions/features/agent-summary/index.ts +37 -0
  10. package/extensions/features/compact-thinking.ts +1262 -0
  11. package/extensions/features/context.ts +857 -0
  12. package/extensions/features/reference/index.ts +643 -0
  13. package/extensions/features/reference/session.ts +278 -0
  14. package/extensions/features/reference/subagent.ts +218 -0
  15. package/extensions/features/shell/aliases.ts +28 -0
  16. package/extensions/features/shell/flush-docked-bash.ts +39 -0
  17. package/extensions/features/shell/startup-header.ts +357 -0
  18. package/extensions/features/shell/working-message.ts +226 -0
  19. package/extensions/index.ts +52 -0
  20. package/extensions/shell/accent-rail-editor.ts +112 -0
  21. package/extensions/shell/accent-rail-layout-patch.ts +530 -0
  22. package/extensions/shell/completion-menu.ts +186 -0
  23. package/extensions/shell/editor-metadata-format.ts +287 -0
  24. package/extensions/shell/editor-transfer.ts +73 -0
  25. package/extensions/shell/extension-status.ts +102 -0
  26. package/extensions/shell/footer-format.ts +388 -0
  27. package/extensions/shell/footer-layout.ts +144 -0
  28. package/extensions/shell/footer.ts +1035 -0
  29. package/extensions/shell/format.ts +577 -0
  30. package/extensions/shell/git.ts +434 -0
  31. package/extensions/shell/icons.ts +261 -0
  32. package/extensions/shell/index.ts +1659 -0
  33. package/extensions/shell/interaction-summary.ts +1194 -0
  34. package/extensions/shell/live-context.ts +82 -0
  35. package/extensions/shell/minimalist-editor.ts +548 -0
  36. package/extensions/shell/package-version.ts +675 -0
  37. package/extensions/shell/project-refresh.ts +130 -0
  38. package/extensions/shell/project-state.ts +60 -0
  39. package/extensions/shell/prototype-patch-registry.ts +183 -0
  40. package/extensions/shell/runtime.ts +915 -0
  41. package/extensions/shell/selector-border.ts +109 -0
  42. package/extensions/shell/session-lifecycle.ts +60 -0
  43. package/extensions/shell/settings-command.ts +2144 -0
  44. package/extensions/shell/settings-previews.ts +137 -0
  45. package/extensions/shell/state.ts +83 -0
  46. package/extensions/shell/style.ts +480 -0
  47. package/extensions/shell/telemetry.ts +90 -0
  48. package/extensions/shell/ui.ts +1326 -0
  49. package/extensions/shell/user-message-osc.ts +279 -0
  50. package/extensions/shell/user-message-styles.ts +266 -0
  51. package/extensions/shell/user-message.ts +226 -0
  52. package/extensions/shell/working-line-messages.ts +24 -0
  53. package/extensions/shell/working-line-spinners.ts +54 -0
  54. package/extensions/shell/working-line.ts +1603 -0
  55. package/extensions/tools/ansi-color.ts +44 -0
  56. package/extensions/tools/ansi-text.ts +52 -0
  57. package/extensions/tools/component-tree.ts +37 -0
  58. package/extensions/tools/format.ts +28 -0
  59. package/extensions/tools/fullscreen-detect.ts +11 -0
  60. package/extensions/tools/patch-keys.ts +158 -0
  61. package/extensions/tools/sgr-mouse.ts +59 -0
  62. package/extensions/tools/tool-loading-icon.ts +20 -0
  63. package/extensions/tools/tool-result-sanitize.ts +27 -0
  64. package/extensions/transcript/index.ts +65 -0
  65. package/extensions/transcript/renderer/compact-mode.ts +1443 -0
  66. package/extensions/transcript/renderer/default-mode.ts +735 -0
  67. package/extensions/transcript/renderer/index.ts +286 -0
  68. package/extensions/transcript/renderer/markdown-enhance.ts +325 -0
  69. package/extensions/transcript/renderer/mouse/hover.ts +175 -0
  70. package/extensions/transcript/renderer/mouse/interaction.ts +1016 -0
  71. package/extensions/transcript/renderer/mouse/layout.ts +207 -0
  72. package/extensions/transcript/renderer/mouse/packets.ts +96 -0
  73. package/extensions/transcript/renderer/mouse/scroll.ts +211 -0
  74. package/extensions/transcript/renderer/tool/diff/ATTRIBUTION.md +28 -0
  75. package/extensions/transcript/renderer/tool/diff/ansi-utils.ts +128 -0
  76. package/extensions/transcript/renderer/tool/diff/diff-component.ts +112 -0
  77. package/extensions/transcript/renderer/tool/diff/diff-edit-render.ts +255 -0
  78. package/extensions/transcript/renderer/tool/diff/diff-header.ts +184 -0
  79. package/extensions/transcript/renderer/tool/diff/diff-highlight.ts +131 -0
  80. package/extensions/transcript/renderer/tool/diff/diff-inline.ts +350 -0
  81. package/extensions/transcript/renderer/tool/diff/diff-layout.ts +870 -0
  82. package/extensions/transcript/renderer/tool/diff/diff-limits.ts +189 -0
  83. package/extensions/transcript/renderer/tool/diff/diff-palette.ts +436 -0
  84. package/extensions/transcript/renderer/tool/diff/diff-parse.ts +536 -0
  85. package/extensions/transcript/renderer/tool/diff/diff-presentation.ts +79 -0
  86. package/extensions/transcript/renderer/tool/diff/diff-renderer.ts +29 -0
  87. package/extensions/transcript/renderer/tool/diff/diff-text.ts +57 -0
  88. package/extensions/transcript/renderer/tool/diff/diff-write-render.ts +629 -0
  89. package/extensions/transcript/renderer/tool/diff/index.ts +147 -0
  90. package/extensions/transcript/renderer/tool/diff/line-width-safety.ts +100 -0
  91. package/extensions/transcript/renderer/tool/diff/shiki-highlight.ts +104 -0
  92. package/extensions/transcript/renderer/tool/diff/write-execution.ts +124 -0
  93. package/extensions/transcript/renderer/tool/grouping.ts +619 -0
  94. package/extensions/transcript/renderer/tool/message-display.ts +183 -0
  95. package/extensions/transcript/renderer/tool/names.ts +195 -0
  96. package/extensions/transcript/renderer/tool/result.ts +842 -0
  97. package/extensions/transcript/renderer/tool/show-more-hint.ts +42 -0
  98. package/extensions/transcript/renderer/transcript-refresh.ts +79 -0
  99. package/package.json +58 -0
  100. package/themes/cc-dark.json +91 -0
  101. package/themes/cc-light.json +91 -0
@@ -0,0 +1,1326 @@
1
+ import {
2
+ CustomEditor,
3
+ type KeybindingsManager,
4
+ type Theme,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ type AutocompleteProvider,
8
+ type Component,
9
+ type EditorComponent,
10
+ type EditorTheme,
11
+ type TUI,
12
+ truncateToWidth,
13
+ visibleWidth,
14
+ } from "@earendil-works/pi-tui";
15
+ import {
16
+ ACCENT_RAIL_CHROME_WIDTH,
17
+ renderAccentRailEditorFrame,
18
+ } from "./accent-rail-editor.ts";
19
+ import { renderCompletionPalette } from "./completion-menu.ts";
20
+ import type { EditorStyle, ZentuiConfig } from "../app/config/shell.ts";
21
+ import { renderEditorMetadataFormat } from "./editor-metadata-format.ts";
22
+ import {
23
+ type MinimalistEditorMetadata,
24
+ renderMinimalistFrame,
25
+ } from "./minimalist-editor.ts";
26
+ import {
27
+ EDITOR_ACCENT_FALLBACK,
28
+ EDITOR_BORDER_FALLBACK,
29
+ renderStyleForSourceOrFallback,
30
+ safeThemeFg,
31
+ } from "./style.ts";
32
+
33
+ const LEGACY_SPLIT_POLISHED_FRAME = Symbol.for("pi-zentui.polished-frame");
34
+
35
+ export type ViewportCounts = {
36
+ above?: string;
37
+ below?: string;
38
+ };
39
+
40
+ type PolishedFrameSplit = {
41
+ editorLines: string[];
42
+ /** Logical, unframed autocomplete payload rows owned by this module. */
43
+ trailingLines: string[];
44
+ viewport: ViewportCounts;
45
+ };
46
+
47
+ type PolishedFrameProvenance = {
48
+ rows: readonly string[];
49
+ split: PolishedFrameSplit;
50
+ };
51
+
52
+ const POLISHED_FRAME_SPLITS = new WeakMap<string[], PolishedFrameProvenance>();
53
+
54
+ type AutocompleteListInternals = Pick<Component, "render">;
55
+
56
+ type AutocompleteEditorInternals = {
57
+ autocompleteList?: AutocompleteListInternals;
58
+ isShowingAutocomplete?: () => boolean;
59
+ };
60
+
61
+ type AutocompleteCapture = {
62
+ compatible: boolean;
63
+ called: number;
64
+ rows: string[];
65
+ };
66
+
67
+ type WrappedEditor = EditorComponent &
68
+ AutocompleteEditorInternals & {
69
+ focused?: boolean;
70
+ onEscape?: () => void;
71
+ onCtrlD?: () => void;
72
+ onPasteImage?: () => void;
73
+ onExtensionShortcut?: (data: string) => boolean;
74
+ actionHandlers?: Map<unknown, () => void>;
75
+ wantsKeyRelease?: boolean;
76
+ disableSubmit?: boolean;
77
+ getLines?: () => string[];
78
+ getCursor?: () => unknown;
79
+ getMode?: () => unknown;
80
+ getPaddingX?: () => number;
81
+ getAutocompleteMaxVisible?: () => number;
82
+ addToHistory?: (text: string) => void;
83
+ getExpandedText?: () => string;
84
+ insertTextAtCursor?: (text: string) => void;
85
+ setAutocompleteProvider?: (provider: AutocompleteProvider) => void;
86
+ setPaddingX?: (padding: number) => void;
87
+ setAutocompleteMaxVisible?: (maxVisible: number) => void;
88
+ };
89
+
90
+ export type EditorMeta = {
91
+ modelLabel: string;
92
+ modelId?: string;
93
+ modelName?: string;
94
+ providerLabel: string;
95
+ sessionName?: string;
96
+ };
97
+
98
+ export type PolishedEditorFrameOptions = {
99
+ width: number;
100
+ editorLines: string[];
101
+ autocompleteLines?: string[];
102
+ viewport?: ViewportCounts;
103
+ uiTheme: Theme;
104
+ config: ZentuiConfig;
105
+ modelMeta: EditorMeta;
106
+ thinkingLevel?: string;
107
+ rightStatus?: string;
108
+ borderColor?: (text: string) => string;
109
+ };
110
+
111
+ type PolishedFrameOptions = {
112
+ width: number;
113
+ baseRendered: string[];
114
+ autocompleteSource: AutocompleteEditorInternals;
115
+ autocompleteCapture?: AutocompleteCapture;
116
+ uiTheme: Theme;
117
+ config: ZentuiConfig;
118
+ modelMeta: EditorMeta;
119
+ thinkingLevel: string | undefined;
120
+ rightStatus?: string;
121
+ ownedFrame?: PolishedFrameSplit;
122
+ trustedBaseFrame?: boolean;
123
+ borderColor?: (text: string) => string;
124
+ };
125
+
126
+ type PolishedFrameResult = {
127
+ lines: string[];
128
+ decorated: boolean;
129
+ };
130
+
131
+ type AccentRailFrameAdapterOptions = {
132
+ width: number;
133
+ baseRendered: string[];
134
+ autocompleteSource: AutocompleteEditorInternals;
135
+ autocompleteCapture?: AutocompleteCapture;
136
+ uiTheme: Theme;
137
+ config: ZentuiConfig;
138
+ ownedFrame?: PolishedFrameSplit;
139
+ trustedBaseFrame?: boolean;
140
+ };
141
+
142
+ type MinimalistFrameAdapterOptions = {
143
+ width: number;
144
+ baseRendered: string[];
145
+ autocompleteSource: AutocompleteEditorInternals;
146
+ autocompleteCapture?: AutocompleteCapture;
147
+ uiTheme: Theme;
148
+ config: ZentuiConfig;
149
+ inputText: string;
150
+ metadata: MinimalistEditorMetadata;
151
+ ownedFrame?: PolishedFrameSplit;
152
+ trustedBaseFrame?: boolean;
153
+ borderColor?: (text: string) => string;
154
+ };
155
+
156
+ type AutocompleteCount = { known: true; count: number } | { known: false };
157
+
158
+ function isStringArray(value: unknown): value is string[] {
159
+ return (
160
+ Array.isArray(value) && value.every((line) => typeof line === "string")
161
+ );
162
+ }
163
+
164
+ function isViewportCounts(value: unknown): value is ViewportCounts {
165
+ if (!value || typeof value !== "object") return false;
166
+ const counts = value as Record<string, unknown>;
167
+ return [counts.above, counts.below].every(
168
+ (count) =>
169
+ count === undefined ||
170
+ (typeof count === "string" && /^[1-9]\d*$/.test(count)),
171
+ );
172
+ }
173
+
174
+ function isPolishedFrameSplit(
175
+ value: unknown,
176
+ baseLineCount: number,
177
+ ): value is PolishedFrameSplit {
178
+ if (!value || typeof value !== "object") return false;
179
+ const split = value as Record<string, unknown>;
180
+ return (
181
+ isStringArray(split.editorLines) &&
182
+ isStringArray(split.trailingLines) &&
183
+ split.trailingLines.length <= baseLineCount &&
184
+ isViewportCounts(split.viewport)
185
+ );
186
+ }
187
+
188
+ function stripNativeRightPadding(value: string): string {
189
+ return value.replace(/ +$/, "");
190
+ }
191
+
192
+ function autocompleteCount(
193
+ source: AutocompleteEditorInternals,
194
+ capture: AutocompleteCapture | undefined,
195
+ baseRendered: string[],
196
+ ): AutocompleteCount {
197
+ try {
198
+ const showing = source.isShowingAutocomplete;
199
+ if (typeof showing !== "function" || !showing.call(source))
200
+ return { known: true, count: 0 };
201
+ if (
202
+ !capture?.compatible ||
203
+ capture.called !== 1 ||
204
+ capture.rows.length <= 0 ||
205
+ capture.rows.length >= baseRendered.length
206
+ )
207
+ return { known: false };
208
+ const suffix = baseRendered.slice(-capture.rows.length);
209
+ if (
210
+ !suffix.every((line, index) => {
211
+ const captured = capture.rows[index];
212
+ return (
213
+ captured !== undefined &&
214
+ (line === captured ||
215
+ stripNativeRightPadding(line) === stripNativeRightPadding(captured))
216
+ );
217
+ })
218
+ )
219
+ return { known: false };
220
+ return { known: true, count: capture.rows.length };
221
+ } catch {
222
+ return { known: false };
223
+ }
224
+ }
225
+
226
+ /** @internal Exported only for descriptor-safety regression tests. */
227
+ export function renderWithAutocompleteCapture<T>(
228
+ source: AutocompleteEditorInternals,
229
+ render: () => T,
230
+ ): { value: T; capture?: AutocompleteCapture } {
231
+ let showing = false;
232
+ try {
233
+ showing =
234
+ typeof source.isShowingAutocomplete === "function" &&
235
+ source.isShowingAutocomplete.call(source);
236
+ } catch {
237
+ return { value: render() };
238
+ }
239
+ if (!showing)
240
+ return {
241
+ value: render(),
242
+ capture: { compatible: true, called: 0, rows: [] },
243
+ };
244
+
245
+ let list: AutocompleteListInternals;
246
+ let own: PropertyDescriptor | undefined;
247
+ let predecessor: (...args: unknown[]) => unknown;
248
+ try {
249
+ const candidate = source.autocompleteList;
250
+ if (!candidate) return { value: render() };
251
+ own = Object.getOwnPropertyDescriptor(candidate, "render");
252
+ const current = Reflect.get(candidate, "render");
253
+ if (typeof current !== "function") return { value: render() };
254
+ if (own && (!("value" in own) || own.writable !== true))
255
+ return { value: render() };
256
+ if (!own && !Object.isExtensible(candidate)) return { value: render() };
257
+ list = candidate;
258
+ predecessor = current as (...args: unknown[]) => unknown;
259
+ } catch {
260
+ return { value: render() };
261
+ }
262
+
263
+ const capture: AutocompleteCapture = {
264
+ compatible: true,
265
+ called: 0,
266
+ rows: [],
267
+ };
268
+ const wrapper = function (
269
+ this: AutocompleteListInternals,
270
+ ...args: unknown[]
271
+ ) {
272
+ const result = Reflect.apply(predecessor, this, args);
273
+ capture.called++;
274
+ if (!isStringArray(result)) {
275
+ capture.compatible = false;
276
+ return result;
277
+ }
278
+ capture.rows = [...result];
279
+ return result;
280
+ };
281
+ const installedDescriptor: PropertyDescriptor = {
282
+ ...(own ?? { configurable: true, enumerable: false, writable: true }),
283
+ value: wrapper,
284
+ };
285
+ try {
286
+ Object.defineProperty(list, "render", installedDescriptor);
287
+ } catch {
288
+ return { value: render() };
289
+ }
290
+
291
+ try {
292
+ return { value: render(), capture };
293
+ } finally {
294
+ let current: PropertyDescriptor | undefined;
295
+ let currentValue: unknown;
296
+ let descriptorKnown = true;
297
+ try {
298
+ current = Object.getOwnPropertyDescriptor(list, "render");
299
+ currentValue =
300
+ current && "value" in current
301
+ ? current.value
302
+ : Reflect.get(list, "render");
303
+ } catch {
304
+ descriptorKnown = false;
305
+ try {
306
+ currentValue = Reflect.get(list, "render");
307
+ } catch {
308
+ currentValue = undefined;
309
+ }
310
+ }
311
+ if (currentValue === wrapper) {
312
+ if (
313
+ !descriptorKnown ||
314
+ !current ||
315
+ current.configurable !== installedDescriptor.configurable ||
316
+ current.enumerable !== installedDescriptor.enumerable ||
317
+ current.writable !== installedDescriptor.writable
318
+ )
319
+ capture.compatible = false;
320
+ try {
321
+ if (own) Object.defineProperty(list, "render", own);
322
+ else if (!Reflect.deleteProperty(list, "render"))
323
+ capture.compatible = false;
324
+ } catch {
325
+ capture.compatible = false;
326
+ }
327
+ } else {
328
+ // A synchronous third-party replacement wins; captured semantics are no longer trustworthy.
329
+ capture.compatible = false;
330
+ }
331
+ }
332
+ }
333
+
334
+ function clampRenderedLines(lines: string[], width: number): string[] {
335
+ const maxWidth = Math.max(0, width);
336
+ return lines.map((line) => truncateToWidth(line, maxWidth, ""));
337
+ }
338
+
339
+ function fillLine(content: string, width: number): string {
340
+ const truncated = truncateToWidth(content, Math.max(0, width), "");
341
+ const pad = " ".repeat(Math.max(0, width - visibleWidth(truncated)));
342
+ return `${truncated}${pad}`;
343
+ }
344
+
345
+ function isLowRailPolishedStyle(style: EditorStyle): boolean {
346
+ return style === "opencode-copy-friendly";
347
+ }
348
+
349
+ function selectedPolishedConfig(config: ZentuiConfig) {
350
+ switch (config.components.editor.style) {
351
+ case "opencode":
352
+ return config.components.editor.styles.opencode;
353
+ case "opencode-copy-friendly":
354
+ return config.components.editor.styles["opencode-copy-friendly"];
355
+ case "accent-rail":
356
+ case "minimalist":
357
+ return undefined;
358
+ }
359
+ }
360
+
361
+ function lowRailPrompt(
362
+ config: ZentuiConfig,
363
+ uiTheme: Theme,
364
+ reset: string,
365
+ ): string {
366
+ const promptIcon = config.icons.editorPrompt;
367
+ return promptIcon
368
+ ? `${renderStyleForSourceOrFallback(
369
+ uiTheme,
370
+ config.components.editor.colorSource,
371
+ config.colors.editorPrompt ?? config.colors.editorAccent,
372
+ EDITOR_ACCENT_FALLBACK,
373
+ promptIcon,
374
+ )}${reset} `
375
+ : "";
376
+ }
377
+
378
+ function getEditorChromeWidths(
379
+ config: ZentuiConfig,
380
+ uiTheme: Theme,
381
+ reset: string,
382
+ ) {
383
+ const lowRail = isLowRailPolishedStyle(config.components.editor.style);
384
+ const prompt = lowRailPrompt(config, uiTheme, reset);
385
+ const rail = lowRail
386
+ ? ""
387
+ : `${renderStyleForSourceOrFallback(
388
+ uiTheme,
389
+ config.components.editor.colorSource,
390
+ config.colors.editorAccent,
391
+ EDITOR_ACCENT_FALLBACK,
392
+ config.icons.rail,
393
+ )}${reset} `;
394
+ return {
395
+ prompt,
396
+ promptWidth: visibleWidth(prompt),
397
+ rail,
398
+ railWidth: lowRail ? visibleWidth(prompt) : visibleWidth(rail),
399
+ };
400
+ }
401
+
402
+ function composeMetadataLine(
403
+ left: string,
404
+ right: string | undefined,
405
+ width: number,
406
+ ): string {
407
+ if (!right) return left;
408
+ const maxWidth = Math.max(0, width);
409
+ const rightWidth = visibleWidth(right);
410
+ if (rightWidth >= maxWidth) return truncateToWidth(right, maxWidth, "");
411
+
412
+ const leftWidth = Math.max(0, maxWidth - rightWidth - 1);
413
+ const leftText = truncateToWidth(left, leftWidth, "");
414
+ const gap = " ".repeat(
415
+ Math.max(1, maxWidth - visibleWidth(leftText) - rightWidth),
416
+ );
417
+ return `${leftText}${gap}${right}`;
418
+ }
419
+
420
+ function ansiStrippedText(line: string): string {
421
+ return line
422
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
423
+ .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "");
424
+ }
425
+
426
+ function plainRenderedText(line: string): string {
427
+ return ansiStrippedText(line).replace(/\[\/?[^\]]+\]/g, "");
428
+ }
429
+
430
+ function parseEditorBorder(
431
+ line: string,
432
+ direction: keyof ViewportCounts,
433
+ ): { count?: string } | undefined {
434
+ const plain = ansiStrippedText(line);
435
+ if (/^─+$/.test(plain)) return {};
436
+
437
+ const arrow = direction === "above" ? "↑" : "↓";
438
+ const match = new RegExp(`^─── ${arrow} ([1-9]\\d*) more ─*$`).exec(plain);
439
+ return match?.[1] ? { count: match[1] } : undefined;
440
+ }
441
+
442
+ function renderEditorBorder(
443
+ width: number,
444
+ direction: keyof ViewportCounts,
445
+ count: string | undefined,
446
+ ): string {
447
+ if (!count) return "─".repeat(width);
448
+ const arrow = direction === "above" ? "↑" : "↓";
449
+ const indicator = `─── ${arrow} ${count} more `;
450
+ return `${indicator}${"─".repeat(Math.max(0, width - visibleWidth(indicator)))}`;
451
+ }
452
+
453
+ function unwrapPolishedFrameOnly(
454
+ lines: string[],
455
+ config: ZentuiConfig,
456
+ uiTheme: Theme,
457
+ ): { editorLines: string[]; viewport: ViewportCounts } | undefined {
458
+ if (lines.length < 5) return undefined;
459
+ const top = parseEditorBorder(lines[0] ?? "", "above");
460
+ const bottom = parseEditorBorder(lines.at(-1) ?? "", "below");
461
+ if (!top || !bottom) return undefined;
462
+
463
+ const viewport = { above: top.count, below: bottom.count };
464
+ const interior = lines.slice(1, -1);
465
+ if (interior.length < 3) return undefined;
466
+
467
+ if (isLowRailPolishedStyle(config.components.editor.style)) {
468
+ if (
469
+ plainRenderedText(interior[0] ?? "").trim() !== "" ||
470
+ plainRenderedText(interior.at(-2) ?? "").trim() !== "" ||
471
+ !(interior.at(-1) ?? "").startsWith(" ")
472
+ )
473
+ return undefined;
474
+
475
+ const { prompt, promptWidth } = getEditorChromeWidths(
476
+ config,
477
+ uiTheme,
478
+ "\x1b[0m",
479
+ );
480
+ const continuation = " ".repeat(promptWidth);
481
+ const content = interior.slice(1, -2);
482
+ const unwrapped: string[] = [];
483
+ for (let index = 0; index < content.length; index++) {
484
+ const prefix = index === 0 ? prompt : continuation;
485
+ const line = content[index] ?? "";
486
+ if (prefix && !line.startsWith(prefix)) return undefined;
487
+ unwrapped.push(prefix ? line.slice(prefix.length) : line);
488
+ }
489
+ return { editorLines: unwrapped, viewport };
490
+ }
491
+
492
+ const { rail } = getEditorChromeWidths(config, uiTheme, "\x1b[0m");
493
+ if (!rail || interior.some((line) => !line.startsWith(rail)))
494
+ return undefined;
495
+ const unrailed = interior.map((line) => line.slice(rail.length));
496
+ if (
497
+ plainRenderedText(unrailed[0] ?? "").trim() !== "" ||
498
+ plainRenderedText(unrailed.at(-2) ?? "").trim() !== ""
499
+ )
500
+ return undefined;
501
+ return { editorLines: unrailed.slice(1, -2), viewport };
502
+ }
503
+
504
+ function splitPolishedFrame(
505
+ lines: string[],
506
+ config: ZentuiConfig,
507
+ uiTheme: Theme,
508
+ ): PolishedFrameSplit | undefined {
509
+ if (!parseEditorBorder(lines[0] ?? "", "above")) return undefined;
510
+
511
+ for (let bottomIndex = lines.length - 1; bottomIndex >= 4; bottomIndex--) {
512
+ if (!parseEditorBorder(lines[bottomIndex] ?? "", "below")) continue;
513
+ const frame = unwrapPolishedFrameOnly(
514
+ lines.slice(0, bottomIndex + 1),
515
+ config,
516
+ uiTheme,
517
+ );
518
+ if (frame) return { ...frame, trailingLines: lines.slice(bottomIndex + 1) };
519
+ }
520
+ return undefined;
521
+ }
522
+
523
+ function inspectPolishedFrameProvenance(
524
+ base: WrappedEditor,
525
+ rendered: string[],
526
+ config: ZentuiConfig,
527
+ uiTheme: Theme,
528
+ ): { safe: boolean; ownedFrame?: PolishedFrameSplit } {
529
+ const provenance = POLISHED_FRAME_SPLITS.get(rendered);
530
+ const provenanceMatches = Boolean(
531
+ provenance &&
532
+ provenance.rows.length === rendered.length &&
533
+ provenance.rows.every((line, index) => line === rendered[index]),
534
+ );
535
+ const ownedFrame = provenanceMatches ? provenance?.split : undefined;
536
+ const unsafe =
537
+ Boolean(provenance && !provenanceMatches) ||
538
+ LEGACY_SPLIT_POLISHED_FRAME in base ||
539
+ (!ownedFrame && Boolean(splitPolishedFrame(rendered, config, uiTheme)));
540
+ return { safe: !unsafe, ownedFrame };
541
+ }
542
+
543
+ function vimModeColor(mode: string): string {
544
+ switch (mode.toLowerCase()) {
545
+ case "insert":
546
+ return "success";
547
+ case "normal":
548
+ return "accent";
549
+ case "ex":
550
+ return "warning";
551
+ case "replace":
552
+ return "error";
553
+ case "visual":
554
+ return "syntaxKeyword";
555
+ default:
556
+ return "muted";
557
+ }
558
+ }
559
+
560
+ function readVimStatus(
561
+ editor: WrappedEditor,
562
+ uiTheme: Theme,
563
+ ): string | undefined {
564
+ const mode = editor.getMode?.();
565
+ if (typeof mode !== "string") return undefined;
566
+ const normalized = mode.trim();
567
+ if (!normalized) return undefined;
568
+ const label = `${normalized.toUpperCase()} `;
569
+ return safeThemeFg(uiTheme, vimModeColor(normalized), label);
570
+ }
571
+
572
+ function renderAccentRailFrameFromBase({
573
+ width,
574
+ baseRendered,
575
+ autocompleteSource,
576
+ autocompleteCapture,
577
+ uiTheme,
578
+ config,
579
+ ownedFrame,
580
+ trustedBaseFrame = false,
581
+ }: AccentRailFrameAdapterOptions): PolishedFrameResult {
582
+ if (width < ACCENT_RAIL_CHROME_WIDTH + 1 || baseRendered.length < 2) {
583
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
584
+ }
585
+ if (ownedFrame && !isPolishedFrameSplit(ownedFrame, baseRendered.length)) {
586
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
587
+ }
588
+ const autocomplete = ownedFrame
589
+ ? { known: true as const, count: ownedFrame.trailingLines.length }
590
+ : autocompleteCount(autocompleteSource, autocompleteCapture, baseRendered);
591
+ if (!autocomplete.known) {
592
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
593
+ }
594
+ const editorFrame =
595
+ !ownedFrame && autocomplete.count > 0
596
+ ? baseRendered.slice(0, -autocomplete.count)
597
+ : baseRendered;
598
+ const autocompleteLines = ownedFrame
599
+ ? ownedFrame.trailingLines
600
+ : autocomplete.count > 0
601
+ ? baseRendered.slice(-autocomplete.count)
602
+ : [];
603
+ if (editorFrame.length < 2) {
604
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
605
+ }
606
+ const parsedTop = parseEditorBorder(editorFrame[0] ?? "", "above");
607
+ const parsedBottom = parseEditorBorder(editorFrame.at(-1) ?? "", "below");
608
+ if (!ownedFrame && !trustedBaseFrame && (!parsedTop || !parsedBottom)) {
609
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
610
+ }
611
+ const editorLines = ownedFrame?.editorLines ?? editorFrame.slice(1, -1);
612
+ const viewport = ownedFrame?.viewport ?? {
613
+ above: parsedTop?.count,
614
+ below: parsedBottom?.count,
615
+ };
616
+ const lines = renderAccentRailEditorFrame({
617
+ width,
618
+ editorLines,
619
+ autocompleteLines,
620
+ viewport,
621
+ uiTheme,
622
+ config,
623
+ });
624
+ POLISHED_FRAME_SPLITS.set(lines, {
625
+ rows: Object.freeze([...lines]),
626
+ split: { editorLines, trailingLines: autocompleteLines, viewport },
627
+ });
628
+ return { lines, decorated: true };
629
+ }
630
+
631
+ function renderMinimalistFrameFromBase({
632
+ width,
633
+ baseRendered,
634
+ autocompleteSource,
635
+ autocompleteCapture,
636
+ uiTheme,
637
+ config,
638
+ inputText,
639
+ metadata,
640
+ ownedFrame,
641
+ trustedBaseFrame = false,
642
+ borderColor,
643
+ }: MinimalistFrameAdapterOptions): PolishedFrameResult {
644
+ if (width <= 4 || baseRendered.length < 2) {
645
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
646
+ }
647
+ if (ownedFrame && !isPolishedFrameSplit(ownedFrame, baseRendered.length)) {
648
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
649
+ }
650
+ const autocomplete = ownedFrame
651
+ ? { known: true as const, count: ownedFrame.trailingLines.length }
652
+ : autocompleteCount(autocompleteSource, autocompleteCapture, baseRendered);
653
+ if (!autocomplete.known) {
654
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
655
+ }
656
+ const editorFrame =
657
+ !ownedFrame && autocomplete.count > 0
658
+ ? baseRendered.slice(0, -autocomplete.count)
659
+ : baseRendered;
660
+ const autocompleteLines = ownedFrame
661
+ ? ownedFrame.trailingLines
662
+ : autocomplete.count > 0
663
+ ? baseRendered.slice(-autocomplete.count)
664
+ : [];
665
+ if (editorFrame.length < 2) {
666
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
667
+ }
668
+ const parsedTop = parseEditorBorder(editorFrame[0] ?? "", "above");
669
+ const parsedBottom = parseEditorBorder(editorFrame.at(-1) ?? "", "below");
670
+ if (!ownedFrame && !trustedBaseFrame && (!parsedTop || !parsedBottom)) {
671
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
672
+ }
673
+ const viewport = ownedFrame?.viewport ?? {
674
+ above: parsedTop?.count,
675
+ below: parsedBottom?.count,
676
+ };
677
+ return {
678
+ lines: renderMinimalistFrame({
679
+ width,
680
+ editorLines: ownedFrame?.editorLines ?? editorFrame.slice(1, -1),
681
+ autocompleteLines,
682
+ viewport: config.components.editor.viewportIndicators
683
+ ? viewport
684
+ : undefined,
685
+ inputText,
686
+ metadata,
687
+ uiTheme,
688
+ config,
689
+ borderColor,
690
+ }),
691
+ decorated: true,
692
+ };
693
+ }
694
+
695
+ function renderPolishedFrame({
696
+ width,
697
+ baseRendered,
698
+ autocompleteSource,
699
+ autocompleteCapture,
700
+ uiTheme,
701
+ config,
702
+ modelMeta,
703
+ thinkingLevel,
704
+ rightStatus,
705
+ ownedFrame,
706
+ trustedBaseFrame = false,
707
+ borderColor,
708
+ }: PolishedFrameOptions): PolishedFrameResult {
709
+ if (width <= 2)
710
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
711
+
712
+ if (baseRendered.length < 2) {
713
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
714
+ }
715
+ if (ownedFrame && !isPolishedFrameSplit(ownedFrame, baseRendered.length)) {
716
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
717
+ }
718
+
719
+ const autocomplete = ownedFrame
720
+ ? { known: true, count: ownedFrame.trailingLines.length }
721
+ : autocompleteCount(autocompleteSource, autocompleteCapture, baseRendered);
722
+ if (!autocomplete.known) {
723
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
724
+ }
725
+ const editorFrame =
726
+ !ownedFrame && autocomplete.count > 0
727
+ ? baseRendered.slice(0, -autocomplete.count)
728
+ : baseRendered;
729
+ const autocompleteLines = ownedFrame
730
+ ? ownedFrame.trailingLines
731
+ : autocomplete.count > 0
732
+ ? baseRendered.slice(-autocomplete.count)
733
+ : [];
734
+ if (editorFrame.length < 2) {
735
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
736
+ }
737
+
738
+ const parsedTop = parseEditorBorder(editorFrame[0] ?? "", "above");
739
+ const parsedBottom = parseEditorBorder(editorFrame.at(-1) ?? "", "below");
740
+ if (!ownedFrame && !trustedBaseFrame && (!parsedTop || !parsedBottom)) {
741
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
742
+ }
743
+ const editorLines = ownedFrame?.editorLines ?? editorFrame.slice(1, -1);
744
+ const viewport = ownedFrame?.viewport ?? {
745
+ above: parsedTop?.count,
746
+ below: parsedBottom?.count,
747
+ };
748
+ const lines = renderPolishedEditorFrame({
749
+ width,
750
+ editorLines,
751
+ autocompleteLines,
752
+ viewport,
753
+ uiTheme,
754
+ config,
755
+ modelMeta,
756
+ thinkingLevel,
757
+ rightStatus,
758
+ borderColor,
759
+ });
760
+ POLISHED_FRAME_SPLITS.set(lines, {
761
+ rows: Object.freeze([...lines]),
762
+ split: {
763
+ editorLines,
764
+ trailingLines: autocompleteLines,
765
+ viewport,
766
+ },
767
+ });
768
+ return { lines, decorated: true };
769
+ }
770
+
771
+ /** Pure Opencode frame composition shared by the live editor and settings preview. */
772
+ export function renderPolishedEditorFrame({
773
+ width,
774
+ editorLines,
775
+ autocompleteLines = [],
776
+ viewport = {},
777
+ uiTheme,
778
+ config,
779
+ modelMeta,
780
+ thinkingLevel,
781
+ rightStatus,
782
+ borderColor,
783
+ }: PolishedEditorFrameOptions): string[] {
784
+ if (width <= 2) return clampRenderedLines(editorLines, width);
785
+ const reset = "\x1b[0m";
786
+ const colorSource = config.components.editor.colorSource;
787
+ const { prompt, promptWidth, rail, railWidth } = getEditorChromeWidths(
788
+ config,
789
+ uiTheme,
790
+ reset,
791
+ );
792
+ const innerWidth = Math.max(0, width - railWidth);
793
+ const lowRailContinuation = " ".repeat(promptWidth);
794
+ const meta = renderEditorMetadataFormat(
795
+ selectedPolishedConfig(config)?.metadataFormat ??
796
+ config.components.editor.styles.opencode.metadataFormat,
797
+ {
798
+ model: modelMeta.modelLabel,
799
+ modelId: modelMeta.modelId ?? "",
800
+ modelName: modelMeta.modelName ?? "",
801
+ provider: modelMeta.providerLabel,
802
+ thinking: thinkingLevel ?? "",
803
+ sessionName: modelMeta.sessionName ?? "",
804
+ },
805
+ uiTheme,
806
+ config,
807
+ );
808
+ const lowRailMeta = composeMetadataLine(
809
+ meta,
810
+ rightStatus,
811
+ Math.max(0, width - 1),
812
+ );
813
+ const railedMeta = composeMetadataLine(meta, rightStatus, innerWidth);
814
+
815
+ const renderStaticBorder = (text: string) =>
816
+ renderStyleForSourceOrFallback(
817
+ uiTheme,
818
+ colorSource,
819
+ config.colors.editorBorder,
820
+ EDITOR_BORDER_FALLBACK,
821
+ text,
822
+ );
823
+ const renderBorder = (text: string) => {
824
+ if (
825
+ config.components.editor.borderColorMode !== "adaptive" ||
826
+ typeof borderColor !== "function"
827
+ ) {
828
+ return renderStaticBorder(text);
829
+ }
830
+ try {
831
+ const rendered = borderColor(text);
832
+ return typeof rendered === "string" ? rendered : renderStaticBorder(text);
833
+ } catch {
834
+ return renderStaticBorder(text);
835
+ }
836
+ };
837
+ const top = renderBorder(
838
+ renderEditorBorder(
839
+ width,
840
+ "above",
841
+ config.components.editor.viewportIndicators ? viewport.above : undefined,
842
+ ),
843
+ );
844
+ const bottom = renderBorder(
845
+ renderEditorBorder(
846
+ width,
847
+ "below",
848
+ config.components.editor.viewportIndicators ? viewport.below : undefined,
849
+ ),
850
+ );
851
+ const completionLines =
852
+ selectedPolishedConfig(config)?.completionMenu === "palette"
853
+ ? renderCompletionPalette({
854
+ lines: autocompleteLines,
855
+ width,
856
+ theme: uiTheme,
857
+ renderSeparator: renderBorder,
858
+ ownedBackground: false,
859
+ })
860
+ : autocompleteLines;
861
+ const lines = ["", ...editorLines, "", railedMeta];
862
+ const renderedLines = isLowRailPolishedStyle(config.components.editor.style)
863
+ ? [
864
+ top,
865
+ "",
866
+ ...editorLines.map(
867
+ (line, index) =>
868
+ `${index === 0 ? prompt : lowRailContinuation}${fillLine(line, innerWidth)}`,
869
+ ),
870
+ "",
871
+ ` ${truncateToWidth(lowRailMeta, Math.max(0, width - 1), "")}`,
872
+ bottom,
873
+ ...completionLines,
874
+ ]
875
+ : [
876
+ top,
877
+ ...lines.map((line) => `${rail}${fillLine(line, innerWidth)}`),
878
+ bottom,
879
+ ...completionLines,
880
+ ];
881
+
882
+ return clampRenderedLines(renderedLines, width);
883
+ }
884
+
885
+ export class PolishedEditor extends CustomEditor {
886
+ private readonly getModelMeta: () => EditorMeta;
887
+ private readonly getThinkingLevel: () => string | undefined;
888
+ private readonly getMinimalistMetadata: () => MinimalistEditorMetadata;
889
+ private readonly onMinimalistDecorationChange: (active: boolean) => void;
890
+ private readonly getConfig: () => ZentuiConfig;
891
+ private readonly uiTheme: Theme;
892
+
893
+ constructor(
894
+ tui: TUI,
895
+ theme: EditorTheme,
896
+ keybindings: KeybindingsManager,
897
+ uiTheme: Theme,
898
+ getConfig: () => ZentuiConfig,
899
+ getModelMeta: () => EditorMeta,
900
+ getThinkingLevel: () => string | undefined,
901
+ getMinimalistMetadata: () => MinimalistEditorMetadata = () => ({ cwd: "" }),
902
+ onMinimalistDecorationChange: (active: boolean) => void = () => {},
903
+ ) {
904
+ super(tui, theme, keybindings, { paddingX: 0 });
905
+ this.borderColor = (text: string) => safeThemeFg(uiTheme, "border", text);
906
+ this.uiTheme = uiTheme;
907
+ this.getConfig = getConfig;
908
+ this.getModelMeta = getModelMeta;
909
+ this.getThinkingLevel = getThinkingLevel;
910
+ this.getMinimalistMetadata = getMinimalistMetadata;
911
+ this.onMinimalistDecorationChange = onMinimalistDecorationChange;
912
+ }
913
+
914
+ private reportMinimalistDecoration(active: boolean): void {
915
+ this.onMinimalistDecorationChange(active);
916
+ }
917
+
918
+ render(width: number): string[] {
919
+ const config = this.getConfig();
920
+ if (!config.components.editor.enabled) {
921
+ this.reportMinimalistDecoration(false);
922
+ return clampRenderedLines(super.render(width), width);
923
+ }
924
+ if (config.components.editor.style === "accent-rail") {
925
+ this.reportMinimalistDecoration(false);
926
+ if (width < ACCENT_RAIL_CHROME_WIDTH + 1) {
927
+ return clampRenderedLines(super.render(width), width);
928
+ }
929
+ let captured: { value: string[]; capture?: AutocompleteCapture };
930
+ try {
931
+ captured = renderWithAutocompleteCapture(
932
+ this as unknown as AutocompleteEditorInternals,
933
+ () => super.render(width - ACCENT_RAIL_CHROME_WIDTH),
934
+ );
935
+ } catch {
936
+ return clampRenderedLines(super.render(width), width);
937
+ }
938
+ try {
939
+ const result = renderAccentRailFrameFromBase({
940
+ width,
941
+ baseRendered: captured.value,
942
+ autocompleteSource: this as unknown as AutocompleteEditorInternals,
943
+ autocompleteCapture: captured.capture,
944
+ uiTheme: this.uiTheme,
945
+ config,
946
+ trustedBaseFrame: true,
947
+ });
948
+ if (result.decorated) return result.lines;
949
+ } catch {
950
+ // Decoration is optional; preserve the completed same-render rows below.
951
+ }
952
+ return clampRenderedLines(captured.value, width);
953
+ }
954
+ if (config.components.editor.style === "minimalist") {
955
+ if (width <= 4) {
956
+ this.reportMinimalistDecoration(false);
957
+ return clampRenderedLines(super.render(width), width);
958
+ }
959
+ let captured: { value: string[]; capture?: AutocompleteCapture };
960
+ try {
961
+ captured = renderWithAutocompleteCapture(
962
+ this as unknown as AutocompleteEditorInternals,
963
+ () => super.render(Math.max(0, width - 4)),
964
+ );
965
+ } catch {
966
+ this.reportMinimalistDecoration(false);
967
+ return clampRenderedLines(super.render(width), width);
968
+ }
969
+ try {
970
+ const result = renderMinimalistFrameFromBase({
971
+ width,
972
+ baseRendered: captured.value,
973
+ autocompleteSource: this as unknown as AutocompleteEditorInternals,
974
+ autocompleteCapture: captured.capture,
975
+ uiTheme: this.uiTheme,
976
+ config,
977
+ inputText: this.getText(),
978
+ metadata: this.getMinimalistMetadata(),
979
+ trustedBaseFrame: true,
980
+ borderColor: this.borderColor,
981
+ });
982
+ this.reportMinimalistDecoration(result.decorated);
983
+ return result.lines;
984
+ } catch {
985
+ this.reportMinimalistDecoration(false);
986
+ return clampRenderedLines(captured.value, width);
987
+ }
988
+ }
989
+ this.reportMinimalistDecoration(false);
990
+ if (width <= 2) {
991
+ return clampRenderedLines(super.render(width), width);
992
+ }
993
+
994
+ const { railWidth } = getEditorChromeWidths(
995
+ config,
996
+ this.uiTheme,
997
+ "\x1b[0m",
998
+ );
999
+ const innerWidth = Math.max(0, width - railWidth);
1000
+ let captured: { value: string[]; capture?: AutocompleteCapture };
1001
+ try {
1002
+ captured = renderWithAutocompleteCapture(
1003
+ this as unknown as AutocompleteEditorInternals,
1004
+ () => super.render(innerWidth),
1005
+ );
1006
+ } catch {
1007
+ return clampRenderedLines(super.render(width), width);
1008
+ }
1009
+ try {
1010
+ const result = renderPolishedFrame({
1011
+ width,
1012
+ baseRendered: captured.value,
1013
+ autocompleteSource: this as unknown as AutocompleteEditorInternals,
1014
+ autocompleteCapture: captured.capture,
1015
+ uiTheme: this.uiTheme,
1016
+ config,
1017
+ modelMeta: this.getModelMeta(),
1018
+ thinkingLevel: this.getThinkingLevel(),
1019
+ trustedBaseFrame: true,
1020
+ borderColor: this.borderColor,
1021
+ });
1022
+ if (result.decorated) return result.lines;
1023
+ } catch {
1024
+ // Decoration is optional; preserve the completed same-render rows below.
1025
+ }
1026
+ return clampRenderedLines(captured.value, width);
1027
+ }
1028
+ }
1029
+
1030
+ export class WrappedPolishedEditor implements EditorComponent {
1031
+ declare readonly addToHistory?: (text: string) => void;
1032
+ declare readonly insertTextAtCursor?: (text: string) => void;
1033
+ declare readonly setAutocompleteProvider?: (
1034
+ provider: AutocompleteProvider,
1035
+ ) => void;
1036
+ declare readonly setPaddingX?: (padding: number) => void;
1037
+ declare readonly setAutocompleteMaxVisible?: (maxVisible: number) => void;
1038
+
1039
+ constructor(
1040
+ private readonly base: WrappedEditor,
1041
+ private readonly uiTheme: Theme,
1042
+ private readonly getConfig: () => ZentuiConfig,
1043
+ private readonly getModelMeta: () => EditorMeta,
1044
+ private readonly getThinkingLevel: () => string | undefined,
1045
+ private readonly getMinimalistMetadata: () => MinimalistEditorMetadata = () => ({
1046
+ cwd: "",
1047
+ }),
1048
+ private readonly onMinimalistDecorationChange: (
1049
+ active: boolean,
1050
+ ) => void = () => {},
1051
+ ) {
1052
+ if (typeof base.addToHistory === "function") {
1053
+ this.addToHistory = (text) => base.addToHistory?.(text);
1054
+ }
1055
+ if (typeof base.insertTextAtCursor === "function") {
1056
+ this.insertTextAtCursor = (text) => base.insertTextAtCursor?.(text);
1057
+ }
1058
+ if (typeof base.setAutocompleteProvider === "function") {
1059
+ this.setAutocompleteProvider = (provider) =>
1060
+ base.setAutocompleteProvider?.(provider);
1061
+ }
1062
+ if (typeof base.setPaddingX === "function") {
1063
+ this.setPaddingX = (padding) => base.setPaddingX?.(padding);
1064
+ }
1065
+ if (typeof base.setAutocompleteMaxVisible === "function") {
1066
+ this.setAutocompleteMaxVisible = (maxVisible) =>
1067
+ base.setAutocompleteMaxVisible?.(maxVisible);
1068
+ }
1069
+ }
1070
+
1071
+ get focused(): boolean {
1072
+ return Boolean(this.base.focused);
1073
+ }
1074
+ set focused(value: boolean) {
1075
+ this.base.focused = value;
1076
+ }
1077
+
1078
+ get borderColor(): ((str: string) => string) | undefined {
1079
+ return this.base.borderColor;
1080
+ }
1081
+ set borderColor(value: ((str: string) => string) | undefined) {
1082
+ this.base.borderColor = value;
1083
+ }
1084
+
1085
+ get onSubmit(): ((text: string) => void) | undefined {
1086
+ return this.base.onSubmit;
1087
+ }
1088
+ set onSubmit(value: ((text: string) => void) | undefined) {
1089
+ this.base.onSubmit = value;
1090
+ }
1091
+
1092
+ get onChange(): ((text: string) => void) | undefined {
1093
+ return this.base.onChange;
1094
+ }
1095
+ set onChange(value: ((text: string) => void) | undefined) {
1096
+ this.base.onChange = value;
1097
+ }
1098
+
1099
+ get onEscape(): (() => void) | undefined {
1100
+ return this.base.onEscape;
1101
+ }
1102
+ set onEscape(value: (() => void) | undefined) {
1103
+ this.base.onEscape = value;
1104
+ }
1105
+
1106
+ get onCtrlD(): (() => void) | undefined {
1107
+ return this.base.onCtrlD;
1108
+ }
1109
+ set onCtrlD(value: (() => void) | undefined) {
1110
+ this.base.onCtrlD = value;
1111
+ }
1112
+
1113
+ get onPasteImage(): (() => void) | undefined {
1114
+ return this.base.onPasteImage;
1115
+ }
1116
+ set onPasteImage(value: (() => void) | undefined) {
1117
+ this.base.onPasteImage = value;
1118
+ }
1119
+
1120
+ get onExtensionShortcut(): ((data: string) => boolean) | undefined {
1121
+ return this.base.onExtensionShortcut;
1122
+ }
1123
+ set onExtensionShortcut(value: ((data: string) => boolean) | undefined) {
1124
+ this.base.onExtensionShortcut = value;
1125
+ }
1126
+
1127
+ get actionHandlers(): Map<unknown, () => void> | undefined {
1128
+ return this.base.actionHandlers;
1129
+ }
1130
+ set actionHandlers(value: Map<unknown, () => void> | undefined) {
1131
+ this.base.actionHandlers = value;
1132
+ }
1133
+
1134
+ get wantsKeyRelease(): boolean | undefined {
1135
+ return this.base.wantsKeyRelease;
1136
+ }
1137
+ set wantsKeyRelease(value: boolean | undefined) {
1138
+ this.base.wantsKeyRelease = value;
1139
+ }
1140
+
1141
+ get disableSubmit(): boolean | undefined {
1142
+ return this.base.disableSubmit;
1143
+ }
1144
+ set disableSubmit(value: boolean | undefined) {
1145
+ this.base.disableSubmit = value;
1146
+ }
1147
+
1148
+ private reportMinimalistDecoration(active: boolean): void {
1149
+ this.onMinimalistDecorationChange(active);
1150
+ }
1151
+
1152
+ render(width: number): string[] {
1153
+ const config = this.getConfig();
1154
+ if (!config.components.editor.enabled) {
1155
+ this.reportMinimalistDecoration(false);
1156
+ return clampRenderedLines(this.base.render(width), width);
1157
+ }
1158
+ if (config.components.editor.style === "accent-rail") {
1159
+ this.reportMinimalistDecoration(false);
1160
+ if (width < ACCENT_RAIL_CHROME_WIDTH + 1) {
1161
+ return clampRenderedLines(this.base.render(width), width);
1162
+ }
1163
+ let captured: { value: string[]; capture?: AutocompleteCapture };
1164
+ try {
1165
+ captured = renderWithAutocompleteCapture(this.base, () =>
1166
+ this.base.render(width - ACCENT_RAIL_CHROME_WIDTH),
1167
+ );
1168
+ } catch {
1169
+ return clampRenderedLines(this.base.render(width), width);
1170
+ }
1171
+ try {
1172
+ const provenance = inspectPolishedFrameProvenance(
1173
+ this.base,
1174
+ captured.value,
1175
+ config,
1176
+ this.uiTheme,
1177
+ );
1178
+ if (provenance.safe) {
1179
+ const result = renderAccentRailFrameFromBase({
1180
+ width,
1181
+ baseRendered: captured.value,
1182
+ autocompleteSource: this.base,
1183
+ autocompleteCapture: captured.capture,
1184
+ uiTheme: this.uiTheme,
1185
+ config,
1186
+ ownedFrame: provenance.ownedFrame,
1187
+ });
1188
+ if (result.decorated) return result.lines;
1189
+ }
1190
+ } catch {
1191
+ // Decoration is optional; preserve the completed same-render rows below.
1192
+ }
1193
+ return clampRenderedLines(captured.value, width);
1194
+ }
1195
+ if (config.components.editor.style === "minimalist") {
1196
+ if (width <= 4) {
1197
+ this.reportMinimalistDecoration(false);
1198
+ return clampRenderedLines(this.base.render(width), width);
1199
+ }
1200
+ let captured: { value: string[]; capture?: AutocompleteCapture };
1201
+ try {
1202
+ captured = renderWithAutocompleteCapture(this.base, () =>
1203
+ this.base.render(Math.max(0, width - 4)),
1204
+ );
1205
+ } catch {
1206
+ this.reportMinimalistDecoration(false);
1207
+ return clampRenderedLines(this.base.render(width), width);
1208
+ }
1209
+ try {
1210
+ const provenance = inspectPolishedFrameProvenance(
1211
+ this.base,
1212
+ captured.value,
1213
+ config,
1214
+ this.uiTheme,
1215
+ );
1216
+ if (provenance.safe) {
1217
+ const result = renderMinimalistFrameFromBase({
1218
+ width,
1219
+ baseRendered: captured.value,
1220
+ autocompleteSource: this.base,
1221
+ autocompleteCapture: captured.capture,
1222
+ uiTheme: this.uiTheme,
1223
+ config,
1224
+ inputText: this.base.getText(),
1225
+ metadata: this.getMinimalistMetadata(),
1226
+ ownedFrame: provenance.ownedFrame,
1227
+ borderColor: this.borderColor,
1228
+ });
1229
+ if (result.decorated) {
1230
+ this.reportMinimalistDecoration(true);
1231
+ return result.lines;
1232
+ }
1233
+ }
1234
+ } catch {
1235
+ // Decoration is optional; preserve the completed same-render rows below.
1236
+ }
1237
+ this.reportMinimalistDecoration(false);
1238
+ return clampRenderedLines(captured.value, width);
1239
+ }
1240
+ this.reportMinimalistDecoration(false);
1241
+ if (width <= 2) return clampRenderedLines(this.base.render(width), width);
1242
+
1243
+ const { railWidth } = getEditorChromeWidths(
1244
+ config,
1245
+ this.uiTheme,
1246
+ "\x1b[0m",
1247
+ );
1248
+ const innerWidth = Math.max(0, width - railWidth);
1249
+ let captured: { value: string[]; capture?: AutocompleteCapture };
1250
+ try {
1251
+ captured = renderWithAutocompleteCapture(this.base, () =>
1252
+ this.base.render(innerWidth),
1253
+ );
1254
+ } catch {
1255
+ return clampRenderedLines(this.base.render(width), width);
1256
+ }
1257
+ let result: PolishedFrameResult | undefined;
1258
+ try {
1259
+ const provenance = inspectPolishedFrameProvenance(
1260
+ this.base,
1261
+ captured.value,
1262
+ config,
1263
+ this.uiTheme,
1264
+ );
1265
+ if (provenance.safe) {
1266
+ result = renderPolishedFrame({
1267
+ width,
1268
+ baseRendered: captured.value,
1269
+ autocompleteSource: this.base,
1270
+ autocompleteCapture: captured.capture,
1271
+ uiTheme: this.uiTheme,
1272
+ config,
1273
+ modelMeta: this.getModelMeta(),
1274
+ thinkingLevel: this.getThinkingLevel(),
1275
+ rightStatus: readVimStatus(this.base, this.uiTheme),
1276
+ ownedFrame: provenance.ownedFrame,
1277
+ borderColor: this.borderColor,
1278
+ });
1279
+ }
1280
+ } catch {
1281
+ // Decoration is optional; preserve the completed same-render rows below.
1282
+ }
1283
+ if (result?.decorated) return result.lines;
1284
+ return clampRenderedLines(captured.value, width);
1285
+ }
1286
+
1287
+ invalidate(): void {
1288
+ this.base.invalidate?.();
1289
+ }
1290
+
1291
+ handleInput(data: string): void {
1292
+ this.base.handleInput(data);
1293
+ }
1294
+
1295
+ getText(): string {
1296
+ return this.base.getText();
1297
+ }
1298
+
1299
+ setText(text: string): void {
1300
+ this.base.setText(text);
1301
+ }
1302
+
1303
+ getExpandedText(): string {
1304
+ return this.base.getExpandedText?.() ?? this.base.getText();
1305
+ }
1306
+
1307
+ getLines(): string[] {
1308
+ return this.base.getLines?.() ?? this.base.getText().split("\n");
1309
+ }
1310
+
1311
+ getCursor(): unknown {
1312
+ return this.base.getCursor?.();
1313
+ }
1314
+
1315
+ getMode(): unknown {
1316
+ return this.base.getMode?.();
1317
+ }
1318
+
1319
+ getPaddingX(): number | undefined {
1320
+ return this.base.getPaddingX?.();
1321
+ }
1322
+
1323
+ getAutocompleteMaxVisible(): number | undefined {
1324
+ return this.base.getAutocompleteMaxVisible?.();
1325
+ }
1326
+ }