pi-extension-utils 0.5.0 → 0.5.2
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/dist/src/pane/chrome.js +16 -10
- package/dist/src/pane/overlay.d.ts +6 -1
- package/dist/src/pane/overlay.js +14 -4
- package/package.json +1 -1
package/dist/src/pane/chrome.js
CHANGED
|
@@ -34,7 +34,11 @@ export function pad(s, len) {
|
|
|
34
34
|
return s + " ".repeat(Math.max(0, len - vis));
|
|
35
35
|
}
|
|
36
36
|
function normalizeRenderableText(text) {
|
|
37
|
-
|
|
37
|
+
// Coerce defensively: a non-string here reaches truncateToWidth/text.slice
|
|
38
|
+
// and hard-crashes pi ("text.slice is not a function"). Callers pass values
|
|
39
|
+
// derived from on-disk/agent state that may be out-of-contract.
|
|
40
|
+
const s = typeof text === "string" ? text : text == null ? "" : String(text);
|
|
41
|
+
return s
|
|
38
42
|
.replaceAll("\t", " ")
|
|
39
43
|
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001A\u001C-\u001F\u007F]/g, "");
|
|
40
44
|
}
|
|
@@ -94,12 +98,12 @@ export function titledTopSegment(theme, opts) {
|
|
|
94
98
|
return dash(width);
|
|
95
99
|
const labelColor = opts.labelColor ?? "text";
|
|
96
100
|
const tailColor = opts.tailColor ?? "dim";
|
|
97
|
-
const tailPlain = opts.tailPlain ?? opts.tail ?? "";
|
|
101
|
+
const tailPlain = normalizeRenderableText(opts.tailPlain ?? opts.tail ?? "");
|
|
98
102
|
const tailRendered = opts.tailRendered ?? (opts.tail !== undefined ? theme.fg(tailColor, opts.tail) : "");
|
|
99
103
|
const tailLen = visibleWidth(tailPlain);
|
|
100
104
|
const canShowTail = tailLen > 0 && width - tailLen >= 14;
|
|
101
105
|
const labelBudget = Math.max(0, width - (canShowTail ? tailLen + 6 : 4));
|
|
102
|
-
const labelText = clipText(opts.label ?? "", labelBudget);
|
|
106
|
+
const labelText = clipText(normalizeRenderableText(opts.label ?? ""), labelBudget);
|
|
103
107
|
const labelLen = visibleWidth(labelText);
|
|
104
108
|
if (labelLen === 0)
|
|
105
109
|
return dash(width);
|
|
@@ -118,7 +122,7 @@ export function titledTopSegment(theme, opts) {
|
|
|
118
122
|
return "";
|
|
119
123
|
const labelColor = opts.labelColor ?? "text";
|
|
120
124
|
const tailColor = opts.tailColor ?? "dim";
|
|
121
|
-
const tailPlain = opts.tailPlain ?? opts.tail ?? "";
|
|
125
|
+
const tailPlain = normalizeRenderableText(opts.tailPlain ?? opts.tail ?? "");
|
|
122
126
|
const rawTailRendered = opts.tailRendered ?? (opts.tail !== undefined ? theme.fg(tailColor, opts.tail) : "");
|
|
123
127
|
const tailLen = visibleWidth(tailPlain);
|
|
124
128
|
// With a tail, the minimum chrome is:
|
|
@@ -129,7 +133,7 @@ export function titledTopSegment(theme, opts) {
|
|
|
129
133
|
const labelBudget = tailFits ? Math.max(0, width - tailLen - 7) : Math.max(0, width - 3);
|
|
130
134
|
// Defensive: callers occasionally pass undefined labels for transient/legacy
|
|
131
135
|
// runs that lack agent+mode+label; treat as empty rather than crashing pi.
|
|
132
|
-
const rawLabel = opts.label ?? "";
|
|
136
|
+
const rawLabel = normalizeRenderableText(opts.label ?? "");
|
|
133
137
|
const labelText = truncateToWidth(rawLabel, labelBudget, "").replace(/\u001b\[[0-9;]*m/g, "");
|
|
134
138
|
const labelStyled = opts.labelBold && theme.bold
|
|
135
139
|
? theme.bold(theme.fg(labelColor, labelText))
|
|
@@ -153,11 +157,12 @@ export function titledBottomSegment(theme, width, hint, focused) {
|
|
|
153
157
|
const dash = (n) => theme.fg("dim", "─".repeat(Math.max(0, n)));
|
|
154
158
|
if (width <= 0)
|
|
155
159
|
return "";
|
|
156
|
-
|
|
160
|
+
const safeHint = normalizeRenderableText(hint);
|
|
161
|
+
if (!safeHint)
|
|
157
162
|
return dash(width);
|
|
158
163
|
// Reserve at minimum `─ ` + hint + ` `; truncate hint to fit when narrow so we never overflow.
|
|
159
164
|
const hintBudget = Math.max(0, width - 3);
|
|
160
|
-
const clipped = truncateToWidth(
|
|
165
|
+
const clipped = truncateToWidth(safeHint, hintBudget);
|
|
161
166
|
const clippedLen = visibleWidth(clipped);
|
|
162
167
|
if (clippedLen === 0)
|
|
163
168
|
return dash(width);
|
|
@@ -196,10 +201,11 @@ export function flatRule(theme, title, width, opts) {
|
|
|
196
201
|
if (width <= 0)
|
|
197
202
|
return "";
|
|
198
203
|
const dash = (n) => theme.fg("dim", "─".repeat(Math.max(0, n)));
|
|
199
|
-
|
|
204
|
+
const safeTitle = normalizeRenderableText(title);
|
|
205
|
+
if (!safeTitle)
|
|
200
206
|
return dash(width);
|
|
201
207
|
if (opts?.leadingDashes !== undefined) {
|
|
202
|
-
const label = ` ${
|
|
208
|
+
const label = ` ${safeTitle} `;
|
|
203
209
|
const labelW = visibleWidth(label);
|
|
204
210
|
if (labelW + 4 >= width)
|
|
205
211
|
return dash(width);
|
|
@@ -207,7 +213,7 @@ export function flatRule(theme, title, width, opts) {
|
|
|
207
213
|
const trailing = Math.max(0, width - labelW - leading);
|
|
208
214
|
return `${dash(leading)}${theme.fg("dim", label)}${dash(trailing)}`;
|
|
209
215
|
}
|
|
210
|
-
const clipped = truncateToWidth(
|
|
216
|
+
const clipped = truncateToWidth(safeTitle, Math.max(0, width - 4));
|
|
211
217
|
const clippedLen = visibleWidth(clipped);
|
|
212
218
|
const styled = theme.fg("dim", clipped);
|
|
213
219
|
const trailing = Math.max(0, width - (clippedLen + 3));
|
|
@@ -72,7 +72,12 @@ export interface PaneOverlaySplitOptions {
|
|
|
72
72
|
export interface PaneCollapseOptions {
|
|
73
73
|
key?: string;
|
|
74
74
|
collapsedWidth?: number;
|
|
75
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Legend/divider label for the collapse toggle. A function receives the
|
|
77
|
+
* current collapsed state so the hint can read e.g. "hide sidebar" when open
|
|
78
|
+
* and "open sidebar" when collapsed.
|
|
79
|
+
*/
|
|
80
|
+
label?: string | ((collapsed: boolean) => string);
|
|
76
81
|
}
|
|
77
82
|
export interface PaneOverlayOptions<T = unknown, Row = unknown> {
|
|
78
83
|
primary: PrimaryPaneOptions<Row>;
|
package/dist/src/pane/overlay.js
CHANGED
|
@@ -89,6 +89,10 @@ export function paneOverlay(options) {
|
|
|
89
89
|
let finished = false;
|
|
90
90
|
let focus = "primary";
|
|
91
91
|
let collapsed = false;
|
|
92
|
+
const collapseLabel = (fallback) => {
|
|
93
|
+
const label = collapse?.label;
|
|
94
|
+
return typeof label === "function" ? label(collapsed) : (label ?? fallback);
|
|
95
|
+
};
|
|
92
96
|
let leftFraction = split.initialFraction ?? 0.5;
|
|
93
97
|
let lastRenderWidth = 80;
|
|
94
98
|
let lastSelectedKey;
|
|
@@ -339,7 +343,7 @@ export function paneOverlay(options) {
|
|
|
339
343
|
if (!collapsed)
|
|
340
344
|
entries.push({ key: "[/]", label: "resize" });
|
|
341
345
|
if (collapse)
|
|
342
|
-
entries.push({ key: collapseKey, label:
|
|
346
|
+
entries.push({ key: collapseKey, label: collapseLabel("collapse") });
|
|
343
347
|
for (const action of customActions) {
|
|
344
348
|
if (action.showInLegend === false)
|
|
345
349
|
continue;
|
|
@@ -453,10 +457,16 @@ export function paneOverlay(options) {
|
|
|
453
457
|
: primaryRows.length > primaryHeight
|
|
454
458
|
? formatScrollInfo(primaryState.scrollOffset, Math.max(0, primaryRows.length - primaryHeight), { style: "position" })
|
|
455
459
|
: "");
|
|
456
|
-
const
|
|
457
|
-
|
|
460
|
+
const detailFooterBase = resolveValue(options.detail.footer, ctx) ??
|
|
461
|
+
(detailRows.length > detailHeight
|
|
458
462
|
? formatScrollInfo(detailState.scrollOffset, Math.max(0, detailRows.length - detailHeight), { style: "position" })
|
|
459
463
|
: "");
|
|
464
|
+
// When the primary pane is collapsed its legend (and the collapse hint with
|
|
465
|
+
// it) disappears, so the user can't see how to reopen it. Surface the
|
|
466
|
+
// reopen hint in the always-visible detail footer instead.
|
|
467
|
+
const detailFooterText = collapsed && collapse
|
|
468
|
+
? [`${collapseKey} ${collapseLabel("expand")}`, detailFooterBase].filter(Boolean).join(" \u00b7 ")
|
|
469
|
+
: detailFooterBase;
|
|
460
470
|
const bottomPrimary = titledBottomSegment(chromeTheme, primaryWidth, primaryFooterText, focus === "primary");
|
|
461
471
|
const bottomDetail = titledBottomSegment(chromeTheme, detailWidth, detailFooterText, focus === "detail");
|
|
462
472
|
const bottomBorder = primaryWidth === 0
|
|
@@ -474,7 +484,7 @@ export function paneOverlay(options) {
|
|
|
474
484
|
primaryCell = infoLines[row - infoStart - 1] ?? "";
|
|
475
485
|
}
|
|
476
486
|
else if (legendPlacement === "primary" && row === legendStart && legendLines.length > 0) {
|
|
477
|
-
primaryCell = flatRule(chromeTheme, collapse
|
|
487
|
+
primaryCell = flatRule(chromeTheme, collapse ? collapseLabel("actions") : "actions", primaryWidth);
|
|
478
488
|
}
|
|
479
489
|
if (legendPlacement === "primary" && row > legendStart) {
|
|
480
490
|
const legendIndex = row - legendStart - 1;
|