pi-zentui 0.1.15 → 0.2.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.
@@ -23,7 +23,7 @@ import { readRuntimeInfo } from "./runtime";
23
23
  import { installSelectorBorderStyle } from "./selector-border";
24
24
  import { registerZentuiSettingsCommand } from "./settings-command";
25
25
  import { type FooterState, createInitialState, syncState } from "./state";
26
- import { PolishedEditor } from "./ui";
26
+ import { PolishedEditor, WrappedPolishedEditor } from "./ui";
27
27
  import { installUserMessageStyle } from "./user-message";
28
28
 
29
29
  const ZENTUI_EDITOR_FACTORY = Symbol.for("pi-zentui.editor-factory");
@@ -38,6 +38,8 @@ type ApplyUiResult = {
38
38
  editorBlocked: boolean;
39
39
  };
40
40
 
41
+ type EditorInstallMode = "none" | "standalone" | "wrapper";
42
+
41
43
  function isZentuiEditorFactory(factory: EditorFactory | undefined): boolean {
42
44
  return Boolean((factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_FACTORY]);
43
45
  }
@@ -53,6 +55,8 @@ export default function (pi: ExtensionAPI) {
53
55
  let cleanupPrototypePatches: () => void = () => {};
54
56
  let footerInstalled = false;
55
57
  let editorInstalled = false;
58
+ let editorInstallMode: EditorInstallMode = "none";
59
+ let wrappedEditorFactory: EditorFactory | undefined;
56
60
  let prototypePatchesInstalled = false;
57
61
  let projectRefreshInFlight = false;
58
62
  let projectRefreshPending = false;
@@ -139,12 +143,42 @@ export default function (pi: ExtensionAPI) {
139
143
  return factory;
140
144
  };
141
145
 
146
+ const makeWrappedEditorFactory = (
147
+ ctx: ExtensionContext,
148
+ baseFactory: EditorFactory,
149
+ ): ZentuiEditorFactory => {
150
+ const factory = ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) =>
151
+ new WrappedPolishedEditor(
152
+ baseFactory(tui, theme, keybindings),
153
+ ctx.ui.theme,
154
+ getCurrentConfig,
155
+ () => ({
156
+ modelLabel: state.modelLabel,
157
+ providerLabel: state.providerLabel,
158
+ }),
159
+ getThinkingLevel,
160
+ )) as ZentuiEditorFactory;
161
+ factory[ZENTUI_EDITOR_FACTORY] = true;
162
+ return factory;
163
+ };
164
+
142
165
  const installEditor = (ctx: ExtensionContext): boolean => {
143
166
  const currentFactory = ctx.ui.getEditorComponent();
144
- if (currentFactory && !isZentuiEditorFactory(currentFactory)) return false;
167
+ if (currentFactory && isZentuiEditorFactory(currentFactory)) {
168
+ editorInstalled = true;
169
+ return true;
170
+ }
145
171
 
146
172
  installPrototypePatches();
147
- ctx.ui.setEditorComponent(makeEditorFactory(ctx));
173
+ if (currentFactory) {
174
+ wrappedEditorFactory = currentFactory;
175
+ ctx.ui.setEditorComponent(makeWrappedEditorFactory(ctx, currentFactory));
176
+ editorInstallMode = "wrapper";
177
+ } else {
178
+ wrappedEditorFactory = undefined;
179
+ ctx.ui.setEditorComponent(makeEditorFactory(ctx));
180
+ editorInstallMode = "standalone";
181
+ }
148
182
  editorInstalled = true;
149
183
  return true;
150
184
  };
@@ -154,7 +188,11 @@ export default function (pi: ExtensionAPI) {
154
188
  if (currentFactory && !isZentuiEditorFactory(currentFactory)) return false;
155
189
 
156
190
  uninstallPrototypePatches();
157
- ctx.ui.setEditorComponent(undefined);
191
+ ctx.ui.setEditorComponent(
192
+ editorInstallMode === "wrapper" && wrappedEditorFactory ? wrappedEditorFactory : undefined,
193
+ );
194
+ wrappedEditorFactory = undefined;
195
+ editorInstallMode = "none";
158
196
  editorInstalled = false;
159
197
  return true;
160
198
  };
