@xzzpig/pi-starline 0.1.0

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 (45) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +223 -0
  3. package/extensions/starline/config.ts +1317 -0
  4. package/extensions/starline/editor-cursor.ts +55 -0
  5. package/extensions/starline/editor-factory-marker.ts +49 -0
  6. package/extensions/starline/editor-metadata-format.ts +259 -0
  7. package/extensions/starline/extension-status.ts +88 -0
  8. package/extensions/starline/footer-format.ts +279 -0
  9. package/extensions/starline/footer.ts +744 -0
  10. package/extensions/starline/format.ts +473 -0
  11. package/extensions/starline/git.ts +467 -0
  12. package/extensions/starline/icons.ts +294 -0
  13. package/extensions/starline/index.ts +651 -0
  14. package/extensions/starline/live-context.ts +75 -0
  15. package/extensions/starline/mouse/capabilities.ts +207 -0
  16. package/extensions/starline/mouse/component-tree.ts +230 -0
  17. package/extensions/starline/mouse/editor-caret.ts +498 -0
  18. package/extensions/starline/mouse/editor-hit-test.ts +133 -0
  19. package/extensions/starline/mouse/editor-mouse.ts +142 -0
  20. package/extensions/starline/mouse/editor-scroll.ts +119 -0
  21. package/extensions/starline/mouse/editor-text-cursor.ts +209 -0
  22. package/extensions/starline/mouse/editor-text-edit.ts +90 -0
  23. package/extensions/starline/mouse/hint.ts +14 -0
  24. package/extensions/starline/mouse/hit-test.ts +144 -0
  25. package/extensions/starline/mouse/index.ts +1119 -0
  26. package/extensions/starline/mouse/selection-state.ts +68 -0
  27. package/extensions/starline/mouse/tool-box.ts +290 -0
  28. package/extensions/starline/mouse/transcript-copy.ts +166 -0
  29. package/extensions/starline/mouse/word-select.ts +145 -0
  30. package/extensions/starline/package-version.ts +650 -0
  31. package/extensions/starline/paste-collapse.ts +312 -0
  32. package/extensions/starline/pill-config.ts +122 -0
  33. package/extensions/starline/pill.ts +295 -0
  34. package/extensions/starline/project-refresh.ts +104 -0
  35. package/extensions/starline/project-state.ts +66 -0
  36. package/extensions/starline/prototype-patch-registry.ts +136 -0
  37. package/extensions/starline/runtime.ts +841 -0
  38. package/extensions/starline/selector-border.ts +77 -0
  39. package/extensions/starline/session-lifecycle.ts +60 -0
  40. package/extensions/starline/settings-command.ts +925 -0
  41. package/extensions/starline/state.ts +65 -0
  42. package/extensions/starline/style.ts +528 -0
  43. package/extensions/starline/ui.ts +611 -0
  44. package/extensions/starline/user-message.ts +267 -0
  45. package/package.json +60 -0
