@quandev104/pi-style 0.2.0 → 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.
@@ -96,6 +96,7 @@ export interface PiStyleApp {
96
96
  update(
97
97
  values: import("../domain/status.js").StatusSnapshot,
98
98
  kind?: import("./render-scheduler.js").UpdateClass,
99
+ options?: import("./runtime.js").RuntimeUpdateOptions,
99
100
  ): void;
100
101
  }
101
102
 
@@ -268,10 +269,10 @@ export function createPiStyleApp(
268
269
  sessionShutdown() {
269
270
  runtime.stop();
270
271
  },
271
- update(values, kind = "coalesced") {
272
+ update(values, kind = "coalesced", options) {
272
273
  const active = runtime.current;
273
274
  if (!active) return;
274
- active.update(values);
275
+ if (!active.update(values, options)) return;
275
276
  active.scheduler.schedule(kind);
276
277
  },
277
278
  reload() {
@@ -2,7 +2,7 @@ import type { ExtensionContext, ExtensionUIContext } from "@earendil-works/pi-co
2
2
  import { diffConfig } from "../domain/config-diff.js";
3
3
  import type { NormalizedPiStyleConfig } from "../domain/config-types.js";
4
4
  import type { GitCommandRunner } from "../domain/providers.js";
5
- import type { StatusSnapshot } from "../domain/status.js";
5
+ import type { ContextSnapshot, StatusSnapshot } from "../domain/status.js";
6
6
  import { normalizeThinkingLevel } from "../domain/status.js";
7
7
  import { installEditor } from "../features/editor/index.js";
8
8
  import {
@@ -24,6 +24,11 @@ export interface RuntimeInstallationState {
24
24
  readonly startup: "installed" | "disabled" | "failed";
25
25
  }
26
26
 
27
+ export interface RuntimeUpdateOptions {
28
+ readonly refreshContextUsage?: boolean;
29
+ readonly refreshExtensionStatuses?: boolean;
30
+ }
31
+
27
32
  export interface PiStyleRuntime {
28
33
  generation: number;
29
34
  readonly providerIdentity: { git: object; context: object; usage: object };
@@ -34,8 +39,8 @@ export interface PiStyleRuntime {
34
39
  snapshot: UiSnapshot;
35
40
  disposables: DisposableStore;
36
41
  scheduler: RenderScheduler;
37
- update(values: StatusSnapshot): void;
38
- updateStartupResources(resources: StartupResources): void;
42
+ update(values: StatusSnapshot, options?: RuntimeUpdateOptions): boolean;
43
+ updateStartupResources(resources: StartupResources): boolean;
39
44
  dismissStartup(): void;
40
45
  invalidateGit(): void;
41
46
  configure(config: NormalizedPiStyleConfig): void;
@@ -64,7 +69,7 @@ export interface RuntimeHost {
64
69
  export function createPiStyleRuntime(
65
70
  host: RuntimeHost,
66
71
  generation: number,
67
- requestRender: () => void = () => {},
72
+ requestRender: () => void = host.requestRender ?? (() => {}),
68
73
  ): PiStyleRuntime {
69
74
  let disposed = false;
70
75
  const extensionStatuses = (): readonly import("../domain/status.js").ExtensionStatus[] | undefined => {
@@ -76,13 +81,22 @@ export function createPiStyleRuntime(
76
81
  return [];
77
82
  }
78
83
  };
84
+ const contextSnapshot = (): ContextSnapshot | undefined => {
85
+ const usage = host.getContextUsage?.();
86
+ if (!usage) return undefined;
87
+ return {
88
+ ...(usage.tokens !== null ? { currentTokens: usage.tokens } : {}),
89
+ windowTokens: usage.contextWindow,
90
+ ...(usage.percent !== null ? { percent: usage.percent } : {}),
91
+ };
92
+ };
79
93
  let currentConfig = host.config;
80
94
  const disposables = new DisposableStore();
81
95
  const scheduler = new RenderScheduler({ requestRender }, generation, () => !disposed);
82
96
  const git = new CachedGitProvider(host.gitRunner);
83
97
  const contextProvider = new InMemoryContextProvider();
84
98
  const usageProvider = new InMemoryUsageProvider();
85
- const usage = host.getContextUsage?.();
99
+ const initialContext = contextSnapshot();
86
100
  const initialStatuses = extensionStatuses();
87
101
  const initialValues = {
88
102
  ...(initialStatuses ? { extensionStatuses: initialStatuses } : {}),
@@ -91,15 +105,7 @@ export function createPiStyleRuntime(
91
105
  ...(host.model?.reasoning !== undefined ? { reasoning: host.model.reasoning } : {}),
92
106
  ...(host.thinkingLevel ? { thinkingLevel: normalizeThinkingLevel(host.thinkingLevel) } : {}),
93
107
  ...(host.cwd ? { cwd: host.cwd } : {}),
94
- ...(usage
95
- ? {
96
- context: {
97
- ...(usage.tokens !== null ? { currentTokens: usage.tokens } : {}),
98
- windowTokens: usage.contextWindow,
99
- ...(usage.percent !== null ? { percent: usage.percent } : {}),
100
- },
101
- }
102
- : {}),
108
+ ...(initialContext ? { context: initialContext } : {}),
103
109
  };
104
110
  let currentSnapshot = createSnapshot(generation, 0, initialValues);
105
111
  let statusLine: ReturnType<typeof installStatusLine> | undefined;
@@ -110,14 +116,41 @@ export function createPiStyleRuntime(
110
116
  editor: "disabled",
111
117
  startup: "disabled",
112
118
  };
113
- const startupSnapshot = (config: NormalizedPiStyleConfig): StartupSnapshot => ({
119
+ const snapshotValues = (snapshot: UiSnapshot): StatusSnapshot => {
120
+ const { generation: _generation, revision: _revision, ...values } = snapshot;
121
+ return values;
122
+ };
123
+ const withSnapshotPatch = (
124
+ patch: Partial<Record<keyof StatusSnapshot, StatusSnapshot[keyof StatusSnapshot] | undefined>>,
125
+ ): StatusSnapshot => {
126
+ const next = { ...snapshotValues(currentSnapshot) } as Record<string, unknown>;
127
+ for (const [key, value] of Object.entries(patch)) {
128
+ if (value === undefined) delete next[key];
129
+ else next[key] = value;
130
+ }
131
+ return next as StatusSnapshot;
132
+ };
133
+ const createStartupSnapshot = (
134
+ config: NormalizedPiStyleConfig,
135
+ resources: StartupResources | undefined = host.resources,
136
+ ): StartupSnapshot => ({
114
137
  ...currentSnapshot,
115
138
  reason: host.startupReason ?? "startup",
116
139
  ...(host.provider ? { startupProvider: host.provider } : {}),
117
140
  ...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
118
141
  preset: config.preset,
119
- ...(host.resources ? { resources: host.resources } : {}),
142
+ ...(resources ? { resources } : {}),
120
143
  });
144
+ const applySnapshot = (nextSnapshot: UiSnapshot): boolean => {
145
+ if (nextSnapshot === currentSnapshot) return false;
146
+ currentSnapshot = nextSnapshot;
147
+ statusLine?.update(currentSnapshot);
148
+ editor?.update(currentSnapshot);
149
+ startup?.update(createStartupSnapshot(currentConfig));
150
+ return true;
151
+ };
152
+ const updateSnapshot = (values: StatusSnapshot): boolean =>
153
+ applySnapshot(replaceSnapshot(currentSnapshot, generation, values));
121
154
  const installStatus = () => {
122
155
  if (!host.hasUI || host.mode !== "tui" || !host.ui || !currentConfig.enabled || !currentConfig.statusLine.enabled) {
123
156
  installationState = { ...installationState, status: "disabled" };
@@ -186,7 +219,7 @@ export function createPiStyleRuntime(
186
219
  startup = installStartup({
187
220
  host: { ...(host.ui as unknown as StartupHost), mode: host.mode, hasUI: host.hasUI },
188
221
  config: currentConfig,
189
- snapshot: startupSnapshot(currentConfig),
222
+ snapshot: createStartupSnapshot(currentConfig),
190
223
  generation,
191
224
  requestRender,
192
225
  timeoutMs: 3000,
@@ -235,27 +268,10 @@ export function createPiStyleRuntime(
235
268
  if (host.cwd && currentConfig.enabled && currentConfig.statusLine.enabled) {
236
269
  void git.get(host.cwd).then((value) => {
237
270
  if (disposed) return;
238
- currentSnapshot = replaceSnapshot(currentSnapshot, generation, { ...currentSnapshot, git: value });
239
- statusLine?.update(currentSnapshot);
240
- editor?.update(currentSnapshot);
241
- startup?.update({
242
- ...currentSnapshot,
243
- reason: host.startupReason ?? "startup",
244
- ...(host.provider ? { startupProvider: host.provider } : {}),
245
- ...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
246
- preset: currentConfig.preset,
247
- ...(host.resources ? { resources: host.resources } : {}),
248
- });
249
- requestRender();
250
- });
251
- }
252
- if (usage) {
253
- contextProvider.set("active", {
254
- ...(usage.tokens !== null ? { currentTokens: usage.tokens } : {}),
255
- windowTokens: usage.contextWindow,
256
- ...(usage.percent !== null ? { percent: usage.percent } : {}),
271
+ if (updateSnapshot(withSnapshotPatch({ git: value }))) requestRender();
257
272
  });
258
273
  }
274
+ if (initialContext) contextProvider.set("active", initialContext);
259
275
  return {
260
276
  generation,
261
277
  providerIdentity: { git, context: contextProvider, usage: usageProvider },
@@ -264,51 +280,33 @@ export function createPiStyleRuntime(
264
280
  },
265
281
  mode: host.mode,
266
282
  hasUI: host.hasUI,
267
- snapshot: currentSnapshot,
283
+ get snapshot() {
284
+ return currentSnapshot;
285
+ },
268
286
  disposables,
269
287
  scheduler,
270
288
  updateStartupResources(resources) {
271
- if (disposed) return;
272
- startup?.update({
273
- ...currentSnapshot,
274
- reason: host.startupReason ?? "startup",
275
- ...(host.provider ? { startupProvider: host.provider } : {}),
276
- ...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
277
- preset: currentConfig.preset,
278
- resources,
279
- });
289
+ if (disposed || !startup) return false;
290
+ startup.update(createStartupSnapshot(currentConfig, resources));
280
291
  requestRender();
292
+ return true;
281
293
  },
282
294
  dismissStartup() {
283
295
  startup?.dismiss();
284
296
  },
285
- update(values) {
286
- if (disposed) return;
287
- const liveUsage = host.getContextUsage?.();
288
- const context = liveUsage
289
- ? {
290
- ...(liveUsage.tokens !== null ? { currentTokens: liveUsage.tokens } : {}),
291
- windowTokens: liveUsage.contextWindow,
292
- ...(liveUsage.percent !== null ? { percent: liveUsage.percent } : {}),
293
- }
294
- : undefined;
295
- const statuses = extensionStatuses();
296
- currentSnapshot = replaceSnapshot(currentSnapshot, generation, {
297
- ...currentSnapshot,
298
- ...values,
299
- ...(context ? { context } : {}),
300
- ...(statuses ? { extensionStatuses: statuses } : {}),
301
- });
302
- statusLine?.update(currentSnapshot);
303
- editor?.update(currentSnapshot);
304
- startup?.update({
305
- ...currentSnapshot,
306
- reason: host.startupReason ?? "startup",
307
- ...(host.provider ? { startupProvider: host.provider } : {}),
308
- ...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
309
- preset: currentConfig.preset,
310
- ...(host.resources ? { resources: host.resources } : {}),
311
- });
297
+ update(values, options = {}) {
298
+ if (disposed) return false;
299
+ const patch: Partial<Record<keyof StatusSnapshot, StatusSnapshot[keyof StatusSnapshot] | undefined>> = {
300
+ ...(values as Partial<Record<keyof StatusSnapshot, StatusSnapshot[keyof StatusSnapshot] | undefined>>),
301
+ };
302
+ if (options.refreshContextUsage) {
303
+ const context = contextSnapshot();
304
+ patch.context = context;
305
+ if (context) contextProvider.set("active", context);
306
+ else contextProvider.clear();
307
+ }
308
+ if (options.refreshExtensionStatuses) patch.extensionStatuses = extensionStatuses();
309
+ return updateSnapshot(withSnapshotPatch(patch));
312
310
  },
313
311
  configure(nextConfig) {
314
312
  if (disposed) return;
@@ -350,17 +348,7 @@ export function createPiStyleRuntime(
350
348
  git.invalidate(host.cwd);
351
349
  void git.get(host.cwd).then((value) => {
352
350
  if (disposed) return;
353
- currentSnapshot = replaceSnapshot(currentSnapshot, generation, { ...currentSnapshot, git: value });
354
- statusLine?.update(currentSnapshot);
355
- editor?.update(currentSnapshot);
356
- startup?.update({
357
- ...currentSnapshot,
358
- reason: host.startupReason ?? "startup",
359
- ...(host.provider ? { startupProvider: host.provider } : {}),
360
- ...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
361
- preset: currentConfig.preset,
362
- });
363
- requestRender();
351
+ if (updateSnapshot(withSnapshotPatch({ git: value }))) requestRender();
364
352
  });
365
353
  },
366
354
  get disposed() {
@@ -6,10 +6,49 @@ export interface UiSnapshot extends StatusSnapshot {
6
6
  }
7
7
 
8
8
  export function createSnapshot(generation: number, revision = 0, values: StatusSnapshot = {}): UiSnapshot {
9
- return Object.freeze({ generation, revision, ...values });
9
+ return Object.freeze({ ...values, generation, revision });
10
10
  }
11
11
 
12
12
  export function replaceSnapshot(current: UiSnapshot, generation: number, values: StatusSnapshot): UiSnapshot {
13
13
  if (current.generation !== generation) return current;
14
- return createSnapshot(generation, current.revision + 1, values);
14
+ return equalStatusSnapshot(current, values) ? current : createSnapshot(generation, current.revision + 1, values);
15
+ }
16
+
17
+ function equalStatusSnapshot(current: StatusSnapshot, next: StatusSnapshot): boolean {
18
+ const currentEntries = Object.entries(current).filter(([key]) => key !== "generation" && key !== "revision");
19
+ const nextEntries = Object.entries(next).filter(([key]) => key !== "generation" && key !== "revision");
20
+ if (currentEntries.length !== nextEntries.length) return false;
21
+ for (const [key, value] of nextEntries) {
22
+ if (!hasOwn(current, key)) return false;
23
+ if (!equalValue((current as Record<string, unknown>)[key], value)) return false;
24
+ }
25
+ return true;
26
+ }
27
+
28
+ function equalValue(left: unknown, right: unknown): boolean {
29
+ if (Object.is(left, right)) return true;
30
+ if (Array.isArray(left) && Array.isArray(right)) {
31
+ if (left.length !== right.length) return false;
32
+ for (let index = 0; index < left.length; index++) {
33
+ if (!equalValue(left[index], right[index])) return false;
34
+ }
35
+ return true;
36
+ }
37
+ if (!isPlainRecord(left) || !isPlainRecord(right)) return false;
38
+ const leftKeys = Object.keys(left);
39
+ const rightKeys = Object.keys(right);
40
+ if (leftKeys.length !== rightKeys.length) return false;
41
+ for (const key of rightKeys) {
42
+ if (!hasOwn(left, key)) return false;
43
+ if (!equalValue(left[key], right[key])) return false;
44
+ }
45
+ return true;
46
+ }
47
+
48
+ function hasOwn(value: object, key: string): boolean {
49
+ return Object.hasOwn(value, key);
50
+ }
51
+
52
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
53
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
54
  }
@@ -39,6 +39,19 @@ interface EditorOptions {
39
39
  onSnapshot: (snapshot: StatusSnapshot) => void;
40
40
  }
41
41
 
42
+ interface RenderPlan {
43
+ readonly style: "compact" | "boxed" | "dock" | "native";
44
+ readonly kind: "compact" | "boxed" | "outline" | "rounded" | "native";
45
+ readonly prompt: string;
46
+ readonly promptWidth: number;
47
+ readonly padding: number;
48
+ readonly sideReserve: number;
49
+ readonly renderWidth: number;
50
+ readonly innerWidth: number;
51
+ readonly prefix: string;
52
+ readonly continuation: string;
53
+ }
54
+
42
55
  const widthOf = visibleWidth;
43
56
 
44
57
  function widthSafe(value: string, width: number): string {
@@ -130,6 +143,7 @@ export class StyledEditor extends CustomEditor implements EditorComponent {
130
143
  private readonly onSnapshot: (snapshot: StatusSnapshot) => void;
131
144
  private semantic: ResolvedTheme;
132
145
  private disposed = false;
146
+ private renderPlanCache: { key: string; plan: RenderPlan } | undefined;
133
147
 
134
148
  constructor(tui: Tui, theme: PiEditorTheme, keybindings: Keybindings, options: EditorOptions) {
135
149
  super(tui, theme, keybindings);
@@ -164,88 +178,37 @@ export class StyledEditor extends CustomEditor implements EditorComponent {
164
178
  override invalidate(): void {
165
179
  super.invalidate();
166
180
  this.semantic = semanticTheme(this.piTheme, this.config);
181
+ this.renderPlanCache = undefined;
167
182
  this.tui.requestRender();
168
183
  }
169
184
 
170
185
  override render(width: number): string[] {
171
186
  if (width <= 0) return [];
172
- const nativeLines = super.render(width);
173
- const style = this.styleFor(width);
174
- // Autocomplete (slash menu / @-mentions) restructure: Pi draws the
175
- // suggestions after its own bottom border, which pushes the below-editor
176
- // widgets (status line) down. Re-frame the native output so the dropdown
177
- // lives INSIDE the input box, keeping the footer directly below the input.
178
- // Native layout: [top border, text lines, bottom border, dropdown lines...].
179
- if ((this as unknown as { autocompleteState?: unknown }).autocompleteState) {
180
- const prompt = this.prompt();
181
- const padding = this.paddingFor(width, style);
182
- const promptWidth = widthOf(prompt) + 1;
183
- const prefix = `${" ".repeat(padding)}${prompt} `;
184
- const continuation = " ".repeat(padding + promptWidth);
185
- const borderIndex = nativeLines.slice(1).findIndex((line) => isNativeBorderLine(line));
186
- const split = borderIndex >= 0 ? borderIndex + 1 : nativeLines.length;
187
- const body = nativeLines.slice(1, split);
188
- const dropdown = nativeLines.slice(split);
189
- const border = this.borderFor();
190
- const kind = this.frameKind(style);
191
- const renderWidth = width - (kind === "rounded" ? 2 : 0);
192
- const sideColor = kind === "rounded" ? this.borderColorFor() : undefined;
193
- const wrap = (line: string) =>
194
- kind === "rounded" && sideColor ? `${sideColor("│")}${line}${sideColor("│")}` : line;
195
- const bashHidden = this.bashHiddenCount();
196
- const renderedBody = body.map((line, index) => {
197
- const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
198
- return wrap(widthSafe(`${index === 0 ? prefix : continuation}${source}`, renderWidth));
199
- });
200
- const dropdownLines = dropdown.map((line) => wrap(widthSafe(line, renderWidth)));
201
- if (kind === "rounded") {
202
- return [
203
- border(`╭${"─".repeat(Math.max(0, width - 2))}╮`),
204
- ...renderedBody,
205
- ...dropdownLines,
206
- border(`╰${"─".repeat(Math.max(0, width - 2))}╯`),
207
- ];
208
- }
209
- return [border("─".repeat(width)), ...renderedBody, ...dropdownLines, border("─".repeat(width))];
210
- }
211
- if (style === "native") return nativeLines.map((line) => widthSafe(line, width));
187
+ const plan = this.renderPlan(width);
188
+ const autocompleteState = (this as unknown as { autocompleteState?: unknown }).autocompleteState;
189
+ if (plan.style === "native") return super.render(width).map((line) => widthSafe(line, width));
190
+ if (autocompleteState) return this.renderAutocompleteFrame(width, plan);
212
191
 
213
- const prompt = this.prompt();
214
- const promptWidth = widthOf(prompt) + 1;
215
- const padding = this.paddingFor(width, style);
216
- const kind = this.frameKind(style);
217
- const sideReserve = kind === "rounded" ? 2 : 0;
218
- const renderWidth = Math.max(1, width - sideReserve);
219
- const innerWidth = Math.max(1, renderWidth - promptWidth - padding * 2);
220
- const innerLines = super.render(innerWidth);
192
+ const innerLines = super.render(plan.innerWidth);
221
193
  if (innerLines.length === 0) return [];
222
-
223
194
  const body = innerLines.slice(1, -1);
224
- const prefix = `${" ".repeat(padding)}${prompt} `;
225
- const continuation = " ".repeat(padding + promptWidth);
226
195
  const hint = this.config.editor.hint;
227
196
  const showHint = hint !== "" && this.getText() === "";
228
197
  const bashHidden = this.bashHiddenCount();
229
198
  const renderedBody = body.map((line, index) => {
230
- const lead = index === 0 ? prefix : continuation;
199
+ const lead = index === 0 ? plan.prefix : plan.continuation;
231
200
  const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
232
201
  let content = `${lead}${source}`;
233
- // Empty-input hint: the cursor block (first cell of the native empty
234
- // line) stays at the input position, the dim hint trails it. The native
235
- // line is pre-padded to renderWidth with literal spaces; drop them from
236
- // the raw end (safe: no ANSI follows the padding) before appending the
237
- // hint, or the hint is truncated away by widthSafe. Typing any character
238
- // makes the text non-empty and the hint disappears.
239
202
  if (showHint && index === 0 && line) {
240
203
  let end = content.length;
241
204
  while (end > 0 && content[end - 1] === " ") end--;
242
205
  if (end < content.length) content = content.slice(0, end);
243
206
  content += this.semantic.apply("hint", hint);
244
207
  }
245
- return widthSafe(content, renderWidth);
208
+ return widthSafe(content, plan.renderWidth);
246
209
  });
247
- const metadata = this.metadata(width, style);
248
- const framed = this.frame(width, style, renderedBody, metadata);
210
+ const metadata = this.metadata(width, plan.style);
211
+ const framed = this.frame(width, plan.style, renderedBody, metadata);
249
212
  return framed.map((line) => widthSafe(line, width));
250
213
  }
251
214
 
@@ -255,6 +218,62 @@ export class StyledEditor extends CustomEditor implements EditorComponent {
255
218
  this.invalidate();
256
219
  }
257
220
 
221
+ private renderPlan(width: number): RenderPlan {
222
+ const style = this.styleFor(width);
223
+ const prompt = this.prompt();
224
+ const kind = this.frameKind(style);
225
+ const key = `${width}:${style}:${kind}:${prompt}`;
226
+ if (this.renderPlanCache?.key === key) return this.renderPlanCache.plan;
227
+ const promptWidth = widthOf(prompt) + 1;
228
+ const padding = this.paddingFor(width, style);
229
+ const sideReserve = kind === "rounded" ? 2 : 0;
230
+ const renderWidth = Math.max(1, width - sideReserve);
231
+ const innerWidth = Math.max(1, renderWidth - promptWidth - padding * 2);
232
+ const prefix = `${" ".repeat(padding)}${prompt} `;
233
+ const continuation = " ".repeat(padding + promptWidth);
234
+ const plan = {
235
+ style,
236
+ kind,
237
+ prompt,
238
+ promptWidth,
239
+ padding,
240
+ sideReserve,
241
+ renderWidth,
242
+ innerWidth,
243
+ prefix,
244
+ continuation,
245
+ };
246
+ this.renderPlanCache = { key, plan };
247
+ return plan;
248
+ }
249
+
250
+ private renderAutocompleteFrame(width: number, plan: RenderPlan): string[] {
251
+ const nativeLines = super.render(width);
252
+ const borderIndex = nativeLines.slice(1).findIndex((line) => isNativeBorderLine(line));
253
+ const split = borderIndex >= 0 ? borderIndex + 1 : nativeLines.length;
254
+ const body = nativeLines.slice(1, split);
255
+ const dropdown = nativeLines.slice(split);
256
+ const border = this.borderFor();
257
+ const sideColor = plan.kind === "rounded" ? this.borderColorFor() : undefined;
258
+ const wrap = (line: string) =>
259
+ plan.kind === "rounded" && sideColor ? `${sideColor("│")}${line}${sideColor("│")}` : line;
260
+ const bashHidden = this.bashHiddenCount();
261
+ const renderedBody = body.map((line, index) => {
262
+ const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
263
+ return wrap(widthSafe(`${index === 0 ? plan.prefix : plan.continuation}${source}`, plan.renderWidth));
264
+ });
265
+ const dropdownLines = dropdown.map((line) => wrap(widthSafe(line, plan.renderWidth)));
266
+ if (plan.kind === "rounded") {
267
+ return [
268
+ border(`╭${"─".repeat(Math.max(0, width - 2))}╮`),
269
+ ...renderedBody,
270
+ ...dropdownLines,
271
+ border(`╰${"─".repeat(Math.max(0, width - 2))}╯`),
272
+ ];
273
+ }
274
+ return [border("─".repeat(width)), ...renderedBody, ...dropdownLines, border("─".repeat(width))];
275
+ }
276
+
258
277
  private prompt(): string {
259
278
  if (this.isBashMode()) {
260
279
  // Bash mode (`!` prefix): the prompt glyph becomes the bash icon and the