@@ -192,7 +230,9 @@ export default function (pi: ExtensionAPI) {
192
230
  if (!ctx.hasUI) return result;
193
231
  activeTheme = ctx.ui.theme;
194
232
  if (currentConfig.features.editor) {
195
- if (!editorInstalled) result.editorBlocked = !installEditor(ctx);
233
+ const currentFactory = ctx.ui.getEditorComponent();
234
+ const editorMissingOrReplaced = !editorInstalled || !isZentuiEditorFactory(currentFactory);
235
+ if (editorMissingOrReplaced) result.editorBlocked = !installEditor(ctx);
196
236
  } else if (editorInstalled || prototypePatchesInstalled) {
197
237
  result.editorBlocked = !uninstallEditor(ctx);
198
238
  }
@@ -219,6 +259,17 @@ export default function (pi: ExtensionAPI) {
219
259
  refresh();
220
260
  };
221
261
 
262
+ const scheduleEditorReconciliation = (ctx: ExtensionContext) => {
263
+ setTimeout(() => {
264
+ if (!ctx.hasUI || !currentConfig.features.editor) return;
265
+ const currentFactory = ctx.ui.getEditorComponent();
266
+ if (currentFactory && !isZentuiEditorFactory(currentFactory)) {
267
+ applyConfiguredUi(ctx);
268
+ refresh();
269
+ }
270
+ }, 0);
271
+ };
272
+
222
273
  const cleanupUi = (ctx?: ExtensionContext) => {
223
274
  uninstallPrototypePatches();
224
275
  stopProjectRefresh();
@@ -226,8 +277,17 @@ export default function (pi: ExtensionAPI) {
226
277
  getActiveExtensionStatuses = () => new Map();
227
278
  if (ctx?.hasUI) {
228
279
  ctx.ui.setFooter(undefined);
229
- ctx.ui.setEditorComponent(undefined);
280
+ const currentFactory = ctx.ui.getEditorComponent();
281
+ if (!currentFactory || isZentuiEditorFactory(currentFactory)) {
282
+ ctx.ui.setEditorComponent(
283
+ editorInstallMode === "wrapper" && wrappedEditorFactory
284
+ ? wrappedEditorFactory
285
+ : undefined,
286
+ );
287
+ }
230
288
  }
289
+ wrappedEditorFactory = undefined;
290
+ editorInstallMode = "none";
231
291
  footerInstalled = false;
232
292
  editorInstalled = false;
233
293
  activeTheme = undefined;
@@ -242,6 +302,7 @@ export default function (pi: ExtensionAPI) {
242
302
 
243
303
  pi.on("session_start", async (_event, ctx) => {
244
304
  installUi(ctx);
305
+ scheduleEditorReconciliation(ctx);
245
306
  });
246
307
 
247
308
  registerZentuiSettingsCommand(pi, {
@@ -1,6 +1,8 @@
1
1
  import { CustomEditor, type KeybindingsManager, type Theme } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
+ type AutocompleteProvider,
3
4
  type Component,
5
+ type EditorComponent,
4
6
  type EditorTheme,
5
7
  type TUI,
6
8
  truncateToWidth,
@@ -19,22 +21,260 @@ type AutocompleteEditorInternals = {
19
21
  isShowingAutocomplete?: () => boolean;
20
22
  };
21
23
 
24
+ type WrappedEditor = EditorComponent &
25
+ AutocompleteEditorInternals & {
26
+ focused?: boolean;
27
+ onEscape?: () => void;
28
+ onCtrlD?: () => void;
29
+ onPasteImage?: () => void;
30
+ onExtensionShortcut?: (data: string) => boolean;
31
+ actionHandlers?: Map<unknown, () => void>;
32
+ wantsKeyRelease?: boolean;
33
+ disableSubmit?: boolean;
34
+ getLines?: () => string[];
35
+ getCursor?: () => unknown;
36
+ getMode?: () => unknown;
37
+ getPaddingX?: () => number;
38
+ getAutocompleteMaxVisible?: () => number;
39
+ addToHistory?: (text: string) => void;
40
+ getExpandedText?: () => string;
41
+ insertTextAtCursor?: (text: string) => void;
42
+ setAutocompleteProvider?: (provider: AutocompleteProvider) => void;
43
+ setPaddingX?: (padding: number) => void;
44
+ setAutocompleteMaxVisible?: (maxVisible: number) => void;
45
+ };
46
+
22
47
  type EditorMeta = {
23
48
  modelLabel: string;
24
49
  providerLabel: string;
25
50
  };
26
51
 
52
+ type PolishedFrameOptions = {
53
+ width: number;
54
+ baseRendered: string[];
55
+ autocompleteSource: AutocompleteEditorInternals;
56
+ uiTheme: Theme;
57
+ config: PolishedTuiConfig;
58
+ modelMeta: EditorMeta;
59
+ thinkingLevel: string | undefined;
60
+ rightStatus?: string;
61
+ };
62
+
27
63
  function clampRenderedLines(lines: string[], width: number): string[] {
28
64
  const maxWidth = Math.max(0, width);
29
65
  return lines.map((line) => truncateToWidth(line, maxWidth, ""));
30
66
  }
31
67
 
68
+ function fillLine(content: string, width: number): string {
69
+ const truncated = truncateToWidth(content, Math.max(0, width), "");
70
+ const pad = " ".repeat(Math.max(0, width - visibleWidth(truncated)));
71
+ return `${truncated}${pad}`;
72
+ }
73
+
74
+ function editorThinkingStyle(config: PolishedTuiConfig, level: string): string | undefined {
75
+ switch (level.toLowerCase()) {
76
+ case "minimal":
77
+ return config.colors.editorThinkingMinimal ?? config.colors.editorThinking;
78
+ case "low":
79
+ return config.colors.editorThinkingLow ?? config.colors.editorThinking;
80
+ case "medium":
81
+ return config.colors.editorThinkingMedium ?? config.colors.editorThinking;
82
+ case "high":
83
+ return config.colors.editorThinkingHigh ?? config.colors.editorThinking;
84
+ case "xhigh":
85
+ return config.colors.editorThinkingXhigh ?? config.colors.editorThinking;
86
+ default:
87
+ return config.colors.editorThinking;
88
+ }
89
+ }
90
+
91
+ function copyFriendlyPrompt(config: PolishedTuiConfig, uiTheme: Theme, reset: string): string {
92
+ const promptIcon = config.icons.editorPrompt;
93
+ return promptIcon
94
+ ? `${renderStyleForSourceOrFallback(
95
+ uiTheme,
96
+ config.colorSources.editor,
97
+ config.colors.editorPrompt ?? config.colors.editorAccent,
98
+ EDITOR_ACCENT_FALLBACK,
99
+ promptIcon,
100
+ )}${reset} `
101
+ : "";
102
+ }
103
+
104
+ function getEditorChromeWidths(config: PolishedTuiConfig, uiTheme: Theme, reset: string) {
105
+ const prompt = copyFriendlyPrompt(config, uiTheme, reset);
106
+ const rail = config.features.copyFriendly
107
+ ? ""
108
+ : `${renderStyleForSourceOrFallback(
109
+ uiTheme,
110
+ config.colorSources.editor,
111
+ config.colors.editorAccent,
112
+ EDITOR_ACCENT_FALLBACK,
113
+ "│",
114
+ )}${reset} `;
115
+ return {
116
+ prompt,
117
+ promptWidth: visibleWidth(prompt),
118
+ rail,
119
+ railWidth: config.features.copyFriendly ? visibleWidth(prompt) : visibleWidth(rail),
120
+ };
121
+ }
122
+
123
+ function composeMetadataLine(left: string, right: string | undefined, width: number): string {
124
+ if (!right) return left;
125
+ const maxWidth = Math.max(0, width);
126
+ const rightWidth = visibleWidth(right);
127
+ if (rightWidth >= maxWidth) return truncateToWidth(right, maxWidth, "");
128
+
129
+ const leftWidth = Math.max(0, maxWidth - rightWidth - 1);
130
+ const leftText = truncateToWidth(left, leftWidth, "");
131
+ const gap = " ".repeat(Math.max(1, maxWidth - visibleWidth(leftText) - rightWidth));
132
+ return `${leftText}${gap}${right}`;
133
+ }
134
+
135
+ function vimModeColor(mode: string): string {
136
+ switch (mode.toLowerCase()) {
137
+ case "insert":
138
+ return "success";
139
+ case "normal":
140
+ return "accent";
141
+ case "ex":
142
+ return "warning";
143
+ case "replace":
144
+ return "error";
145
+ case "visual":
146
+ return "syntaxKeyword";
147
+ default:
148
+ return "muted";
149
+ }
150
+ }
151
+
152
+ function readVimStatus(editor: WrappedEditor, uiTheme: Theme): string | undefined {
153
+ const mode = editor.getMode?.();
154
+ if (typeof mode !== "string") return undefined;
155
+ const normalized = mode.trim();
156
+ if (!normalized) return undefined;
157
+ const label = `${normalized.toUpperCase()} `;
158
+ return safeThemeFg(uiTheme, vimModeColor(normalized), label);
159
+ }
160
+
161
+ function renderPolishedFrame({
162
+ width,
163
+ baseRendered,
164
+ autocompleteSource,
165
+ uiTheme,
166
+ config,
167
+ modelMeta,
168
+ thinkingLevel,
169
+ rightStatus,
170
+ }: PolishedFrameOptions): string[] {
171
+ if (width <= 2) return clampRenderedLines(baseRendered, width);
172
+
173
+ const reset = "\x1b[0m";
174
+ const colorSource = config.colorSources.editor;
175
+ const { prompt, promptWidth, rail, railWidth } = getEditorChromeWidths(config, uiTheme, reset);
176
+ const innerWidth = Math.max(0, width - railWidth);
177
+ const copyFriendlyContinuation = " ".repeat(promptWidth);
178
+ const isShowingAutocomplete =
179
+ typeof autocompleteSource.isShowingAutocomplete === "function"
180
+ ? Boolean(autocompleteSource.isShowingAutocomplete())
181
+ : false;
182
+
183
+ if (baseRendered.length < 2) return clampRenderedLines(baseRendered, width);
184
+
185
+ const { autocompleteList } = autocompleteSource;
186
+ const autocompleteCount =
187
+ isShowingAutocomplete && typeof autocompleteList?.render === "function"
188
+ ? autocompleteList.render(innerWidth).length
189
+ : 0;
190
+ const editorFrame =
191
+ autocompleteCount > 0 && autocompleteCount < baseRendered.length
192
+ ? baseRendered.slice(0, -autocompleteCount)
193
+ : baseRendered;
194
+ const autocompleteLines =
195
+ autocompleteCount > 0 && autocompleteCount < baseRendered.length
196
+ ? baseRendered.slice(-autocompleteCount)
197
+ : [];
198
+
199
+ if (editorFrame.length < 2) return clampRenderedLines(baseRendered, width);
200
+
201
+ const editorLines = editorFrame.slice(1, -1);
202
+ const model = renderStyleForSourceOrFallback(
203
+ uiTheme,
204
+ colorSource,
205
+ config.colors.editorModel,
206
+ EDITOR_ACCENT_FALLBACK,
207
+ modelMeta.modelLabel,
208
+ );
209
+ const provider = renderStyleForSourceOrFallback(
210
+ uiTheme,
211
+ colorSource,
212
+ config.colors.editorProvider,
213
+ "text",
214
+ modelMeta.providerLabel,
215
+ );
216
+ const renderedModelMeta = [model, provider]
217
+ .filter(Boolean)
218
+ .join(safeThemeFg(uiTheme, "borderMuted", " "));
219
+ const metaParts = [renderedModelMeta];
220
+ if (thinkingLevel && thinkingLevel !== "off") {
221
+ metaParts.push(
222
+ renderStyleForSourceOrFallback(
223
+ uiTheme,
224
+ colorSource,
225
+ editorThinkingStyle(config, thinkingLevel),
226
+ "muted",
227
+ thinkingLevel,
228
+ ),
229
+ );
230
+ }
231
+ const meta = metaParts.filter(Boolean).join(safeThemeFg(uiTheme, "border", " "));
232
+ const copyFriendlyMeta = composeMetadataLine(meta, rightStatus, Math.max(0, width - 1));
233
+ const railedMeta = composeMetadataLine(meta, rightStatus, innerWidth);
234
+
235
+ const top = renderStyleForSourceOrFallback(
236
+ uiTheme,
237
+ colorSource,
238
+ config.colors.editorBorder,
239
+ EDITOR_BORDER_FALLBACK,
240
+ "─".repeat(width),
241
+ );
242
+ const bottom = renderStyleForSourceOrFallback(
243
+ uiTheme,
244
+ colorSource,
245
+ config.colors.editorBorder,
246
+ EDITOR_BORDER_FALLBACK,
247
+ "─".repeat(width),
248
+ );
249
+ const lines = ["", ...editorLines, "", railedMeta];
250
+ const renderedLines = config.features.copyFriendly
251
+ ? [
252
+ top,
253
+ "",
254
+ ...editorLines.map(
255
+ (line, index) =>
256
+ `${index === 0 ? prompt : copyFriendlyContinuation}${fillLine(line, innerWidth)}`,
257
+ ),
258
+ "",
259
+ ` ${truncateToWidth(copyFriendlyMeta, Math.max(0, width - 1), "")}`,
260
+ bottom,
261
+ ...autocompleteLines,
262
+ ]
263
+ : [
264
+ top,
265
+ ...lines.map((line) => `${rail}${fillLine(line, innerWidth)}`),
266
+ bottom,
267
+ ...autocompleteLines,
268
+ ];
269
+
270
+ return clampRenderedLines(renderedLines, width);
271
+ }
272
+
32
273
  export class PolishedEditor extends CustomEditor {
33
274
  private readonly getModelMeta: () => EditorMeta;
34
275
  private readonly getThinkingLevel: () => string | undefined;
35
276
  private readonly getConfig: () => PolishedTuiConfig;
36
277
  private readonly uiTheme: Theme;
37
- private readonly reset = "\x1b[0m";
38
278
 
39
279
  constructor(
40
280
  tui: TUI,
@@ -53,160 +293,190 @@ export class PolishedEditor extends CustomEditor {
53
293
  this.getThinkingLevel = getThinkingLevel;
54
294
  }
55
295
 
56
- private fillLine(content: string, width: number): string {
57
- const truncated = truncateToWidth(content, Math.max(0, width), "");
58
- const pad = " ".repeat(Math.max(0, width - visibleWidth(truncated)));
59
- return `${truncated}${pad}`;
60
- }
61
-
62
- private editorThinkingStyle(config: PolishedTuiConfig, level: string): string | undefined {
63
- switch (level.toLowerCase()) {
64
- case "minimal":
65
- return config.colors.editorThinkingMinimal ?? config.colors.editorThinking;
66
- case "low":
67
- return config.colors.editorThinkingLow ?? config.colors.editorThinking;
68
- case "medium":
69
- return config.colors.editorThinkingMedium ?? config.colors.editorThinking;
70
- case "high":
71
- return config.colors.editorThinkingHigh ?? config.colors.editorThinking;
72
- case "xhigh":
73
- return config.colors.editorThinkingXhigh ?? config.colors.editorThinking;
74
- default:
75
- return config.colors.editorThinking;
76
- }
77
- }
78
-
79
296
  render(width: number): string[] {
80
297
  if (width <= 2) {
81
298
  return clampRenderedLines(super.render(width), width);
82
299
  }
83
300
 
84
301
  const config = this.getConfig();
85
- const colorSource = config.colorSources.editor;
86
- const promptIcon = config.icons.editorPrompt;
87
- const copyFriendlyPrompt = promptIcon
88
- ? `${renderStyleForSourceOrFallback(
89
- this.uiTheme,
90
- colorSource,
91
- config.colors.editorPrompt ?? config.colors.editorAccent,
92
- EDITOR_ACCENT_FALLBACK,
93
- promptIcon,
94
- )}${this.reset} `
95
- : "";
96
- const copyFriendlyPromptWidth = visibleWidth(copyFriendlyPrompt);
97
- const copyFriendlyContinuation = " ".repeat(copyFriendlyPromptWidth);
98
- const rail = config.features.copyFriendly
99
- ? ""
100
- : `${renderStyleForSourceOrFallback(
101
- this.uiTheme,
102
- colorSource,
103
- config.colors.editorAccent,
104
- EDITOR_ACCENT_FALLBACK,
105
- "│",
106
- )}${this.reset} `;
107
- const railWidth = config.features.copyFriendly ? copyFriendlyPromptWidth : visibleWidth(rail);
302
+ const { railWidth } = getEditorChromeWidths(config, this.uiTheme, "\x1b[0m");
108
303
  const innerWidth = Math.max(0, width - railWidth);
109
304
  const rendered = super.render(innerWidth);
110
- const editorInternals = this as unknown as AutocompleteEditorInternals;
111
- const isShowingAutocomplete =
112
- typeof editorInternals.isShowingAutocomplete === "function"
113
- ? Boolean(editorInternals.isShowingAutocomplete())
114
- : false;
305
+ return renderPolishedFrame({
306
+ width,
307
+ baseRendered: rendered,
308
+ autocompleteSource: this as unknown as AutocompleteEditorInternals,
309
+ uiTheme: this.uiTheme,
310
+ config,
311
+ modelMeta: this.getModelMeta(),
312
+ thinkingLevel: this.getThinkingLevel(),
313
+ });
314
+ }
315
+ }
115
316
 
116
- if (rendered.length < 2) {
117
- return clampRenderedLines(super.render(width), width);
118
- }
317
+ export class WrappedPolishedEditor implements EditorComponent {
318
+ constructor(
319
+ private readonly base: WrappedEditor,
320
+ private readonly uiTheme: Theme,
321
+ private readonly getConfig: () => PolishedTuiConfig,
322
+ private readonly getModelMeta: () => EditorMeta,
323
+ private readonly getThinkingLevel: () => string | undefined,
324
+ ) {}
119
325
 
120
- const { autocompleteList } = editorInternals;
121
- const autocompleteCount =
122
- isShowingAutocomplete && typeof autocompleteList?.render === "function"
123
- ? autocompleteList.render(innerWidth).length
124
- : 0;
125
- const editorFrame =
126
- autocompleteCount > 0 && autocompleteCount < rendered.length
127
- ? rendered.slice(0, -autocompleteCount)
128
- : rendered;
129
- const autocompleteLines =
130
- autocompleteCount > 0 && autocompleteCount < rendered.length
131
- ? rendered.slice(-autocompleteCount)
132
- : [];
133
-
134
- if (editorFrame.length < 2) {
135
- return clampRenderedLines(rendered, width);
136
- }
326
+ get focused(): boolean {
327
+ return Boolean(this.base.focused);
328
+ }
329
+ set focused(value: boolean) {
330
+ this.base.focused = value;
331
+ }
137
332
 
138
- const editorLines = editorFrame.slice(1, -1);
139
- const { modelLabel, providerLabel } = this.getModelMeta();
140
- const model = renderStyleForSourceOrFallback(
141
- this.uiTheme,
142
- colorSource,
143
- config.colors.editorModel,
144
- EDITOR_ACCENT_FALLBACK,
145
- modelLabel,
146
- );
147
- const provider = renderStyleForSourceOrFallback(
148
- this.uiTheme,
149
- colorSource,
150
- config.colors.editorProvider,
151
- "text",
152
- providerLabel,
153
- );
154
- const modelMeta = [model, provider]
155
- .filter(Boolean)
156
- .join(safeThemeFg(this.uiTheme, "borderMuted", " "));
157
- const metaParts = [modelMeta];
158
- const thinkingLevel = this.getThinkingLevel();
159
- if (thinkingLevel && thinkingLevel !== "off") {
160
- metaParts.push(
161
- renderStyleForSourceOrFallback(
162
- this.uiTheme,
163
- colorSource,
164
- this.editorThinkingStyle(config, thinkingLevel),
165
- "muted",
166
- thinkingLevel,
167
- ),
168
- );
169
- }
170
- const meta = metaParts.filter(Boolean).join(safeThemeFg(this.uiTheme, "border", " "));
171
-
172
- const top = renderStyleForSourceOrFallback(
173
- this.uiTheme,
174
- colorSource,
175
- config.colors.editorBorder,
176
- EDITOR_BORDER_FALLBACK,
177
- "─".repeat(width),
178
- );
179
- const bottom = renderStyleForSourceOrFallback(
180
- this.uiTheme,
181
- colorSource,
182
- config.colors.editorBorder,
183
- EDITOR_BORDER_FALLBACK,
184
- "─".repeat(width),
185
- );
186
- const lines = ["", ...editorLines, "", meta];
187
- const renderedLines = config.features.copyFriendly
188
- ? [
189
- top,
190
- "",
191
- ...editorLines.map(
192
- (line, index) =>
193
- `${index === 0 ? copyFriendlyPrompt : copyFriendlyContinuation}${this.fillLine(
194
- line,
195
- innerWidth,
196
- )}`,
197
- ),
198
- "",
199
- ` ${truncateToWidth(meta, Math.max(0, width - 1), "")}`,
200
- bottom,
201
- ...autocompleteLines,
202
- ]
203
- : [
204
- top,
205
- ...lines.map((line) => `${rail}${this.fillLine(line, innerWidth)}`),
206
- bottom,
207
- ...autocompleteLines,
208
- ];
209
-
210
- return clampRenderedLines(renderedLines, width);
333
+ get borderColor(): ((str: string) => string) | undefined {
334
+ return this.base.borderColor;
335
+ }
336
+ set borderColor(value: ((str: string) => string) | undefined) {
337
+ this.base.borderColor = value;
338
+ }
339
+
340
+ get onSubmit(): ((text: string) => void) | undefined {
341
+ return this.base.onSubmit;
342
+ }
343
+ set onSubmit(value: ((text: string) => void) | undefined) {
344
+ this.base.onSubmit = value;
345
+ }
346
+
347
+ get onChange(): ((text: string) => void) | undefined {
348
+ return this.base.onChange;
349
+ }
350
+ set onChange(value: ((text: string) => void) | undefined) {
351
+ this.base.onChange = value;
352
+ }
353
+
354
+ get onEscape(): (() => void) | undefined {
355
+ return this.base.onEscape;
356
+ }
357
+ set onEscape(value: (() => void) | undefined) {
358
+ this.base.onEscape = value;
359
+ }
360
+
361
+ get onCtrlD(): (() => void) | undefined {
362
+ return this.base.onCtrlD;
363
+ }
364
+ set onCtrlD(value: (() => void) | undefined) {
365
+ this.base.onCtrlD = value;
366
+ }
367
+
368
+ get onPasteImage(): (() => void) | undefined {
369
+ return this.base.onPasteImage;
370
+ }
371
+ set onPasteImage(value: (() => void) | undefined) {
372
+ this.base.onPasteImage = value;
373
+ }
374
+
375
+ get onExtensionShortcut(): ((data: string) => boolean) | undefined {
376
+ return this.base.onExtensionShortcut;
377
+ }
378
+ set onExtensionShortcut(value: ((data: string) => boolean) | undefined) {
379
+ this.base.onExtensionShortcut = value;
380
+ }
381
+
382
+ get actionHandlers(): Map<unknown, () => void> | undefined {
383
+ return this.base.actionHandlers;
384
+ }
385
+ set actionHandlers(value: Map<unknown, () => void> | undefined) {
386
+ this.base.actionHandlers = value;
387
+ }
388
+
389
+ get wantsKeyRelease(): boolean | undefined {
390
+ return this.base.wantsKeyRelease;
391
+ }
392
+ set wantsKeyRelease(value: boolean | undefined) {
393
+ this.base.wantsKeyRelease = value;
394
+ }
395
+
396
+ get disableSubmit(): boolean | undefined {
397
+ return this.base.disableSubmit;
398
+ }
399
+ set disableSubmit(value: boolean | undefined) {
400
+ this.base.disableSubmit = value;
401
+ }
402
+
403
+ render(width: number): string[] {
404
+ if (width <= 2) return clampRenderedLines(this.base.render(width), width);
405
+
406
+ const config = this.getConfig();
407
+ const { railWidth } = getEditorChromeWidths(config, this.uiTheme, "\x1b[0m");
408
+ const innerWidth = Math.max(0, width - railWidth);
409
+ const rendered = this.base.render(innerWidth);
410
+ const vimStatus = readVimStatus(this.base, this.uiTheme);
411
+ return renderPolishedFrame({
412
+ width,
413
+ baseRendered: rendered,
414
+ autocompleteSource: this.base,
415
+ uiTheme: this.uiTheme,
416
+ config,
417
+ modelMeta: this.getModelMeta(),
418
+ thinkingLevel: this.getThinkingLevel(),
419
+ rightStatus: vimStatus,
420
+ });
421
+ }
422
+
423
+ invalidate(): void {
424
+ this.base.invalidate?.();
425
+ }
426
+
427
+ handleInput(data: string): void {
428
+ this.base.handleInput(data);
429
+ }
430
+
431
+ getText(): string {
432
+ return this.base.getText();
433
+ }
434
+
435
+ setText(text: string): void {
436
+ this.base.setText(text);
437
+ }
438
+
439
+ addToHistory(text: string): void {
440
+ this.base.addToHistory?.(text);
441
+ }
442
+
443
+ insertTextAtCursor(text: string): void {
444
+ this.base.insertTextAtCursor?.(text);
445
+ }
446
+
447
+ getExpandedText(): string {
448
+ return this.base.getExpandedText?.() ?? this.base.getText();
449
+ }
450
+
451
+ setAutocompleteProvider(provider: AutocompleteProvider): void {
452
+ this.base.setAutocompleteProvider?.(provider);
453
+ }
454
+
455
+ setPaddingX(padding: number): void {
456
+ this.base.setPaddingX?.(padding);
457
+ }
458
+
459
+ setAutocompleteMaxVisible(maxVisible: number): void {
460
+ this.base.setAutocompleteMaxVisible?.(maxVisible);
461
+ }
462
+
463
+ getLines(): string[] {
464
+ return this.base.getLines?.() ?? this.base.getText().split("\n");
465
+ }
466
+
467
+ getCursor(): unknown {
468
+ return this.base.getCursor?.();
469
+ }
470
+
471
+ getMode(): unknown {
472
+ return this.base.getMode?.();
473
+ }
474
+
475
+ getPaddingX(): number | undefined {
476
+ return this.base.getPaddingX?.();
477
+ }
478
+
479
+ getAutocompleteMaxVisible(): number | undefined {
480
+ return this.base.getAutocompleteMaxVisible?.();
211
481
  }
212
482
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-zentui",
3
- "version": "0.1.15",
3
+ "version": "0.2.1",
4
4
  "description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",