@@ -0,0 +1,611 @@
1
+ import { CustomEditor, type KeybindingsManager, type Theme } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ type AutocompleteProvider,
4
+ type Component,
5
+ type EditorComponent,
6
+ type EditorTheme,
7
+ type TUI,
8
+ truncateToWidth,
9
+ visibleWidth,
10
+ } from "@earendil-works/pi-tui";
11
+ import type { PolishedTuiConfig } from "./config";
12
+ import { applyEditorCursorStyleToLines } from "./editor-cursor";
13
+ import { renderEditorMetadataFormat } from "./editor-metadata-format";
14
+ import { activeSelectionHintText, externalEditorHintText } from "./mouse";
15
+ import { composeHints } from "./mouse/hint";
16
+ import { pasteExpandHintText } from "./paste-collapse";
17
+ import {
18
+ EDITOR_ACCENT_FALLBACK,
19
+ EDITOR_BORDER_FALLBACK,
20
+ renderStyleForSourceOrFallback,
21
+ safeThemeFg,
22
+ } from "./style";
23
+
24
+ const SPLIT_POLISHED_FRAME: unique symbol = Symbol.for("pi-starline.polished-frame");
25
+
26
+ /**
27
+ * The key this package used before it was renamed from pi-zentui. Read, never
28
+ * written as the primary key: if both packages are loaded in the same
29
+ * session, a base editor built by either one must still resolve the
30
+ * splitter, or the wrapper double-renders the inner frame instead of
31
+ * unwrapping it. See the alias assignments after each class below, which
32
+ * expose the splitter under this key too so the reverse direction resolves.
33
+ */
34
+ const LEGACY_SPLIT_POLISHED_FRAME: unique symbol = Symbol.for("pi-zentui.polished-frame");
35
+
36
+ type PolishedFrameSplit = {
37
+ editorLines: string[];
38
+ trailingLines: string[];
39
+ };
40
+
41
+ type AutocompleteEditorInternals = {
42
+ autocompleteList?: Pick<Component, "render">;
43
+ isShowingAutocomplete?: () => boolean;
44
+ };
45
+
46
+ type WrappedEditor = EditorComponent &
47
+ AutocompleteEditorInternals & {
48
+ focused?: boolean;
49
+ onEscape?: () => void;
50
+ onCtrlD?: () => void;
51
+ onPasteImage?: () => void;
52
+ onExtensionShortcut?: (data: string) => boolean;
53
+ actionHandlers?: Map<unknown, () => void>;
54
+ wantsKeyRelease?: boolean;
55
+ disableSubmit?: boolean;
56
+ getLines?: () => string[];
57
+ getCursor?: () => unknown;
58
+ getMode?: () => unknown;
59
+ getPaddingX?: () => number;
60
+ getAutocompleteMaxVisible?: () => number;
61
+ addToHistory?: (text: string) => void;
62
+ getExpandedText?: () => string;
63
+ insertTextAtCursor?: (text: string) => void;
64
+ setAutocompleteProvider?: (provider: AutocompleteProvider) => void;
65
+ setPaddingX?: (padding: number) => void;
66
+ setAutocompleteMaxVisible?: (maxVisible: number) => void;
67
+ [SPLIT_POLISHED_FRAME]?: (lines: string[]) => PolishedFrameSplit | undefined;
68
+ [LEGACY_SPLIT_POLISHED_FRAME]?: (lines: string[]) => PolishedFrameSplit | undefined;
69
+ };
70
+
71
+ type EditorMeta = {
72
+ modelLabel: string;
73
+ modelId?: string;
74
+ modelName?: string;
75
+ providerLabel: string;
76
+ sessionName?: string;
77
+ };
78
+
79
+ type PolishedFrameOptions = {
80
+ width: number;
81
+ baseRendered: string[];
82
+ autocompleteSource: AutocompleteEditorInternals;
83
+ uiTheme: Theme;
84
+ config: PolishedTuiConfig;
85
+ modelMeta: EditorMeta;
86
+ thinkingLevel: string | undefined;
87
+ rightStatus?: string;
88
+ splitBaseFrame?: (lines: string[]) => PolishedFrameSplit | undefined;
89
+ };
90
+
91
+ function clampRenderedLines(lines: string[], width: number): string[] {
92
+ const maxWidth = Math.max(0, width);
93
+ return lines.map((line) => truncateToWidth(line, maxWidth, ""));
94
+ }
95
+
96
+ function fillLine(content: string, width: number): string {
97
+ const truncated = truncateToWidth(content, Math.max(0, width), "");
98
+ const pad = " ".repeat(Math.max(0, width - visibleWidth(truncated)));
99
+ return `${truncated}${pad}`;
100
+ }
101
+
102
+ function copyFriendlyPrompt(config: PolishedTuiConfig, uiTheme: Theme, reset: string): string {
103
+ const promptIcon = config.icons.editorPrompt;
104
+ return promptIcon
105
+ ? `${renderStyleForSourceOrFallback(
106
+ uiTheme,
107
+ config.colorSources.editor,
108
+ config.colors.editorPrompt ?? config.colors.editorAccent,
109
+ EDITOR_ACCENT_FALLBACK,
110
+ promptIcon,
111
+ )}${reset} `
112
+ : "";
113
+ }
114
+
115
+ function getEditorChromeWidths(config: PolishedTuiConfig, uiTheme: Theme, reset: string) {
116
+ const prompt = copyFriendlyPrompt(config, uiTheme, reset);
117
+ const rail = config.features.copyFriendly
118
+ ? ""
119
+ : `${renderStyleForSourceOrFallback(
120
+ uiTheme,
121
+ config.colorSources.editor,
122
+ config.colors.editorAccent,
123
+ EDITOR_ACCENT_FALLBACK,
124
+ config.icons.rail,
125
+ )}${reset} `;
126
+ return {
127
+ prompt,
128
+ promptWidth: visibleWidth(prompt),
129
+ rail,
130
+ railWidth: config.features.copyFriendly ? visibleWidth(prompt) : visibleWidth(rail),
131
+ };
132
+ }
133
+
134
+ /**
135
+ * The right side of the metadata row: vim mode, the paste-expand hint, or both.
136
+ *
137
+ * The hint used to live on the editor's bottom border, drawn there by the fixed
138
+ * editor's compositor. Pi 0.84 supersedes the fixed editor, so the hint needs a
139
+ * home that does not depend on it — and the border is not it, because
140
+ * `isHorizontalBorder` requires an unbroken rule to find the frame again. The
141
+ * metadata row is always rendered, even when `editorMetadataFormat` is blank,
142
+ * so the hint shows up whatever the user has done to that template.
143
+ */
144
+ function composeRightStatus(vimStatus: string | undefined, uiTheme: Theme): string | undefined {
145
+ // A live selection hint already spells the external editor out ("N
146
+ // characters selected, ctrl+c to copy ⋅ ctrl+g to edit in $EDITOR"), so it
147
+ // takes precedence; the always-on hint only speaks when nothing else does.
148
+ const hint = composeHints(
149
+ pasteExpandHintText(),
150
+ activeSelectionHintText() ?? externalEditorHintText(),
151
+ );
152
+ if (!hint) return vimStatus;
153
+ const styled = safeThemeFg(uiTheme, "muted", hint);
154
+ return vimStatus ? `${vimStatus} ⋅ ${styled}` : styled;
155
+ }
156
+
157
+ function composeMetadataLine(left: string, right: string | undefined, width: number): string {
158
+ if (!right) return left;
159
+ const maxWidth = Math.max(0, width);
160
+ const rightWidth = visibleWidth(right);
161
+ if (rightWidth >= maxWidth) return truncateToWidth(right, maxWidth, "");
162
+
163
+ const leftWidth = Math.max(0, maxWidth - rightWidth - 1);
164
+ const leftText = truncateToWidth(left, leftWidth, "");
165
+ const gap = " ".repeat(Math.max(1, maxWidth - visibleWidth(leftText) - rightWidth));
166
+ return `${leftText}${gap}${right}`;
167
+ }
168
+
169
+ function plainRenderedText(line: string): string {
170
+ return line
171
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
172
+ .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
173
+ .replace(/\[\/?[^\]]+\]/g, "");
174
+ }
175
+
176
+ function isHorizontalBorder(line: string): boolean {
177
+ const plain = plainRenderedText(line).trim();
178
+ return plain.length > 0 && /^─+$/.test(plain);
179
+ }
180
+
181
+ function unwrapPolishedFrameOnly(
182
+ lines: string[],
183
+ config: PolishedTuiConfig,
184
+ uiTheme: Theme,
185
+ ): string[] | undefined {
186
+ // The frame is parsed back by position, so the padding the renderer emitted has
187
+ // to be the padding assumed here.
188
+ const padY = config.editorPaddingY > 0 ? 1 : 0;
189
+ if (
190
+ lines.length < 4 + padY ||
191
+ !isHorizontalBorder(lines[0] ?? "") ||
192
+ !isHorizontalBorder(lines.at(-1) ?? "")
193
+ )
194
+ return undefined;
195
+
196
+ const interior = lines.slice(1, -1);
197
+ if (interior.length < 2 + padY) return undefined;
198
+
199
+ if (config.features.copyFriendly) {
200
+ if (
201
+ (padY > 0 && plainRenderedText(interior[0] ?? "").trim() !== "") ||
202
+ (padY > 0 && plainRenderedText(interior.at(-2) ?? "").trim() !== "") ||
203
+ !(interior.at(-1) ?? "").startsWith(" ")
204
+ )
205
+ return undefined;
206
+
207
+ const { prompt, promptWidth } = getEditorChromeWidths(config, uiTheme, "\x1b[0m");
208
+ const continuation = " ".repeat(promptWidth);
209
+ const content = interior.slice(padY, -(1 + padY));
210
+ const unwrapped: string[] = [];
211
+ for (let index = 0; index < content.length; index++) {
212
+ const prefix = index === 0 ? prompt : continuation;
213
+ const line = content[index] ?? "";
214
+ if (prefix && !line.startsWith(prefix)) return undefined;
215
+ unwrapped.push(prefix ? line.slice(prefix.length) : line);
216
+ }
217
+ return unwrapped;
218
+ }
219
+
220
+ const { rail } = getEditorChromeWidths(config, uiTheme, "\x1b[0m");
221
+ if (!rail || interior.some((line) => !line.startsWith(rail))) return undefined;
222
+ const unrailed = interior.map((line) => line.slice(rail.length));
223
+ if (
224
+ padY > 0 &&
225
+ (plainRenderedText(unrailed[0] ?? "").trim() !== "" ||
226
+ plainRenderedText(unrailed.at(-2) ?? "").trim() !== "")
227
+ )
228
+ return undefined;
229
+ return unrailed.slice(padY, -(1 + padY));
230
+ }
231
+
232
+ function splitPolishedFrame(
233
+ lines: string[],
234
+ config: PolishedTuiConfig,
235
+ uiTheme: Theme,
236
+ ): PolishedFrameSplit | undefined {
237
+ if (!isHorizontalBorder(lines[0] ?? "")) return undefined;
238
+ for (let bottomIndex = lines.length - 1; bottomIndex >= 4; bottomIndex--) {
239
+ if (!isHorizontalBorder(lines[bottomIndex] ?? "")) continue;
240
+ const editorLines = unwrapPolishedFrameOnly(lines.slice(0, bottomIndex + 1), config, uiTheme);
241
+ if (editorLines) {
242
+ return { editorLines, trailingLines: lines.slice(bottomIndex + 1) };
243
+ }
244
+ }
245
+ return undefined;
246
+ }
247
+
248
+ function vimModeColor(mode: string): string {
249
+ switch (mode.toLowerCase()) {
250
+ case "insert":
251
+ return "success";
252
+ case "normal":
253
+ return "accent";
254
+ case "ex":
255
+ return "warning";
256
+ case "replace":
257
+ return "error";
258
+ case "visual":
259
+ return "syntaxKeyword";
260
+ default:
261
+ return "muted";
262
+ }
263
+ }
264
+
265
+ function readVimStatus(editor: WrappedEditor, uiTheme: Theme): string | undefined {
266
+ const mode = editor.getMode?.();
267
+ if (typeof mode !== "string") return undefined;
268
+ const normalized = mode.trim();
269
+ if (!normalized) return undefined;
270
+ const label = `${normalized.toUpperCase()} `;
271
+ return safeThemeFg(uiTheme, vimModeColor(normalized), label);
272
+ }
273
+
274
+ function renderPolishedFrame({
275
+ width,
276
+ baseRendered,
277
+ autocompleteSource,
278
+ uiTheme,
279
+ config,
280
+ modelMeta,
281
+ thinkingLevel,
282
+ rightStatus,
283
+ splitBaseFrame,
284
+ }: PolishedFrameOptions): string[] {
285
+ if (width <= 2) return clampRenderedLines(baseRendered, width);
286
+
287
+ const reset = "\x1b[0m";
288
+ const colorSource = config.colorSources.editor;
289
+ const { prompt, promptWidth, rail, railWidth } = getEditorChromeWidths(config, uiTheme, reset);
290
+ const innerWidth = Math.max(0, width - railWidth);
291
+ const copyFriendlyContinuation = " ".repeat(promptWidth);
292
+ const isShowingAutocomplete =
293
+ typeof autocompleteSource.isShowingAutocomplete === "function"
294
+ ? Boolean(autocompleteSource.isShowingAutocomplete())
295
+ : false;
296
+
297
+ if (baseRendered.length < 2) return clampRenderedLines(baseRendered, width);
298
+
299
+ const ownedFrame = splitBaseFrame?.(baseRendered);
300
+ const { autocompleteList } = autocompleteSource;
301
+ const autocompleteCount =
302
+ !ownedFrame && isShowingAutocomplete && typeof autocompleteList?.render === "function"
303
+ ? autocompleteList.render(innerWidth).length
304
+ : 0;
305
+ const editorFrame =
306
+ !ownedFrame && autocompleteCount > 0 && autocompleteCount < baseRendered.length
307
+ ? baseRendered.slice(0, -autocompleteCount)
308
+ : baseRendered;
309
+ const autocompleteLines = ownedFrame
310
+ ? ownedFrame.trailingLines
311
+ : autocompleteCount > 0 && autocompleteCount < baseRendered.length
312
+ ? baseRendered.slice(-autocompleteCount)
313
+ : [];
314
+ if (editorFrame.length < 2) return clampRenderedLines(baseRendered, width);
315
+
316
+ const editorLines = applyEditorCursorStyleToLines(
317
+ ownedFrame?.editorLines ?? editorFrame.slice(1, -1),
318
+ config.editorCursor,
319
+ );
320
+ const meta = renderEditorMetadataFormat(
321
+ config.editorMetadataFormat,
322
+ {
323
+ model: modelMeta.modelLabel,
324
+ modelId: modelMeta.modelId ?? "",
325
+ modelName: modelMeta.modelName ?? "",
326
+ provider: modelMeta.providerLabel,
327
+ thinking: thinkingLevel ?? "",
328
+ sessionName: modelMeta.sessionName ?? "",
329
+ },
330
+ uiTheme,
331
+ config,
332
+ );
333
+ const status = composeRightStatus(rightStatus, uiTheme);
334
+ const copyFriendlyMeta = composeMetadataLine(meta, status, Math.max(0, width - 1));
335
+ const railedMeta = composeMetadataLine(meta, status, innerWidth);
336
+
337
+ const top = renderStyleForSourceOrFallback(
338
+ uiTheme,
339
+ colorSource,
340
+ config.colors.editorBorder,
341
+ EDITOR_BORDER_FALLBACK,
342
+ "─".repeat(width),
343
+ );
344
+ const bottom = renderStyleForSourceOrFallback(
345
+ uiTheme,
346
+ colorSource,
347
+ config.colors.editorBorder,
348
+ EDITOR_BORDER_FALLBACK,
349
+ "─".repeat(width),
350
+ );
351
+ // The metadata row stays even when it renders blank: splitPolishedFrame parses
352
+ // the frame back by position, so changing the line count breaks the wrapped
353
+ // editor path. An empty editorMetadataFormat leaves a blank row, not no row.
354
+ const padRows = config.editorPaddingY > 0 ? [""] : [];
355
+ const lines = [...padRows, ...editorLines, ...padRows, railedMeta];
356
+ const renderedLines = config.features.copyFriendly
357
+ ? [
358
+ top,
359
+ ...padRows,
360
+ ...editorLines.map(
361
+ (line, index) =>
362
+ `${index === 0 ? prompt : copyFriendlyContinuation}${fillLine(line, innerWidth)}`,
363
+ ),
364
+ ...padRows,
365
+ ` ${truncateToWidth(copyFriendlyMeta, Math.max(0, width - 1), "")}`,
366
+ bottom,
367
+ ...autocompleteLines,
368
+ ]
369
+ : [
370
+ top,
371
+ ...lines.map((line) => `${rail}${fillLine(line, innerWidth)}`),
372
+ bottom,
373
+ ...autocompleteLines,
374
+ ];
375
+
376
+ return clampRenderedLines(renderedLines, width);
377
+ }
378
+
379
+ export class PolishedEditor extends CustomEditor {
380
+ private readonly getModelMeta: () => EditorMeta;
381
+ private readonly getThinkingLevel: () => string | undefined;
382
+ private readonly getConfig: () => PolishedTuiConfig;
383
+ private readonly uiTheme: Theme;
384
+
385
+ constructor(
386
+ tui: TUI,
387
+ theme: EditorTheme,
388
+ keybindings: KeybindingsManager,
389
+ uiTheme: Theme,
390
+ getConfig: () => PolishedTuiConfig,
391
+ getModelMeta: () => EditorMeta,
392
+ getThinkingLevel: () => string | undefined,
393
+ ) {
394
+ super(tui, theme, keybindings, { paddingX: 0 });
395
+ this.borderColor = (text: string) => safeThemeFg(uiTheme, "border", text);
396
+ this.uiTheme = uiTheme;
397
+ this.getConfig = getConfig;
398
+ this.getModelMeta = getModelMeta;
399
+ this.getThinkingLevel = getThinkingLevel;
400
+ }
401
+
402
+ render(width: number): string[] {
403
+ if (width <= 2) {
404
+ return clampRenderedLines(super.render(width), width);
405
+ }
406
+
407
+ const config = this.getConfig();
408
+ const { railWidth } = getEditorChromeWidths(config, this.uiTheme, "\x1b[0m");
409
+ const innerWidth = Math.max(0, width - railWidth);
410
+ const rendered = super.render(innerWidth);
411
+ const modelMeta = this.getModelMeta();
412
+ const result = renderPolishedFrame({
413
+ width,
414
+ baseRendered: rendered,
415
+ autocompleteSource: this as unknown as AutocompleteEditorInternals,
416
+ uiTheme: this.uiTheme,
417
+ config,
418
+ modelMeta,
419
+ thinkingLevel: this.getThinkingLevel(),
420
+ });
421
+ return result;
422
+ }
423
+
424
+ [SPLIT_POLISHED_FRAME](lines: string[]): PolishedFrameSplit | undefined {
425
+ return splitPolishedFrame(lines, this.getConfig(), this.uiTheme);
426
+ }
427
+ }
428
+ // Also resolvable under the pre-rename key, so a pi-zentui wrapper reading a
429
+ // pi-starline-built base still finds the splitter.
430
+ (PolishedEditor.prototype as unknown as Record<PropertyKey, unknown>)[LEGACY_SPLIT_POLISHED_FRAME] =
431
+ PolishedEditor.prototype[SPLIT_POLISHED_FRAME];
432
+
433
+ export class WrappedPolishedEditor implements EditorComponent {
434
+ constructor(
435
+ private readonly base: WrappedEditor,
436
+ private readonly uiTheme: Theme,
437
+ private readonly getConfig: () => PolishedTuiConfig,
438
+ private readonly getModelMeta: () => EditorMeta,
439
+ private readonly getThinkingLevel: () => string | undefined,
440
+ ) {}
441
+
442
+ get focused(): boolean {
443
+ return Boolean(this.base.focused);
444
+ }
445
+ set focused(value: boolean) {
446
+ this.base.focused = value;
447
+ }
448
+
449
+ get borderColor(): ((str: string) => string) | undefined {
450
+ return this.base.borderColor;
451
+ }
452
+ set borderColor(value: ((str: string) => string) | undefined) {
453
+ this.base.borderColor = value;
454
+ }
455
+
456
+ get onSubmit(): ((text: string) => void) | undefined {
457
+ return this.base.onSubmit;
458
+ }
459
+ set onSubmit(value: ((text: string) => void) | undefined) {
460
+ this.base.onSubmit = value;
461
+ }
462
+
463
+ get onChange(): ((text: string) => void) | undefined {
464
+ return this.base.onChange;
465
+ }
466
+ set onChange(value: ((text: string) => void) | undefined) {
467
+ this.base.onChange = value;
468
+ }
469
+
470
+ get onEscape(): (() => void) | undefined {
471
+ return this.base.onEscape;
472
+ }
473
+ set onEscape(value: (() => void) | undefined) {
474
+ this.base.onEscape = value;
475
+ }
476
+
477
+ get onCtrlD(): (() => void) | undefined {
478
+ return this.base.onCtrlD;
479
+ }
480
+ set onCtrlD(value: (() => void) | undefined) {
481
+ this.base.onCtrlD = value;
482
+ }
483
+
484
+ get onPasteImage(): (() => void) | undefined {
485
+ return this.base.onPasteImage;
486
+ }
487
+ set onPasteImage(value: (() => void) | undefined) {
488
+ this.base.onPasteImage = value;
489
+ }
490
+
491
+ get onExtensionShortcut(): ((data: string) => boolean) | undefined {
492
+ return this.base.onExtensionShortcut;
493
+ }
494
+ set onExtensionShortcut(value: ((data: string) => boolean) | undefined) {
495
+ this.base.onExtensionShortcut = value;
496
+ }
497
+
498
+ get actionHandlers(): Map<unknown, () => void> | undefined {
499
+ return this.base.actionHandlers;
500
+ }
501
+ set actionHandlers(value: Map<unknown, () => void> | undefined) {
502
+ this.base.actionHandlers = value;
503
+ }
504
+
505
+ get wantsKeyRelease(): boolean | undefined {
506
+ return this.base.wantsKeyRelease;
507
+ }
508
+ set wantsKeyRelease(value: boolean | undefined) {
509
+ this.base.wantsKeyRelease = value;
510
+ }
511
+
512
+ get disableSubmit(): boolean | undefined {
513
+ return this.base.disableSubmit;
514
+ }
515
+ set disableSubmit(value: boolean | undefined) {
516
+ this.base.disableSubmit = value;
517
+ }
518
+
519
+ render(width: number): string[] {
520
+ if (width <= 2) return clampRenderedLines(this.base.render(width), width);
521
+
522
+ const config = this.getConfig();
523
+ const { railWidth } = getEditorChromeWidths(config, this.uiTheme, "\x1b[0m");
524
+ const innerWidth = Math.max(0, width - railWidth);
525
+ const rendered = this.base.render(innerWidth);
526
+ const vimStatus = readVimStatus(this.base, this.uiTheme);
527
+ const modelMeta = this.getModelMeta();
528
+ return renderPolishedFrame({
529
+ width,
530
+ baseRendered: rendered,
531
+ autocompleteSource: this.base,
532
+ uiTheme: this.uiTheme,
533
+ config,
534
+ modelMeta,
535
+ thinkingLevel: this.getThinkingLevel(),
536
+ rightStatus: vimStatus,
537
+ splitBaseFrame: (
538
+ this.base[SPLIT_POLISHED_FRAME] ?? this.base[LEGACY_SPLIT_POLISHED_FRAME]
539
+ )?.bind(this.base),
540
+ });
541
+ }
542
+
543
+ [SPLIT_POLISHED_FRAME](lines: string[]): PolishedFrameSplit | undefined {
544
+ return splitPolishedFrame(lines, this.getConfig(), this.uiTheme);
545
+ }
546
+
547
+ invalidate(): void {
548
+ this.base.invalidate?.();
549
+ }
550
+
551
+ handleInput(data: string): void {
552
+ this.base.handleInput(data);
553
+ }
554
+
555
+ getText(): string {
556
+ return this.base.getText();
557
+ }
558
+
559
+ setText(text: string): void {
560
+ this.base.setText(text);
561
+ }
562
+
563
+ addToHistory(text: string): void {
564
+ this.base.addToHistory?.(text);
565
+ }
566
+
567
+ insertTextAtCursor(text: string): void {
568
+ this.base.insertTextAtCursor?.(text);
569
+ }
570
+
571
+ getExpandedText(): string {
572
+ return this.base.getExpandedText?.() ?? this.base.getText();
573
+ }
574
+
575
+ setAutocompleteProvider(provider: AutocompleteProvider): void {
576
+ this.base.setAutocompleteProvider?.(provider);
577
+ }
578
+
579
+ setPaddingX(padding: number): void {
580
+ this.base.setPaddingX?.(padding);
581
+ }
582
+
583
+ setAutocompleteMaxVisible(maxVisible: number): void {
584
+ this.base.setAutocompleteMaxVisible?.(maxVisible);
585
+ }
586
+
587
+ getLines(): string[] {
588
+ return this.base.getLines?.() ?? this.base.getText().split("\n");
589
+ }
590
+
591
+ getCursor(): unknown {
592
+ return this.base.getCursor?.();
593
+ }
594
+
595
+ getMode(): unknown {
596
+ return this.base.getMode?.();
597
+ }
598
+
599
+ getPaddingX(): number | undefined {
600
+ return this.base.getPaddingX?.();
601
+ }
602
+
603
+ getAutocompleteMaxVisible(): number | undefined {
604
+ return this.base.getAutocompleteMaxVisible?.();
605
+ }
606
+ }
607
+ // Also resolvable under the pre-rename key, so a pi-zentui wrapper reading a
608
+ // pi-starline-built base still finds the splitter.
609
+ (WrappedPolishedEditor.prototype as unknown as Record<PropertyKey, unknown>)[
610
+ LEGACY_SPLIT_POLISHED_FRAME
611
+ ] = WrappedPolishedEditor.prototype[SPLIT_POLISHED_FRAME];