@pi-archimedes/ui 2.11.0 → 2.11.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/package.json +2 -2
- package/src/bash/renderer.test.ts +7 -0
- package/src/bash/renderer.ts +5 -2
- package/src/index.test.ts +75 -25
- package/src/index.ts +23 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/ui",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.2",
|
|
4
4
|
"description": "Terminal UI enhancements, styled bash tool rendering, and visual presentation components for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"./config": "./src/config.ts"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@pi-archimedes/core": "2.11.
|
|
31
|
+
"@pi-archimedes/core": "2.11.2"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@earendil-works/pi-ai": "1.2.0",
|
|
@@ -141,6 +141,13 @@ describe("TruncatedTextComponent", () => {
|
|
|
141
141
|
const comp = new TruncatedTextComponent("");
|
|
142
142
|
expect(comp.render(5)).toEqual([""]);
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
it("does not emit \\x1b[0m when truncated so an enclosing Box background is preserved", () => {
|
|
146
|
+
const comp = new TruncatedTextComponent("a".repeat(50));
|
|
147
|
+
const lines = comp.render(10);
|
|
148
|
+
expect(lines[0]!.includes("\x1b[0m")).toBe(false);
|
|
149
|
+
expect(lines[0]!.includes("\x1b[m")).toBe(false);
|
|
150
|
+
});
|
|
144
151
|
});
|
|
145
152
|
|
|
146
153
|
describe("renderBashCall", () => {
|
package/src/bash/renderer.ts
CHANGED
|
@@ -69,8 +69,11 @@ export class TruncatedTextComponent implements Component {
|
|
|
69
69
|
const singleLine = this.text.replace(/[\r\n]+/g, " ");
|
|
70
70
|
const ellipsis = this.theme ? this.theme.fg("accent", "…") : "…";
|
|
71
71
|
const truncated = truncateToWidth(singleLine, width, ellipsis);
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
// truncateToWidth emits \x1b[0m, which resets the enclosing Box background mid-line.
|
|
73
|
+
// Downgrade full resets to fg/style resets so padding spaces keep the container background.
|
|
74
|
+
const safeTruncated = truncated.replace(/\x1b\[0?m/g, "\x1b[22;23;24;27;29;39m");
|
|
75
|
+
const padNeeded = Math.max(0, width - visibleWidth(safeTruncated));
|
|
76
|
+
return [safeTruncated + " ".repeat(padNeeded)];
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
|
package/src/index.test.ts
CHANGED
|
@@ -365,9 +365,10 @@ describe("packages/ui lifecycle and registration", () => {
|
|
|
365
365
|
// plus its own animated indicator) and our border spinner say the same
|
|
366
366
|
// thing — `editorSpinLabel` defaults to "Working" too — so with the spinner
|
|
367
367
|
// on the row is hidden through pi's own API rather than left showing an
|
|
368
|
-
// indicator-less label.
|
|
368
|
+
// indicator-less label. The indicator itself is never written: pi's setter
|
|
369
|
+
// is an unskeyed last-write-wins and belongs to whoever configures it (#147).
|
|
369
370
|
expect(ui.setWorkingVisible).toHaveBeenCalledWith(false);
|
|
370
|
-
expect(ui.setWorkingIndicator).
|
|
371
|
+
expect(ui.setWorkingIndicator).not.toHaveBeenCalled();
|
|
371
372
|
expect(ui.setEditorComponent).toHaveBeenCalledTimes(1);
|
|
372
373
|
const editorFactory = ui.setEditorComponent.mock.calls[0]![0];
|
|
373
374
|
const editorInstance = editorFactory(fakeTui, { borderColor: (s: string) => s } as EditorTheme, {} as KeybindingsManager);
|
|
@@ -428,11 +429,13 @@ describe("packages/ui lifecycle and registration", () => {
|
|
|
428
429
|
|
|
429
430
|
triggerStart(ctx);
|
|
430
431
|
|
|
431
|
-
// No border spinner, so pi's
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
|
|
435
|
-
|
|
432
|
+
// No border spinner, so nothing here touches pi's working row at all: pi's
|
|
433
|
+
// own row and its own animated indicator stand exactly as pi (and any other
|
|
434
|
+
// extension) left them. Writing `setWorkingVisible(true)` would un-hide the
|
|
435
|
+
// row for a user who never asked for it, and for an extension that hid it on
|
|
436
|
+
// purpose to draw its own spinner (#147).
|
|
437
|
+
expect(ui.setWorkingVisible).not.toHaveBeenCalled();
|
|
438
|
+
expect(ui.setWorkingIndicator).not.toHaveBeenCalled();
|
|
436
439
|
expect(ui.setEditorComponent).not.toHaveBeenCalled();
|
|
437
440
|
});
|
|
438
441
|
|
|
@@ -473,7 +476,11 @@ describe("packages/ui lifecycle and registration", () => {
|
|
|
473
476
|
triggerStart(ctx);
|
|
474
477
|
triggerShutdown(ctx);
|
|
475
478
|
|
|
476
|
-
|
|
479
|
+
// The row was hidden by this session (`editorSpinBorder` defaults to on), so
|
|
480
|
+
// hiding it is the one thing handed back. The indicator is not: pi resets it
|
|
481
|
+
// itself before the next session, and our writing it is what overwrote a
|
|
482
|
+
// sibling extension's spinner in 2.11.1 (#147).
|
|
483
|
+
expect(ui.setWorkingIndicator).not.toHaveBeenCalled();
|
|
477
484
|
expect(ui.setHiddenThinkingLabel).toHaveBeenLastCalledWith(undefined);
|
|
478
485
|
expect(ui.setWorkingVisible).toHaveBeenLastCalledWith(true);
|
|
479
486
|
});
|
|
@@ -482,8 +489,11 @@ describe("packages/ui lifecycle and registration", () => {
|
|
|
482
489
|
// lives in the editor's own top border and its default label is "Working" as
|
|
483
490
|
// well. With both on screen the word appeared twice, which is what the
|
|
484
491
|
// maintainer's screenshot shows. The setting that decides is
|
|
485
|
-
// `editorSpinBorder`, and the row
|
|
486
|
-
// `setWorkingVisible()` — `types.d.ts:88`.
|
|
492
|
+
// `editorSpinBorder`, and hiding the row goes through pi's own
|
|
493
|
+
// `setWorkingVisible()` — `types.d.ts:88`. Only that one direction is ours to
|
|
494
|
+
// write: pi's working-row setters carry no key or scope (`types.d.ts`
|
|
495
|
+
// `setWorkingIndicator` is a plain assignment, `interactive-mode.js:1831-1837`),
|
|
496
|
+
// so anything more we assert overwrites whichever extension wrote last (#147).
|
|
487
497
|
describe("pi's working row and the border spinner", () => {
|
|
488
498
|
it("hides pi's working row while the border spinner carries the announcement", () => {
|
|
489
499
|
vi.mocked(loadUIConfig).mockReturnValue({ ...DEFAULT_UI_CONFIG, editorSpinBorder: true });
|
|
@@ -493,29 +503,31 @@ describe("packages/ui lifecycle and registration", () => {
|
|
|
493
503
|
triggerStart(ctx);
|
|
494
504
|
|
|
495
505
|
expect(ui.setWorkingVisible).toHaveBeenLastCalledWith(false);
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
expect(ui.setWorkingIndicator).
|
|
500
|
-
expect(ui.setWorkingIndicator).not.toHaveBeenCalledWith({ frames: [] });
|
|
506
|
+
// Nothing about the indicator is ours to write — neither `frames: []` nor
|
|
507
|
+
// the `undefined` that means "pi's default spinner". Whoever configures a
|
|
508
|
+
// spinner owns it; pi restores its own default per session.
|
|
509
|
+
expect(ui.setWorkingIndicator).not.toHaveBeenCalled();
|
|
501
510
|
});
|
|
502
511
|
|
|
503
|
-
it("
|
|
512
|
+
it("leaves pi's working row and indicator entirely alone when the spinner is off", () => {
|
|
504
513
|
vi.mocked(loadUIConfig).mockReturnValue({ ...DEFAULT_UI_CONFIG, editorSpinBorder: false });
|
|
505
514
|
const { triggerStart } = setupHarness();
|
|
506
515
|
const { ctx, ui } = makeCtx(true);
|
|
507
516
|
|
|
508
517
|
triggerStart(ctx);
|
|
509
518
|
|
|
510
|
-
expect(ui.setWorkingVisible).
|
|
511
|
-
expect(ui.setWorkingIndicator).
|
|
519
|
+
expect(ui.setWorkingVisible).not.toHaveBeenCalled();
|
|
520
|
+
expect(ui.setWorkingIndicator).not.toHaveBeenCalled();
|
|
512
521
|
});
|
|
513
522
|
|
|
514
|
-
it("
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
523
|
+
it("reads the setting per session and asserts only the direction it asks for", () => {
|
|
524
|
+
// The setting can change between sessions, so the answer is read from the
|
|
525
|
+
// config every time rather than remembered from the first install — and
|
|
526
|
+
// only the `false` direction is ever written. The row coming back is pi's
|
|
527
|
+
// job, not ours: `resetExtensionUI()` sets `workingVisible = true`
|
|
528
|
+
// (`interactive-mode.js:1921`) on startup defaults and on every session
|
|
529
|
+
// replacement and `/reload`, so a spinner-off session needs no handback
|
|
530
|
+
// from us and cannot be asserted from here.
|
|
519
531
|
vi.mocked(loadUIConfig).mockReturnValue({ ...DEFAULT_UI_CONFIG, editorSpinBorder: true });
|
|
520
532
|
const { triggerStart } = setupHarness();
|
|
521
533
|
const spinnerOn = makeCtx(true);
|
|
@@ -525,10 +537,11 @@ describe("packages/ui lifecycle and registration", () => {
|
|
|
525
537
|
const spinnerOff = makeCtx(true);
|
|
526
538
|
triggerStart(spinnerOff.ctx);
|
|
527
539
|
|
|
528
|
-
// Two sessions, two answers: the row
|
|
540
|
+
// Two sessions, two answers: the row was hidden for the first and left
|
|
541
|
+
// completely untouched for the second.
|
|
529
542
|
const calls = spinnerOn.ui.setWorkingVisible.mock.calls.map((c) => c[0]);
|
|
530
543
|
expect(calls).toEqual([false]);
|
|
531
|
-
expect(spinnerOff.ui.setWorkingVisible.mock.calls.map((c) => c[0])).toEqual([
|
|
544
|
+
expect(spinnerOff.ui.setWorkingVisible.mock.calls.map((c) => c[0])).toEqual([]);
|
|
532
545
|
});
|
|
533
546
|
|
|
534
547
|
it("hides the row for a session installed on a second pi handle", () => {
|
|
@@ -548,6 +561,43 @@ describe("packages/ui lifecycle and registration", () => {
|
|
|
548
561
|
expect(firstCtx.ui.setWorkingVisible).toHaveBeenLastCalledWith(false);
|
|
549
562
|
expect(secondCtx.ui.setWorkingVisible).toHaveBeenLastCalledWith(false);
|
|
550
563
|
});
|
|
564
|
+
|
|
565
|
+
it("leaves a sibling extension's custom working indicator alone (#147)", () => {
|
|
566
|
+
vi.mocked(loadUIConfig).mockReturnValue({ ...DEFAULT_UI_CONFIG, editorSpinBorder: false });
|
|
567
|
+
const { pi, listeners } = makePi();
|
|
568
|
+
const { ctx, ui } = makeCtx(true);
|
|
569
|
+
|
|
570
|
+
// Stands in for another extension in `~/.pi/agent/extensions/`: it configures
|
|
571
|
+
// pi's spinner on session start, and pi gives no key or scoping to that call.
|
|
572
|
+
// Registered before ours because `setupHarness()` cannot be used here — it
|
|
573
|
+
// registers ours first — and pi does not sort extension files, so whichever
|
|
574
|
+
// order `readdirSync` yields is the order the handlers run in.
|
|
575
|
+
pi.on("session_start", (_event, siblingCtx) => {
|
|
576
|
+
siblingCtx.ui.setWorkingIndicator({ frames: ["◐"], intervalMs: 80 });
|
|
577
|
+
});
|
|
578
|
+
registerUI(pi);
|
|
579
|
+
|
|
580
|
+
for (const handler of listeners["session_start"] ?? []) handler(null, ctx);
|
|
581
|
+
|
|
582
|
+
// Exactly one call — the sibling's — and nothing of ours after it. Asserting
|
|
583
|
+
// the count (not just "never called with undefined") is what catches a
|
|
584
|
+
// handback written as `setWorkingIndicator()` with no argument at all.
|
|
585
|
+
expect(ui.setWorkingIndicator).toHaveBeenCalledTimes(1);
|
|
586
|
+
expect(ui.setWorkingIndicator).toHaveBeenCalledWith({ frames: ["◐"], intervalMs: 80 });
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it("does not assert the working row visible on shutdown when the spinner was never on", () => {
|
|
590
|
+
vi.mocked(loadUIConfig).mockReturnValue({ ...DEFAULT_UI_CONFIG, editorSpinBorder: false });
|
|
591
|
+
const { triggerStart, triggerShutdown } = setupHarness();
|
|
592
|
+
const { ctx, ui } = makeCtx(true);
|
|
593
|
+
|
|
594
|
+
triggerStart(ctx);
|
|
595
|
+
triggerShutdown(ctx);
|
|
596
|
+
|
|
597
|
+
// Nothing was taken, so nothing is returned — an unconditional
|
|
598
|
+
// `setWorkingVisible(true)` here is the second half of the same overwrite bug.
|
|
599
|
+
expect(ui.setWorkingVisible).not.toHaveBeenCalled();
|
|
600
|
+
});
|
|
551
601
|
});
|
|
552
602
|
|
|
553
603
|
it("registers no bash tool and yields pi's native row when bashToolStyling is false", () => {
|
package/src/index.ts
CHANGED
|
@@ -139,13 +139,15 @@ export function registerUI(pi: ExtensionAPI): void {
|
|
|
139
139
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
140
140
|
const targetCtx = ctx?.hasUI ? ctx : uiCtx;
|
|
141
141
|
if (targetCtx?.hasUI) {
|
|
142
|
-
// Give
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
// Give back what this session took: the row we hid for the border spinner,
|
|
143
|
+
// and only if we hid it. pi resets the indicator and the row's visibility
|
|
144
|
+
// itself before the next session starts (`resetExtensionUI()`), and asserting
|
|
145
|
+
// a default here would overwrite another extension's choice — so the
|
|
146
|
+
// indicator is not touched on the way out either (#147, ADR 0031).
|
|
147
|
+
if (spinFlag) targetCtx.ui.setWorkingVisible(true);
|
|
148
|
+
// These two ARE ours to hand back: `labelText` is an archimedes setting and
|
|
149
|
+
// the editor component is ours outright, so both calls undo state this
|
|
150
|
+
// session demonstrably took rather than asserting a default.
|
|
149
151
|
targetCtx.ui.setHiddenThinkingLabel(undefined);
|
|
150
152
|
targetCtx.ui.setEditorComponent(undefined);
|
|
151
153
|
}
|
|
@@ -304,27 +306,23 @@ export function registerUI(pi: ExtensionAPI): void {
|
|
|
304
306
|
// One announcement, in one place. pi's working row writes "Working" above
|
|
305
307
|
// the editor and our border spinner writes its label — `editorSpinLabel`
|
|
306
308
|
// defaults to "Working" as well — into the editor's own top border, so with
|
|
307
|
-
// both on screen the word appeared twice. The border is ours, so
|
|
308
|
-
//
|
|
309
|
-
//
|
|
309
|
+
// both on screen the word appeared twice. The border is ours, so while it
|
|
310
|
+
// spins the row is hidden through pi's own switch (`setWorkingVisible()`,
|
|
311
|
+
// `types.d.ts:88`).
|
|
310
312
|
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
313
|
+
// Only the direction this setting asks for is written. `setWorkingIndicator`
|
|
314
|
+
// is never called, and the row is never asserted visible: pi restores both
|
|
315
|
+
// itself wherever they could have been altered (`resetExtensionUI()`:
|
|
316
|
+
// `workingVisible = true` at `interactive-mode.js:1921`, `setWorkingIndicator()`
|
|
317
|
+
// at `:1922`, reached by startup defaults and by `/new`, `/resume`, `/fork`
|
|
318
|
+
// and `/reload`), and asserting either would overwrite another extension's
|
|
319
|
+
// choice — pi's working-row setters are an unskeyed last-write-wins
|
|
320
|
+
// (`:1831-1837`). Writing `setWorkingIndicator(undefined)` unconditionally is
|
|
321
|
+
// how 2.11.1 deleted a user's customised spinner (#147, ADR 0031).
|
|
319
322
|
//
|
|
320
323
|
// Read per session rather than remembered: `editorSpinBorder` can change
|
|
321
|
-
// between sessions, and pi
|
|
322
|
-
|
|
323
|
-
// toggle needs no keying on the `pi` handle the way a registration does —
|
|
324
|
-
// it is one idempotent setter call inside the `session_start` handler, and
|
|
325
|
-
// every handle gets its own handler.
|
|
326
|
-
ctx.ui.setWorkingVisible(!spinFlag);
|
|
327
|
-
ctx.ui.setWorkingIndicator(undefined);
|
|
324
|
+
// between sessions, and every `pi` handle gets its own handler.
|
|
325
|
+
if (spinFlag) ctx.ui.setWorkingVisible(false);
|
|
328
326
|
|
|
329
327
|
// The word a collapsed thinking block shows is pi's own row, so the
|
|
330
328
|
// setting is handed to pi rather than rebuilt here. An empty setting
|