pi-powerline-footer 0.2.10 → 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 +29 -0
- package/index.ts +208 -79
- package/package.json +7 -1
- package/presets.ts +2 -1
- package/segments.ts +1 -1
- package/types.ts +2 -0
- package/welcome.ts +31 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
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
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- Added `pi` manifest to package.json for pi v0.50.0 package system compliance
|
|
30
|
+
- Added `pi-package` keyword for npm discoverability
|
|
31
|
+
|
|
3
32
|
## [0.2.10] - 2026-01-17
|
|
4
33
|
|
|
5
34
|
### Fixed
|
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
|
-
/**
|
|
47
|
-
function
|
|
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
|
|
53
|
-
for (const segId of
|
|
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
|
-
|
|
72
|
+
parts.push(rendered.content);
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
if (
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
351
|
-
const
|
|
352
|
-
const statusContent =
|
|
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
|
-
|
|
357
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
440
|
-
|
|
441
|
-
if (!statusContent) return [];
|
|
533
|
+
const lines: string[] = [];
|
|
442
534
|
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Powerline-style status bar extension for pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"cli.js"
|
|
13
13
|
],
|
|
14
14
|
"keywords": [
|
|
15
|
+
"pi-package",
|
|
15
16
|
"pi",
|
|
16
17
|
"coding-agent",
|
|
17
18
|
"powerline",
|
|
@@ -23,5 +24,10 @@
|
|
|
23
24
|
"repository": {
|
|
24
25
|
"type": "git",
|
|
25
26
|
"url": "git+https://github.com/nicobailon/pi-powerline-footer.git"
|
|
27
|
+
},
|
|
28
|
+
"pi": {
|
|
29
|
+
"extensions": [
|
|
30
|
+
"./index.ts"
|
|
31
|
+
]
|
|
26
32
|
}
|
|
27
33
|
}
|
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", "
|
|
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 = `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
}
|