@bismawy/pi-arnative 0.2.9 → 0.2.11
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 +13 -0
- package/extensions/footer.ts +175 -30
- package/extensions/tools.ts +35 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.11] - 2026-10-01
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- The footer status area now reflows: 2 lines stay the default while everything fits, but on a narrow terminal the statuses and the metrics (tok/s, cache, context) move onto a 3rd line instead of being truncated. `extensions/footer.ts` `layoutStatusLines()` (`rows` = line budget of the whole footer, `maxRows` caps the growth); self-check covers the wide/narrow/cap cases.
|
|
7
|
+
|
|
8
|
+
## [0.2.10] - 2026-10-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- The working loader now shows elapsed turn time next to the label — `⠴ Working (50s)`, `⠴ Executing (1m 5s)`. One `setInterval(1s)` per turn (`extensions/footer.ts`), cleared on `turn_end`/`session_shutdown`; the suffix only appears from 1s (no `(0s)` flicker) and the timer keeps counting across tool switches inside a turn. With parallel tools the label follows the last started tool and only falls back to `Working` when every `toolCallId` has ended.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Tool image previews (e.g. `read` on a PNG) no longer break the layout. pi renders a preview as an extra child below the tool box, padded with `result.rows - 1` blank rows, and the gap-stripping patch deleted exactly those rows — the terminal then painted the image over the rows underneath (chat text, editor, footer). Only the leading Spacer is dropped now; from the image sequence on, every line is kept.
|
|
15
|
+
|
|
3
16
|
## [0.2.9] - 2026-09-30
|
|
4
17
|
|
|
5
18
|
### Fixed
|
package/extensions/footer.ts
CHANGED
|
@@ -58,6 +58,23 @@ let assistantStartMs: number | null = null;
|
|
|
58
58
|
let assistantChars = 0;
|
|
59
59
|
let latestSpeed: number | null = null;
|
|
60
60
|
|
|
61
|
+
// Live elapsed clock for the working loader: "⠴ Working (50s)".
|
|
62
|
+
const WORK_TIMER_KEY = Symbol.for("pi-arnative.workTimer");
|
|
63
|
+
const WORK_LABEL = "Working";
|
|
64
|
+
let workVerb = WORK_LABEL;
|
|
65
|
+
let workStartMs: number | null = null;
|
|
66
|
+
let workUI: { setWorkingMessage?: (message?: string) => void } | null = null;
|
|
67
|
+
// Verbs of tools still running (toolCallId -> verb): the label must not reset to
|
|
68
|
+
// "Working" while a sibling tool is still going (pi executes tools in parallel).
|
|
69
|
+
const activeToolVerbs = new Map<string, string>();
|
|
70
|
+
|
|
71
|
+
/** Label while `active` tools remain: last started wins, empty -> default. */
|
|
72
|
+
export function currentToolVerb(active: ReadonlyMap<string, string>): string {
|
|
73
|
+
let verb = WORK_LABEL;
|
|
74
|
+
for (const v of active.values()) verb = v;
|
|
75
|
+
return verb;
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
function run(cmd: string, args: string[], cwd: string): Promise<string> {
|
|
62
79
|
return new Promise((resolve) => {
|
|
63
80
|
execFile(cmd, args, { cwd, timeout: 2000 }, (err, stdout) => {
|
|
@@ -159,7 +176,64 @@ export function boxEditorLines(
|
|
|
159
176
|
return out;
|
|
160
177
|
}
|
|
161
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Wraps the footer status lines: `groups` separated by `sep`, with the already-joined
|
|
181
|
+
* `metrics` right-aligned on the last group line when they fit, else on a line of
|
|
182
|
+
* their own.
|
|
183
|
+
*
|
|
184
|
+
* `rows` is the footer line budget (line 1 plus the status lines): 2 reproduces the
|
|
185
|
+
* classic layout as long as it fits, 3 lets the metrics take their own line instead
|
|
186
|
+
* of crowding the statuses. `maxRows` caps the growth — a group wider than the
|
|
187
|
+
* terminal is never split, the caller truncates it.
|
|
188
|
+
*/
|
|
189
|
+
export function layoutStatusLines(
|
|
190
|
+
groups: readonly string[],
|
|
191
|
+
metrics: string,
|
|
192
|
+
sep: string,
|
|
193
|
+
width: number,
|
|
194
|
+
rows: number,
|
|
195
|
+
maxRows = Number.MAX_SAFE_INTEGER,
|
|
196
|
+
vw: (s: string) => number = visibleWidth,
|
|
197
|
+
): string[] {
|
|
198
|
+
if (rows > maxRows) rows = maxRows;
|
|
199
|
+
const budget = Math.max(1, rows - 1);
|
|
200
|
+
const out: string[] = [];
|
|
201
|
+
let line = "";
|
|
202
|
+
|
|
203
|
+
for (const group of groups) {
|
|
204
|
+
const candidate = line ? line + sep + group : group;
|
|
205
|
+
// An empty line is never flushed: an oversized group must not loop here.
|
|
206
|
+
if (line && vw(candidate) > width) {
|
|
207
|
+
out.push(line);
|
|
208
|
+
line = group;
|
|
209
|
+
} else {
|
|
210
|
+
line = candidate;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (metrics) {
|
|
215
|
+
const pad = (before: string) => " ".repeat(Math.max(1, width - vw(before) - vw(metrics))) + metrics;
|
|
216
|
+
if (!line) {
|
|
217
|
+
line = pad("");
|
|
218
|
+
} else if (vw(line + sep + metrics) <= width) {
|
|
219
|
+
// Metrics share the last group line, right-aligned as before.
|
|
220
|
+
line = line + pad(line);
|
|
221
|
+
} else if (out.length < budget) {
|
|
222
|
+
// `budget` is the only thing `rows` controls: 2 lets the metrics take a
|
|
223
|
+
// line of their own instead of crowding the groups, 1 packs everything.
|
|
224
|
+
out.push(line);
|
|
225
|
+
line = pad("");
|
|
226
|
+
} else {
|
|
227
|
+
line = line + sep + metrics;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
out.push(line);
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
162
235
|
function parseOptimizer(raw: string | undefined): string | null {
|
|
236
|
+
|
|
163
237
|
if (!raw) return null;
|
|
164
238
|
const m = raw.match(/([A-Za-z0-9_-]+)\s+cache\s+(\d+\/\d+)·[^\s]+\s+([\d.]+%)/);
|
|
165
239
|
return m ? `${m[1]} ${m[2]} (${m[3]})` : null;
|
|
@@ -233,9 +307,48 @@ function mcpStatusOf(sub: string): string {
|
|
|
233
307
|
return "Executing";
|
|
234
308
|
}
|
|
235
309
|
|
|
310
|
+
/** Loader label with the elapsed suffix; no suffix below 1s (avoids a "(0s)" flicker). */
|
|
311
|
+
export function workingLabel(verb: string, elapsedMs: number): string {
|
|
312
|
+
return elapsedMs >= 1000 ? `${verb} (${formatDuration(elapsedMs)})` : verb;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function paintWorking(): void {
|
|
316
|
+
if (!workUI || workStartMs === null) return;
|
|
317
|
+
try {
|
|
318
|
+
workUI.setWorkingMessage?.(workingLabel(workVerb, Date.now() - workStartMs));
|
|
319
|
+
} catch {
|
|
320
|
+
// ignore
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function stopWorkingClock(): void {
|
|
325
|
+
const t = (globalThis as Record<symbol, any>)[WORK_TIMER_KEY];
|
|
326
|
+
if (t) clearInterval(t);
|
|
327
|
+
(globalThis as Record<symbol, any>)[WORK_TIMER_KEY] = null;
|
|
328
|
+
workStartMs = null;
|
|
329
|
+
activeToolVerbs.clear();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Start (or restart) the 1s tick from a fresh turn. */
|
|
333
|
+
function startWorkingClock(ctx: { ui?: any }): void {
|
|
334
|
+
stopWorkingClock();
|
|
335
|
+
workUI = ctx.ui ?? null;
|
|
336
|
+
workVerb = WORK_LABEL;
|
|
337
|
+
workStartMs = Date.now();
|
|
338
|
+
(globalThis as Record<symbol, any>)[WORK_TIMER_KEY] = setInterval(paintWorking, 1000);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Swap the verb (tool change) without resetting the turn clock. */
|
|
342
|
+
function setWorkingVerb(ctx: { ui?: any }, verb: string): void {
|
|
343
|
+
workUI = ctx.ui ?? null;
|
|
344
|
+
workVerb = verb;
|
|
345
|
+
if (workStartMs === null) workStartMs = Date.now();
|
|
346
|
+
paintWorking();
|
|
347
|
+
}
|
|
348
|
+
|
|
236
349
|
export function getToolWorkingMessage(toolName: string, args?: any): string {
|
|
237
350
|
const n = (toolName ?? "").trim();
|
|
238
|
-
if (!n) return
|
|
351
|
+
if (!n) return WORK_LABEL;
|
|
239
352
|
|
|
240
353
|
// Standard / file tools
|
|
241
354
|
if (n === "edit") return "Editing";
|
|
@@ -263,7 +376,7 @@ export function getToolWorkingMessage(toolName: string, args?: any): string {
|
|
|
263
376
|
return mcpStatusOf(sub);
|
|
264
377
|
}
|
|
265
378
|
|
|
266
|
-
return
|
|
379
|
+
return WORK_LABEL;
|
|
267
380
|
}
|
|
268
381
|
|
|
269
382
|
export default function (pi: ExtensionAPI) {
|
|
@@ -278,6 +391,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
278
391
|
});
|
|
279
392
|
|
|
280
393
|
pi.on("session_start", async (event, ctx) => {
|
|
394
|
+
// A /reload re-imports this module while the old instance's interval (and its
|
|
395
|
+
// ctx.ui closures) may still be alive; clear it before starting a new one.
|
|
396
|
+
stopWorkingClock();
|
|
281
397
|
if (event.reason !== "reload") {
|
|
282
398
|
sessionStartMs = Date.now();
|
|
283
399
|
(globalThis as Record<symbol, number>)[SESSION_START_KEY] = sessionStartMs;
|
|
@@ -353,7 +469,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
353
469
|
}
|
|
354
470
|
})();
|
|
355
471
|
const rawCache = statuses.get("pi-cache-stats");
|
|
356
|
-
const segs: string[] = [];
|
|
357
472
|
const cleanStatus = (s: string) => {
|
|
358
473
|
if (s.includes("MCP:")) {
|
|
359
474
|
let clean = stripAnsi(s);
|
|
@@ -391,12 +506,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
391
506
|
return tint(clean.trim());
|
|
392
507
|
};
|
|
393
508
|
|
|
509
|
+
const groups: string[] = [];
|
|
394
510
|
const mcp = statuses.get("mcp");
|
|
395
|
-
if (mcp !== undefined)
|
|
511
|
+
if (mcp !== undefined) groups.push(cleanStatus(mcp));
|
|
396
512
|
for (const [k, s] of statuses) {
|
|
397
|
-
if (k !== "mcp" && k !== "pi-cache-stats")
|
|
513
|
+
if (k !== "mcp" && k !== "pi-cache-stats") groups.push(cleanStatus(s));
|
|
398
514
|
}
|
|
399
|
-
const left2 = segs.join(sep);
|
|
400
515
|
|
|
401
516
|
const opt = parseOptimizer(rawCache);
|
|
402
517
|
let usageStr = "";
|
|
@@ -406,13 +521,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
406
521
|
// fallback
|
|
407
522
|
}
|
|
408
523
|
const speedStr = latestSpeed !== null && latestSpeed > 0 ? `${acc("\udb81\udcc5")} ${tint(`${latestSpeed.toFixed(1)} tok/s`)}` : "";
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
524
|
+
const metrics = [speedStr, opt ? `${acc("\udb80\udf5b")} ${tint(opt)}` : "", usageStr].filter(Boolean).join(` ${dim("·")} `);
|
|
525
|
+
|
|
526
|
+
// 2 footer lines normally; grow to 3 rather than crowding the second one on
|
|
527
|
+
// a narrow terminal, then truncate (a single status can still overflow).
|
|
528
|
+
const MAX_ROWS = 3;
|
|
529
|
+
for (let rows = 2; ; rows++) {
|
|
530
|
+
const wrapped = layoutStatusLines(groups, metrics, sep, width, rows, MAX_ROWS);
|
|
531
|
+
if (rows >= MAX_ROWS || wrapped.every((l) => visibleWidth(l) <= width)) {
|
|
532
|
+
for (const l of wrapped) lines.push(truncateToWidth(l, width));
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
416
535
|
}
|
|
417
536
|
return lines;
|
|
418
537
|
},
|
|
@@ -502,29 +621,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
502
621
|
(globalThis as Record<symbol, any>)[MODEL_KEY] = currentModel;
|
|
503
622
|
(globalThis as Record<symbol, any>)[THINKING_KEY] = currentThinkingLevel;
|
|
504
623
|
(globalThis as Record<symbol, any>)[CWD_KEY] = ctx.cwd;
|
|
505
|
-
|
|
506
|
-
ctx.ui?.setWorkingMessage?.();
|
|
507
|
-
} catch {
|
|
508
|
-
// ignore
|
|
509
|
-
}
|
|
624
|
+
startWorkingClock(ctx);
|
|
510
625
|
void refreshGit(ctx.cwd).then(() => poke());
|
|
511
626
|
});
|
|
512
627
|
|
|
513
628
|
pi.on("tool_execution_start", async (event, ctx) => {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
} catch {
|
|
518
|
-
// ignore
|
|
519
|
-
}
|
|
629
|
+
const verb = getToolWorkingMessage(event.toolName, event.args);
|
|
630
|
+
activeToolVerbs.set(event.toolCallId, verb);
|
|
631
|
+
setWorkingVerb(ctx, verb);
|
|
520
632
|
});
|
|
521
633
|
|
|
522
|
-
pi.on("tool_execution_end", async (
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
} catch {
|
|
526
|
-
// ignore
|
|
527
|
-
}
|
|
634
|
+
pi.on("tool_execution_end", async (event, ctx) => {
|
|
635
|
+
activeToolVerbs.delete(event.toolCallId);
|
|
636
|
+
setWorkingVerb(ctx, currentToolVerb(activeToolVerbs));
|
|
528
637
|
});
|
|
529
638
|
|
|
530
639
|
pi.on("message_update", async (event) => {
|
|
@@ -566,6 +675,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
566
675
|
});
|
|
567
676
|
|
|
568
677
|
pi.on("turn_end", async (_event, ctx) => {
|
|
678
|
+
stopWorkingClock();
|
|
569
679
|
try {
|
|
570
680
|
ctx.ui?.setWorkingMessage?.();
|
|
571
681
|
} catch {
|
|
@@ -587,6 +697,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
587
697
|
});
|
|
588
698
|
|
|
589
699
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
700
|
+
stopWorkingClock();
|
|
590
701
|
rerender = null;
|
|
591
702
|
const cur = ((globalThis as Record<symbol, number>)[GEN_KEY] ?? 0);
|
|
592
703
|
if (cur !== runtimeGen) return;
|
|
@@ -613,6 +724,27 @@ if (isMain(import.meta.url)) {
|
|
|
613
724
|
assert(formatDuration(65_000) === "1m 5s", "minutes + seconds");
|
|
614
725
|
assert(formatDuration(3_700_000) === "1h 1m", "hours + minutes");
|
|
615
726
|
|
|
727
|
+
// Self-check: footer status wrapping (2 lines until it crowds, then 3).
|
|
728
|
+
const vw = (s: string) => s.length;
|
|
729
|
+
const g = (n: number) => "g".repeat(n);
|
|
730
|
+
// Wide terminal: everything on one status line, metrics right-aligned.
|
|
731
|
+
const wide = layoutStatusLines([g(4), g(4), g(4)], "Mmmmm", " | ", 40, 2, 3, vw);
|
|
732
|
+
assert(wide.length === 1, "wide: statuses share one line");
|
|
733
|
+
assert(wide[0]!.endsWith("Mmmmm") && vw(wide[0]!) === 40, "wide: metrics right-aligned on that line");
|
|
734
|
+
// Narrow: groups stop sharing a line and metrics moves to its own third line.
|
|
735
|
+
const narrow = layoutStatusLines([g(4), g(4), g(4)], g(8), " | ", 14, 3, 3, vw);
|
|
736
|
+
assert(narrow.length === 3, "narrow: grows to 3 lines");
|
|
737
|
+
assert(narrow.every((l) => vw(l) <= 14), "narrow: every line fits the terminal");
|
|
738
|
+
// The same width with a 2-line budget cannot fit -> the caller asks for row 3.
|
|
739
|
+
assert(
|
|
740
|
+
layoutStatusLines([g(4), g(4), g(4)], g(8), " | ", 14, 2, 3, vw).some((l) => vw(l) > 14),
|
|
741
|
+
"2-line budget overflows, so the caller grows to 3"
|
|
742
|
+
);
|
|
743
|
+
// Tight cap: never exceed maxRows, even though lines then overflow (caller truncates).
|
|
744
|
+
assert(layoutStatusLines([g(4), g(4), g(4)], "Mmmmm", " | ", 3, 3, 3, vw).length <= 3, "cap: 3 lines at most");
|
|
745
|
+
assert(layoutStatusLines([g(9)], "", " | ", 4, 2, 3, vw).length === 1, "oversized group is not split");
|
|
746
|
+
assert(layoutStatusLines([], "m", " | ", 8, 2, 3, vw).length === 1, "metrics only");
|
|
747
|
+
|
|
616
748
|
const plain = (s: string) => s;
|
|
617
749
|
const D = "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"; // 8 columns = content width in this test
|
|
618
750
|
const boxed = boxEditorLines([D, " hi ", " lo ", D, " /mo "], 2, plain);
|
|
@@ -638,6 +770,19 @@ if (isMain(import.meta.url)) {
|
|
|
638
770
|
assert(getToolWorkingMessage("todo") === "Updating tasks", "todo -> Updating tasks");
|
|
639
771
|
assert(getToolWorkingMessage("unknown_tool") === "Working", "unknown -> Working");
|
|
640
772
|
assert(getToolWorkingMessage("mcp", { tool: "fetch_repo" }) === "Fetching", "mcp fetch -> Fetching");
|
|
773
|
+
|
|
774
|
+
// Self-check: working loader clock
|
|
775
|
+
assert(workingLabel("Working", 0) === "Working", "no elapsed suffix below 1s");
|
|
776
|
+
assert(workingLabel("Working", 999) === "Working", "no elapsed suffix at 999ms");
|
|
777
|
+
assert(workingLabel("Working", 50_000) === "Working (50s)", "elapsed seconds suffix");
|
|
778
|
+
assert(workingLabel("Executing", 65_000) === "Executing (1m 5s)", "elapsed minutes suffix");
|
|
779
|
+
assert(currentToolVerb(new Map()) === WORK_LABEL, "no active tool -> default label");
|
|
780
|
+
assert(currentToolVerb(new Map([["a", "Reading"], ["b", "Writing"]])) === "Writing", "parallel tools: last started wins");
|
|
781
|
+
assert(currentToolVerb(new Map([["a", "Reading"]])) === "Reading", "after one ends, the remaining tool's label shows");
|
|
782
|
+
startWorkingClock({ ui: { setWorkingMessage: () => {} } });
|
|
783
|
+
assert(Boolean((globalThis as Record<symbol, unknown>)[WORK_TIMER_KEY]), "clock starts: timer stored on globalThis");
|
|
784
|
+
stopWorkingClock();
|
|
785
|
+
assert((globalThis as Record<symbol, unknown>)[WORK_TIMER_KEY] == null, "clock stops: reload guard clears the timer");
|
|
641
786
|
assert(boxed.slice(0, 4).every((l) => visibleWidth(l) === visibleWidth(D) + 5), "uniform box width (sides + prompt)");
|
|
642
787
|
assert(boxEditorLines(["\u2500\u2500\u2500\u2500"], 1, plain).length === 1, "too narrow: left unchanged");
|
|
643
788
|
|
package/extensions/tools.ts
CHANGED
|
@@ -215,6 +215,23 @@ const OWN_BOX = new Set<string>();
|
|
|
215
215
|
const hasOwnRendererDef = (self: { toolDefinition?: { renderCall?: unknown; renderResult?: unknown } }): boolean =>
|
|
216
216
|
Boolean(self?.toolDefinition?.renderCall || self?.toolDefinition?.renderResult);
|
|
217
217
|
|
|
218
|
+
// Inline image line: kitty (\x1b_G) or iTerm2 (\x1b]1337;File=) graphics payload.
|
|
219
|
+
// pi renders a tool's image preview as an extra child BELOW the box, padding it with
|
|
220
|
+
// `result.rows - 1` blank rows; the last row carries a cursor-up prefix before the
|
|
221
|
+
// sequence. Not exported by pi-tui, so match both prefixes.
|
|
222
|
+
export const isImageLine = (line: string): boolean => line.includes("\x1b_G") || line.includes("\x1b]1337;File=");
|
|
223
|
+
|
|
224
|
+
/** Drop the leading Spacer only when an image preview follows: its blank tail rows
|
|
225
|
+
* are the image's vertical space, and deleting them made the terminal draw the
|
|
226
|
+
* image over the rows below (chat text, editor, footer). */
|
|
227
|
+
export function stripEdgesKeepingImages(lines: string[]): string[] {
|
|
228
|
+
const firstImage = lines.findIndex(isImageLine);
|
|
229
|
+
if (firstImage < 0) return stripBlankEdges(lines);
|
|
230
|
+
let s = 0;
|
|
231
|
+
while (s < firstImage && isBlankLine(lines[s])) s++;
|
|
232
|
+
return lines.slice(s);
|
|
233
|
+
}
|
|
234
|
+
|
|
218
235
|
const GAP_KEY = Symbol.for("pi-arnative.toolGapStripped");
|
|
219
236
|
if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol, boolean>)[GAP_KEY]) {
|
|
220
237
|
(globalThis as Record<symbol, boolean>)[GAP_KEY] = true;
|
|
@@ -222,7 +239,7 @@ if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol,
|
|
|
222
239
|
ToolExecutionComponent.prototype.render = function (width: number): string[] {
|
|
223
240
|
const lines = origRender.call(this, width);
|
|
224
241
|
const name = (this as { toolName?: string }).toolName ?? "";
|
|
225
|
-
return OWN_BOX.has(name) || !hasOwnRendererDef(this) ?
|
|
242
|
+
return OWN_BOX.has(name) || !hasOwnRendererDef(this) ? stripEdgesKeepingImages(lines) : lines;
|
|
226
243
|
};
|
|
227
244
|
}
|
|
228
245
|
|
|
@@ -682,6 +699,23 @@ if (isMain(import.meta.url)) {
|
|
|
682
699
|
assert(stripBlankEdges(["", "\x1b[2m\x1b[22m", "x"]).join() === "x", "blank ANSI line counts as blank");
|
|
683
700
|
assert(stripBlankEdges(["a"]).join() === "a", "no blanks: kept intact");
|
|
684
701
|
|
|
702
|
+
// Image preview: pi pads the image with blank rows (`result.rows - 1`), and the last
|
|
703
|
+
// one carries a cursor-up prefix before the sequence. Those rows are the image's
|
|
704
|
+
// vertical space — deleting them made the terminal paint the image over whatever
|
|
705
|
+
// sits below (chat text, editor, footer). Regression: preview destroyed the layout.
|
|
706
|
+
const kittySeq = "\x1b[5A\x1b_Ga=T,f=100,i=7,q=2,C=1,m=0;AAAA\x1b\\";
|
|
707
|
+
const itermSeq = "\x1b]1337;File=inline=1:AAAA\x07";
|
|
708
|
+
const withImage = ["", "╭─╮", "│ ✓ read form_crop.png │", "╰─╯", "", kittySeq, "", ""];
|
|
709
|
+
const kept = stripEdgesKeepingImages(withImage);
|
|
710
|
+
assert(kept.length === 7, "image: only the leading gap dropped, blank tail kept");
|
|
711
|
+
assert(kept[0] === "╭─╮", "image: leading Spacer removed");
|
|
712
|
+
assert(kept.indexOf(kittySeq) === 4 && kept.length - 5 === 2, "image: rows after the sequence intact");
|
|
713
|
+
assert(
|
|
714
|
+
stripEdgesKeepingImages(["", "a", "", "b", " ", ""]).join() === "a,,b",
|
|
715
|
+
"no image: falls back to plain edge stripping",
|
|
716
|
+
);
|
|
717
|
+
assert(isImageLine(itermSeq) && !isImageLine("│ plain text │"), "image line detection: kitty + iTerm2 only");
|
|
718
|
+
|
|
685
719
|
// tools without their own renderer -> same box (memory_write etc.)
|
|
686
720
|
const noRenderer = { toolName: "memory_write", toolDefinition: {} };
|
|
687
721
|
// tool with no definition (e.g. "Tool glob not found") -> still a box, not plain text
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bismawy/pi-arnative",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "Arnative look for Pi: warm-neutral themes, rounded tool boxes, tabbed header, compact footer, transcript clock, and a /usage dashboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|