pi-powerline-footer 0.2.11 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.2.12] - 2026-01-27
6
+
7
+ ### Added
8
+ - **Responsive segment layout** — Segments dynamically flow between top bar and secondary row based on terminal width
9
+ - When terminal is wide: all segments fit in top bar, secondary row hidden
10
+ - When terminal is narrow: overflow segments move to secondary row automatically
11
+
12
+ ### Changed
13
+ - **Default preset reordered** — New order: π → folder → model → think → git → context% → cache → cost
14
+ - Path now appears before model name for better visual hierarchy
15
+ - Thinking level now appears right after model name
16
+ - Added git, cache_read, and cost to primary row in default preset
17
+ - **Thinking label shortened** — `thinking:level` → `think:level` to save 3 characters
18
+
19
+ ### Fixed
20
+ - **Narrow terminal crash** — Welcome screen now gracefully skips rendering on terminals < 44 columns wide
21
+ - **Editor crash on very narrow terminals** — Falls back to original render when width < 10
22
+ - **Streaming footer crash** — Truncation now properly handles edge cases and won't render content that exceeds terminal width
23
+ - **Secondary widget crash** — Content width is now validated before rendering
24
+ - **Layout cache invalidation** — Cache now properly clears when preset changes or powerline is toggled off
25
+
26
+ ## [0.2.11] - 2026-01-26
27
+
5
28
  ### Changed
6
29
  - Added `pi` manifest to package.json for pi v0.50.0 package system compliance
7
30
  - Added `pi-package` keyword for npm discoverability
package/index.ts CHANGED
@@ -4,7 +4,7 @@ import { visibleWidth } from "@mariozechner/pi-tui";
4
4
  import { readFileSync, existsSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
 
7
- import type { SegmentContext, StatusLinePreset } from "./types.js";
7
+ import type { SegmentContext, StatusLinePreset, StatusLineSegmentId } from "./types.js";
8
8
  import { getPreset, PRESETS } from "./presets.js";
9
9
  import { getSeparator } from "./separators.js";
10
10
  import { renderSegment } from "./segments.js";
@@ -43,36 +43,122 @@ function isQuietStartup(): boolean {
43
43
  // Status Line Builder (for top border)
44
44
  // ═══════════════════════════════════════════════════════════════════════════
45
45
 
46
- /** Build just the status content (segments with separators, no borders) */
47
- function buildStatusContent(ctx: SegmentContext, presetDef: ReturnType<typeof getPreset>): string {
46
+ /** Render a single segment and return its content with width */
47
+ function renderSegmentWithWidth(
48
+ segId: StatusLineSegmentId,
49
+ ctx: SegmentContext
50
+ ): { content: string; width: number; visible: boolean } {
51
+ const rendered = renderSegment(segId, ctx);
52
+ if (!rendered.visible || !rendered.content) {
53
+ return { content: "", width: 0, visible: false };
54
+ }
55
+ return { content: rendered.content, width: visibleWidth(rendered.content), visible: true };
56
+ }
57
+
58
+ /** Build status content from a list of segment IDs */
59
+ function buildStatusContentFromSegments(
60
+ segmentIds: StatusLineSegmentId[],
61
+ ctx: SegmentContext,
62
+ presetDef: ReturnType<typeof getPreset>
63
+ ): string {
48
64
  const separatorDef = getSeparator(presetDef.separator);
49
65
  const sepAnsi = getFgAnsiCode("sep");
50
66
 
51
67
  // Collect visible segment contents
52
- const leftParts: string[] = [];
53
- for (const segId of presetDef.leftSegments) {
54
- const rendered = renderSegment(segId, ctx);
55
- if (rendered.visible && rendered.content) {
56
- leftParts.push(rendered.content);
57
- }
58
- }
59
-
60
- const rightParts: string[] = [];
61
- for (const segId of presetDef.rightSegments) {
68
+ const parts: string[] = [];
69
+ for (const segId of segmentIds) {
62
70
  const rendered = renderSegment(segId, ctx);
63
71
  if (rendered.visible && rendered.content) {
64
- rightParts.push(rendered.content);
72
+ parts.push(rendered.content);
65
73
  }
66
74
  }
67
75
 
68
- if (leftParts.length === 0 && rightParts.length === 0) {
76
+ if (parts.length === 0) {
69
77
  return "";
70
78
  }
71
79
 
72
80
  // Build content with powerline separators (no background)
73
81
  const sep = separatorDef.left;
74
- const allParts = [...leftParts, ...rightParts];
75
- return " " + allParts.join(` ${sepAnsi}${sep}${ansi.reset} `) + ansi.reset + " ";
82
+ return " " + parts.join(` ${sepAnsi}${sep}${ansi.reset} `) + ansi.reset + " ";
83
+ }
84
+
85
+ /** Build content string from pre-rendered parts */
86
+ function buildContentFromParts(
87
+ parts: string[],
88
+ presetDef: ReturnType<typeof getPreset>
89
+ ): string {
90
+ if (parts.length === 0) return "";
91
+ const separatorDef = getSeparator(presetDef.separator);
92
+ const sepAnsi = getFgAnsiCode("sep");
93
+ const sep = separatorDef.left;
94
+ return " " + parts.join(` ${sepAnsi}${sep}${ansi.reset} `) + ansi.reset + " ";
95
+ }
96
+
97
+ /**
98
+ * Responsive segment layout - fits segments into top bar, overflows to secondary row.
99
+ * When terminal is wide enough, secondary segments move up to top bar.
100
+ * When narrow, top bar segments overflow down to secondary row.
101
+ */
102
+ function computeResponsiveLayout(
103
+ ctx: SegmentContext,
104
+ presetDef: ReturnType<typeof getPreset>,
105
+ availableWidth: number
106
+ ): { topContent: string; secondaryContent: string } {
107
+ const separatorDef = getSeparator(presetDef.separator);
108
+ const sepWidth = visibleWidth(separatorDef.left) + 2; // separator + spaces around it
109
+
110
+ // Get all segments: primary first, then secondary
111
+ const primaryIds = [...presetDef.leftSegments, ...presetDef.rightSegments];
112
+ const secondaryIds = presetDef.secondarySegments ?? [];
113
+ const allSegmentIds = [...primaryIds, ...secondaryIds];
114
+
115
+ // Render all segments and get their widths
116
+ const renderedSegments: { id: StatusLineSegmentId; content: string; width: number }[] = [];
117
+ for (const segId of allSegmentIds) {
118
+ const { content, width, visible } = renderSegmentWithWidth(segId, ctx);
119
+ if (visible) {
120
+ renderedSegments.push({ id: segId, content, width });
121
+ }
122
+ }
123
+
124
+ if (renderedSegments.length === 0) {
125
+ return { topContent: "", secondaryContent: "" };
126
+ }
127
+
128
+ // Calculate how many segments fit in top bar
129
+ // Account for: leading space (1) + trailing space (1) = 2 chars overhead
130
+ const baseOverhead = 2;
131
+ let currentWidth = baseOverhead;
132
+ let topSegments: string[] = [];
133
+ let secondarySegments: string[] = [];
134
+ let overflow = false;
135
+
136
+ for (let i = 0; i < renderedSegments.length; i++) {
137
+ const seg = renderedSegments[i];
138
+ // Width needed: segment width + separator (except for first segment)
139
+ const neededWidth = seg.width + (topSegments.length > 0 ? sepWidth : 0);
140
+
141
+ if (!overflow && currentWidth + neededWidth <= availableWidth) {
142
+ // Fits in top bar
143
+ topSegments.push(seg.content);
144
+ currentWidth += neededWidth;
145
+ } else {
146
+ // Overflow to secondary row
147
+ overflow = true;
148
+ secondarySegments.push(seg.content);
149
+ }
150
+ }
151
+
152
+ return {
153
+ topContent: buildContentFromParts(topSegments, presetDef),
154
+ secondaryContent: buildContentFromParts(secondarySegments, presetDef),
155
+ };
156
+ }
157
+
158
+ /** Build primary status content (for top border) - legacy, used during streaming */
159
+ function buildStatusContent(ctx: SegmentContext, presetDef: ReturnType<typeof getPreset>): string {
160
+ const allSegments = [...presetDef.leftSegments, ...presetDef.rightSegments];
161
+ return buildStatusContentFromSegments(allSegments, ctx, presetDef);
76
162
  }
77
163
 
78
164
  // ═══════════════════════════════════════════════════════════════════════════
@@ -90,6 +176,11 @@ export default function powerlineFooter(pi: ExtensionAPI) {
90
176
  let dismissWelcomeOverlay: (() => void) | null = null; // Callback to dismiss welcome overlay
91
177
  let welcomeHeaderActive = false; // Track if welcome header should be cleared on first input
92
178
  let welcomeOverlayShouldDismiss = false; // Track early dismissal request (before overlay setup completes)
179
+
180
+ // Cache for responsive layout (shared between editor and widget for consistency)
181
+ let lastLayoutWidth = 0;
182
+ let lastLayoutResult: { topContent: string; secondaryContent: string } | null = null;
183
+ let lastLayoutTimestamp = 0;
93
184
 
94
185
  // Track session start
95
186
  pi.on("session_start", async (_event, ctx) => {
@@ -208,8 +299,11 @@ export default function powerlineFooter(pi: ExtensionAPI) {
208
299
  ctx.ui.setEditorComponent(undefined);
209
300
  ctx.ui.setFooter(undefined);
210
301
  ctx.ui.setHeader(undefined);
302
+ ctx.ui.setWidget("powerline-secondary", undefined);
211
303
  footerDataRef = null;
212
304
  tuiRef = null;
305
+ // Clear layout cache
306
+ lastLayoutResult = null;
213
307
  ctx.ui.notify("Defaults restored", "info");
214
308
  }
215
309
  return;
@@ -219,6 +313,8 @@ export default function powerlineFooter(pi: ExtensionAPI) {
219
313
  const preset = args.trim().toLowerCase() as StatusLinePreset;
220
314
  if (preset in PRESETS) {
221
315
  config.preset = preset;
316
+ // Invalidate layout cache since preset changed
317
+ lastLayoutResult = null;
222
318
  if (enabled) {
223
319
  setupCustomEditor(ctx);
224
320
  }
@@ -294,6 +390,29 @@ export default function powerlineFooter(pi: ExtensionAPI) {
294
390
  };
295
391
  }
296
392
 
393
+ /**
394
+ * Get cached responsive layout or compute fresh one.
395
+ * Layout is cached per render cycle (same width = same layout).
396
+ */
397
+ function getResponsiveLayout(width: number): { topContent: string; secondaryContent: string } {
398
+ const now = Date.now();
399
+ // Cache is valid if same width and within 50ms (same render cycle)
400
+ if (lastLayoutResult && lastLayoutWidth === width && now - lastLayoutTimestamp < 50) {
401
+ return lastLayoutResult;
402
+ }
403
+
404
+ const presetDef = getPreset(config.preset);
405
+ const segmentCtx = buildSegmentContext(currentCtx, width);
406
+ // Available width for top bar content (minus box corners: ╭─ and ─╮ = 4 chars)
407
+ const topBarAvailable = width - 4;
408
+
409
+ lastLayoutWidth = width;
410
+ lastLayoutResult = computeResponsiveLayout(segmentCtx, presetDef, topBarAvailable);
411
+ lastLayoutTimestamp = now;
412
+
413
+ return lastLayoutResult;
414
+ }
415
+
297
416
  function setupCustomEditor(ctx: any) {
298
417
  // Import CustomEditor dynamically and create wrapper
299
418
  import("@mariozechner/pi-coding-agent").then(({ CustomEditor }) => {
@@ -318,6 +437,12 @@ export default function powerlineFooter(pi: ExtensionAPI) {
318
437
  // ╰─ ─╯
319
438
  // + autocomplete items (if showing)
320
439
  editor.render = (width: number): string[] => {
440
+ // Minimum width for box layout: borders (4) + minimal content (1) = 5
441
+ // Fall back to original render on extremely narrow terminals
442
+ if (width < 10) {
443
+ return originalRender(width);
444
+ }
445
+
321
446
  const bc = (s: string) => `${getFgAnsiCode("border")}${s}${ansi.reset}`;
322
447
 
323
448
  // Box drawing chars
@@ -347,43 +472,14 @@ export default function powerlineFooter(pi: ExtensionAPI) {
347
472
  const result: string[] = [];
348
473
 
349
474
  // Top border: ╭─ status ────────────╮
350
- const presetDef = getPreset(config.preset);
351
- const segmentCtx = buildSegmentContext(currentCtx, width);
352
- const statusContent = buildStatusContent(segmentCtx, presetDef);
475
+ // Use responsive layout - overflow goes to secondary row
476
+ const layout = getResponsiveLayout(width);
477
+ const statusContent = layout.topContent;
353
478
  const statusWidth = visibleWidth(statusContent);
354
479
  const topFillWidth = width - 4; // Reserve 4 for corners (╭─ and ─╮)
355
480
 
356
- if (statusWidth <= topFillWidth) {
357
- const fillWidth = topFillWidth - statusWidth;
358
- result.push(topLeft + statusContent + bc("─".repeat(fillWidth)) + topRight);
359
- } else {
360
- // Status too wide - truncate by removing segments from the end
361
- // Build progressively shorter content until it fits
362
- let truncatedContent = "";
363
-
364
- for (let numSegments = presetDef.leftSegments.length - 1; numSegments >= 1; numSegments--) {
365
- const limitedPreset = {
366
- ...presetDef,
367
- leftSegments: presetDef.leftSegments.slice(0, numSegments),
368
- rightSegments: [],
369
- };
370
- truncatedContent = buildStatusContent(segmentCtx, limitedPreset);
371
- const truncWidth = visibleWidth(truncatedContent);
372
- if (truncWidth <= topFillWidth - 1) { // -1 for ellipsis
373
- truncatedContent += "…";
374
- break;
375
- }
376
- }
377
-
378
- const truncWidth = visibleWidth(truncatedContent);
379
- if (truncWidth <= topFillWidth) {
380
- const fillWidth = topFillWidth - truncWidth;
381
- result.push(topLeft + truncatedContent + bc("─".repeat(fillWidth)) + topRight);
382
- } else {
383
- // Still too wide, show minimal
384
- result.push(topLeft + bc("─".repeat(Math.max(0, topFillWidth))) + topRight);
385
- }
386
- }
481
+ const fillWidth = Math.max(0, topFillWidth - statusWidth);
482
+ result.push(topLeft + statusContent + bc("─".repeat(fillWidth)) + topRight);
387
483
 
388
484
  // Content lines (between top border at 0 and bottom border)
389
485
  for (let i = 1; i < bottomBorderIndex; i++) {
@@ -418,7 +514,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
418
514
  return editor;
419
515
  });
420
516
 
421
- // Also set up footer data provider access via a minimal footer
517
+ // Set up footer data provider access via a minimal footer
422
518
  ctx.ui.setFooter((tui: any, _theme: any, footerData: ReadonlyFooterDataProvider) => {
423
519
  footerDataRef = footerData;
424
520
  tuiRef = tui; // Store TUI reference for re-renders on git branch changes
@@ -430,43 +526,76 @@ export default function powerlineFooter(pi: ExtensionAPI) {
430
526
  // No cache to clear - render is always fresh
431
527
  },
432
528
  render(width: number): string[] {
433
- // Only show status in footer during streaming (editor hidden)
434
- // When editor is visible, status shows in editor top border instead
435
- if (!isStreaming || !currentCtx) return [];
529
+ if (!currentCtx) return [];
436
530
 
437
531
  const presetDef = getPreset(config.preset);
438
532
  const segmentCtx = buildSegmentContext(currentCtx, width);
439
- const statusContent = buildStatusContent(segmentCtx, presetDef);
440
-
441
- if (!statusContent) return [];
533
+ const lines: string[] = [];
442
534
 
443
- // Single line with status content, padded/truncated to width
444
- const statusWidth = visibleWidth(statusContent);
445
- if (statusWidth <= width) {
446
- return [statusContent + " ".repeat(width - statusWidth)];
447
- } else {
448
- // Truncate by removing segments (same logic as editor)
449
- let truncatedContent = "";
450
-
451
- for (let numSegments = presetDef.leftSegments.length - 1; numSegments >= 1; numSegments--) {
452
- const limitedPreset = {
453
- ...presetDef,
454
- leftSegments: presetDef.leftSegments.slice(0, numSegments),
455
- rightSegments: [],
456
- };
457
- truncatedContent = buildStatusContent(segmentCtx, limitedPreset);
458
- const truncWidth = visibleWidth(truncatedContent);
459
- if (truncWidth <= width - 1) {
460
- truncatedContent += "…";
461
- break;
535
+ // During streaming, show primary status in footer (editor hidden)
536
+ if (isStreaming) {
537
+ const statusContent = buildStatusContent(segmentCtx, presetDef);
538
+ if (statusContent) {
539
+ const statusWidth = visibleWidth(statusContent);
540
+ if (statusWidth <= width) {
541
+ lines.push(statusContent + " ".repeat(width - statusWidth));
542
+ } else {
543
+ // Truncate by removing segments until it fits
544
+ // Start from leftSegments.length to try "just leftSegments" when rightSegments exists
545
+ let truncatedContent = "";
546
+ let foundFit = false;
547
+ for (let numSegments = presetDef.leftSegments.length; numSegments >= 1; numSegments--) {
548
+ const limitedPreset = {
549
+ ...presetDef,
550
+ leftSegments: presetDef.leftSegments.slice(0, numSegments),
551
+ rightSegments: [],
552
+ };
553
+ truncatedContent = buildStatusContent(segmentCtx, limitedPreset);
554
+ const truncWidth = visibleWidth(truncatedContent);
555
+ if (truncWidth <= width - 1) {
556
+ truncatedContent += "…";
557
+ foundFit = true;
558
+ break;
559
+ }
560
+ }
561
+ // Only push if we found a fit, otherwise skip (don't crash on very narrow terminals)
562
+ if (foundFit) {
563
+ lines.push(truncatedContent);
564
+ }
462
565
  }
463
566
  }
464
-
465
- return [truncatedContent];
466
567
  }
568
+
569
+ return lines;
467
570
  },
468
571
  };
469
572
  });
573
+
574
+ // Set up secondary row as a widget below editor (above sub bar)
575
+ // Shows overflow segments when top bar is too narrow
576
+ ctx.ui.setWidget("powerline-secondary", (tui: any, _theme: any) => {
577
+ return {
578
+ dispose() {},
579
+ invalidate() {},
580
+ render(width: number): string[] {
581
+ if (!currentCtx) return [];
582
+
583
+ // Use responsive layout - secondary row shows overflow from top bar
584
+ const layout = getResponsiveLayout(width);
585
+
586
+ // Only show secondary row if there's overflow content that fits
587
+ if (layout.secondaryContent) {
588
+ const contentWidth = visibleWidth(layout.secondaryContent);
589
+ // Don't render if content exceeds terminal width (graceful degradation)
590
+ if (contentWidth <= width) {
591
+ return [layout.secondaryContent];
592
+ }
593
+ }
594
+
595
+ return [];
596
+ },
597
+ };
598
+ }, { placement: "belowEditor" });
470
599
  });
471
600
  }
472
601
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-powerline-footer",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "Powerline-style status bar extension for pi coding agent",
5
5
  "type": "module",
6
6
  "bin": {
package/presets.ts CHANGED
@@ -2,8 +2,9 @@ import type { PresetDef, StatusLinePreset } from "./types.js";
2
2
 
3
3
  export const PRESETS: Record<StatusLinePreset, PresetDef> = {
4
4
  default: {
5
- leftSegments: ["pi", "model", "thinking", "path", "git", "context_pct", "token_total", "cost", "extension_statuses"],
5
+ leftSegments: ["pi", "path", "model", "thinking", "git", "context_pct", "cache_read", "cost"],
6
6
  rightSegments: [],
7
+ secondarySegments: ["extension_statuses"],
7
8
  separator: "powerline-thin",
8
9
  segmentOptions: {
9
10
  model: { showThinkingLevel: false },
package/segments.ts CHANGED
@@ -177,7 +177,7 @@ const thinkingSegment: StatusLineSegment = {
177
177
  xhigh: "xhigh",
178
178
  };
179
179
  const label = levelText[level] || level;
180
- const content = `thinking:${label}`;
180
+ const content = `think:${label}`;
181
181
 
182
182
  // Use rainbow effect for high/xhigh (like Claude Code ultrathink)
183
183
  if (level === "high" || level === "xhigh") {
package/types.ts CHANGED
@@ -58,6 +58,8 @@ export interface StatusLineSegmentOptions {
58
58
  export interface PresetDef {
59
59
  leftSegments: StatusLineSegmentId[];
60
60
  rightSegments: StatusLineSegmentId[];
61
+ /** Secondary row segments (shown in footer, above sub bar) */
62
+ secondarySegments?: StatusLineSegmentId[];
61
63
  separator: StatusLineSeparatorStyle;
62
64
  segmentOptions?: StatusLineSegmentOptions;
63
65
  }
package/welcome.ts CHANGED
@@ -182,11 +182,20 @@ function renderWelcomeBox(
182
182
  termWidth: number,
183
183
  bottomLine: string,
184
184
  ): string[] {
185
+ // Minimum width for two-column layout: leftCol(26) + separator(3) + minRightCol(15) = 44
186
+ const minLayoutWidth = 44;
187
+
188
+ // If terminal is too narrow for the layout, return empty (skip welcome box)
189
+ if (termWidth < minLayoutWidth) {
190
+ return [];
191
+ }
192
+
185
193
  const minWidth = 76;
186
194
  const maxWidth = 96;
187
- const boxWidth = Math.max(minWidth, Math.min(termWidth - 2, maxWidth));
195
+ // Clamp to termWidth to prevent crash on narrow terminals
196
+ const boxWidth = Math.min(termWidth, Math.max(minWidth, Math.min(termWidth - 2, maxWidth)));
188
197
  const leftCol = 26;
189
- const rightCol = boxWidth - leftCol - 3;
198
+ const rightCol = Math.max(1, boxWidth - leftCol - 3); // Ensure rightCol is at least 1
190
199
 
191
200
  const hChar = "─";
192
201
  const v = dim("│");
@@ -251,9 +260,16 @@ export class WelcomeComponent implements Component {
251
260
  invalidate(): void {}
252
261
 
253
262
  render(termWidth: number): string[] {
263
+ // Minimum width for two-column layout (must match renderWelcomeBox)
264
+ const minLayoutWidth = 44;
265
+ if (termWidth < minLayoutWidth) {
266
+ return [];
267
+ }
268
+
254
269
  const minWidth = 76;
255
270
  const maxWidth = 96;
256
- const boxWidth = Math.max(minWidth, Math.min(termWidth - 2, maxWidth));
271
+ // Clamp to termWidth to prevent crash on narrow terminals
272
+ const boxWidth = Math.min(termWidth, Math.max(minWidth, Math.min(termWidth - 2, maxWidth)));
257
273
 
258
274
  // Bottom line with countdown
259
275
  const countdownText = ` Press any key to continue (${this.countdown}s) `;
@@ -290,18 +306,27 @@ export class WelcomeHeader implements Component {
290
306
  invalidate(): void {}
291
307
 
292
308
  render(termWidth: number): string[] {
309
+ // Minimum width for two-column layout (must match renderWelcomeBox)
310
+ const minLayoutWidth = 44;
311
+ if (termWidth < minLayoutWidth) {
312
+ return [];
313
+ }
314
+
293
315
  const minWidth = 76;
294
316
  const maxWidth = 96;
295
- const boxWidth = Math.max(minWidth, Math.min(termWidth - 2, maxWidth));
317
+ // Clamp to termWidth to prevent crash on narrow terminals
318
+ const boxWidth = Math.min(termWidth, Math.max(minWidth, Math.min(termWidth - 2, maxWidth)));
296
319
  const hChar = "─";
297
320
 
298
321
  // Bottom line with column separator (leftCol=26, rightCol=boxWidth-29)
299
322
  const leftCol = 26;
300
- const rightCol = boxWidth - leftCol - 3;
323
+ const rightCol = Math.max(1, boxWidth - leftCol - 3);
301
324
  const bottomLine = dim(hChar.repeat(leftCol)) + dim("┴") + dim(hChar.repeat(rightCol));
302
325
 
303
326
  const lines = renderWelcomeBox(this.data, termWidth, bottomLine);
304
- lines.push(""); // Add empty line for spacing
327
+ if (lines.length > 0) {
328
+ lines.push(""); // Add empty line for spacing only if we rendered content
329
+ }
305
330
  return lines;
306
331
  }
307
332
  }