@danypops/pi-packed 0.15.0 → 0.16.0

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/README.md CHANGED
@@ -11,7 +11,7 @@ The extension connects to Packed's authenticated user daemon and starts the pack
11
11
  ## Commands
12
12
 
13
13
  - `/packed` -- a floating overlay panel: every installed Pi package, with update availability. Mnemonics follow lazy.nvim's own convention (uppercase acts on every row, lowercase on the one under the cursor):
14
- - `u` / `U` -- update the selected package / update every outdated package, one combined confirmation and one reload. `U` shows a spinner inline next to the package currently updating, settling into a real ✓ or ✗ plus a short tail of that update's own captured output once it finishes -- the list stays visible throughout, nothing swaps to a separate screen. A reload is confirmed separately from the update itself -- decline it to defer: the update already happened, only picking it up in this Pi session is deferred until `/reload`.
14
+ - `u` / `U` -- update the selected package / update every outdated package, one combined confirmation and one reload. `U` shows a spinner inline next to the package currently updating, settling into a real ✓ or ✗ plus a short tail of that update's own captured output once it finishes -- the list stays visible throughout, nothing swaps to a separate screen. A reload is confirmed separately from the update itself -- decline it to defer: the update already happened, only picking it up in this Pi session is deferred until `/reload`. Every confirm in this flow -- approval and reload alike -- renders inline on this same panel (`y`/`n` keys), never a separate popup.
15
15
  - `x` -- remove the selected package.
16
16
  - `d` -- disable (or re-enable) the selected package's own extensions.
17
17
  - `c` -- jump to resource config for the selected package (skills, prompts, themes).
@@ -1,5 +1,5 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import type { MenuTheme } from "malevich-tui-components";
2
+ import type { DialogTheme, MenuTheme } from "malevich-tui-components";
3
3
 
4
4
  /** Maps Pi's own Theme onto Malevich's Menu -- the one place this mapping
5
5
  * exists, shared by every ctx.ui.custom overlay menu in this extension. */
@@ -12,3 +12,16 @@ export function menuTheme(theme: Theme): MenuTheme {
12
12
  title: (s) => theme.fg("accent", s),
13
13
  };
14
14
  }
15
+
16
+ /** Maps Pi's own Theme onto Malevich's Dialog -- used for an inline y/n
17
+ * decision (e.g. confirmReload) rendered as part of an already-open
18
+ * ctx.ui.custom overlay's own content, instead of ctx.ui.confirm's separate,
19
+ * arrow-select-only native dialog. */
20
+ export function dialogTheme(theme: Theme): DialogTheme {
21
+ return {
22
+ border: (s) => theme.fg("border", s),
23
+ title: (s) => theme.bold(theme.fg("accent", s)),
24
+ body: (s) => s,
25
+ dim: (s) => theme.fg("muted", s),
26
+ };
27
+ }
@@ -14,26 +14,31 @@
14
14
  * was pinned 1 row below the absolute top on any terminal size). Enter
15
15
  * opens a second, smaller overlay action menu on top of it. U's batch
16
16
  * update stays on this same package list -- an indeterminate spinner
17
- * (Spinner, this package's own -- neither Malevich nor pi-tui exposes an
18
- * embeddable one) renders inline next to the row currently updating,
19
- * settling into a real ✓/✗ plus a bounded tail of that row's own actual
20
- * captured stdout/stderr once it finishes, never a determinate bar (a
21
- * single subprocess call has no knowable percentage). Rows render through
22
- * Malevich's Table (real column-aligned Package/Version/status cells,
23
- * per-row selection styling baked into each cell since Table's own
24
- * cellStyle is column-wide, not row-wide) inside this panel's own
25
- * scroll-window slice -- Table deliberately owns no pagination of its
26
- * own, so the visible-window-around-selectedIndex math stays here. Every
27
- * mutation that actually changes something asks confirmReload separately
28
- * from the earlier mutation-approval confirm -- declining defers the
29
- * reload (the mutation itself already happened) and keeps the panel open
30
- * with refreshed rows instead of ending the session. All
31
- * data flows through the packed CLI (thin seam).
17
+ * (Malevich's Spinner, extracted upstream from this panel's original need
18
+ * once no embeddable one existed anywhere) renders inline next to the row
19
+ * currently updating, settling into a real ✓/✗ plus a bounded tail of
20
+ * that row's own actual captured stdout/stderr once it finishes, never a
21
+ * determinate bar (a single subprocess call has no knowable percentage).
22
+ * Rows render through Malevich's Table (real column-aligned
23
+ * Package/Version/status cells, per-row selection styling baked into each
24
+ * cell since Table's own cellStyle is column-wide, not row-wide) inside
25
+ * this panel's own scroll-window slice -- Table deliberately owns no
26
+ * pagination of its own, so the visible-window-around-selectedIndex math
27
+ * stays here. u/x/d and the Enter action menu all run their whole
28
+ * approve+mutate+confirmReload flow inline, without ever closing this
29
+ * overlay first -- every confirm() along the way (mutation approval,
30
+ * confirmReload's separate reload gate) renders as a real Malevich Dialog
31
+ * on this SAME overlay, dispatched by literal y/n keys, instead of
32
+ * ctx.ui.confirm's own separate native dialog (arrow-select only, and a
33
+ * genuinely different overlay stacked on top -- confirmed live as a real
34
+ * user complaint). Declining a reload defers it: the mutation itself
35
+ * already happened, and the panel stays open with refreshed rows instead
36
+ * of ending the session. All data flows through the packed CLI (thin seam).
32
37
  */
33
38
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
34
39
  import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
35
40
  import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
36
- import { Envelope, Menu, Table, type MenuItem, type TableColumn, type TextMeasure } from "malevich-tui-components";
41
+ import { Dialog, Envelope, Menu, Spinner, Table, type MenuItem, type TableColumn, type TextMeasure } from "malevich-tui-components";
37
42
  import { filterRows, mergeRows, nextMode, visibleRows } from "./model.js";
38
43
  import type { Row, ViewMode } from "./model.js";
39
44
  import type { Natives, PackageResources } from "./packed.js";
@@ -41,12 +46,11 @@ import { approvePackageOperation } from "./tools.js";
41
46
  import { showPackedSettings } from "./security-tui.js";
42
47
  import { showResourceConfig, applyResourceToggle } from "./resource-config.js";
43
48
  import { showDiscoverPanel } from "./discover.js";
44
- import { menuTheme } from "./menu-theme.js";
49
+ import { dialogTheme, menuTheme } from "./menu-theme.js";
45
50
  import { confirmReload } from "./reload.js";
46
- import { Spinner } from "./spinner.js";
47
51
 
48
52
  interface PanelAction {
49
- type: "update" | "remove" | "disable" | "config" | "find" | "refresh" | "settings";
53
+ type: "config" | "find" | "refresh" | "settings";
50
54
  row?: Row;
51
55
  }
52
56
 
@@ -366,8 +370,10 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
366
370
  }
367
371
 
368
372
  // Panel loop: actions resolve the component, run outside it, then reopen.
369
- // U/updateAll is handled entirely inside renderPanel itself (progress
370
- // rendered on the same already-open overlay) and never reaches here.
373
+ // Every mutation (u/x/d, U, and whatever the Enter action menu picks) is
374
+ // handled entirely inside renderPanel itself -- approval and reload
375
+ // confirms render inline on the same already-open overlay -- and never
376
+ // reaches here; only non-mutation navigation resolves this loop.
371
377
  for (;;) {
372
378
  const action = await renderPanel(ctx, natives, rows);
373
379
  if (!action) return; // closed
@@ -387,24 +393,8 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
387
393
  continue; // a successful install already reloaded; a no-op returns here
388
394
  }
389
395
 
390
- if (action.type === "config") {
391
- await showResourceConfig(ctx, natives, action.row?.name);
392
- continue; // showResourceConfig already handles its own reload prompt
393
- }
394
-
395
- const row = action.row;
396
- if (!row) continue;
397
-
398
- if (action.type === "disable") {
399
- const outcome = await applyDisableExtensions(row, natives, ctx);
400
- if (outcome === "changed") return;
401
- if (outcome === "deferred") await refreshRows();
402
- continue;
403
- }
404
-
405
- const outcome = await applyPackageChoice(action.type === "update" ? `Update to ${row.latest}` : "Remove", row, natives, ctx);
406
- if (outcome === "changed") return; // ctx.reload() already replaced the session
407
- if (outcome === "deferred") await refreshRows();
396
+ await showResourceConfig(ctx, natives, action.row?.name);
397
+ // showResourceConfig already handles its own reload prompt
408
398
  }
409
399
  }
410
400
 
@@ -434,6 +424,60 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
434
424
  selectedIndex = 0;
435
425
  }
436
426
 
427
+ // A y/n decision rendered as this SAME overlay's own content (via a real
428
+ // Malevich Dialog, dispatched by literal key press) instead of
429
+ // ctx.ui.confirm's separate native dialog -- confirmed live as a real
430
+ // user complaint: ctx.ui.confirm opens as its own distinct overlay on
431
+ // top of this one ("a new window"), arrow-select only, no y/n keys.
432
+ let pendingDialog: Dialog | undefined;
433
+
434
+ function confirmInline(title: string, message: string): Promise<boolean> {
435
+ return new Promise((resolve) => {
436
+ const settle = (value: boolean) => {
437
+ pendingDialog = undefined;
438
+ tui.requestRender();
439
+ resolve(value);
440
+ };
441
+ pendingDialog = new Dialog({
442
+ title,
443
+ body: message,
444
+ actions: [
445
+ { label: "Yes", key: "y", action: () => settle(true) },
446
+ { label: "No", key: "n", action: () => settle(false) },
447
+ ],
448
+ theme: dialogTheme(theme),
449
+ });
450
+ tui.requestRender();
451
+ });
452
+ }
453
+
454
+ // Every confirm() call inside a mutation flow driven from this open
455
+ // overlay (approvePackageOperation's own approval, confirmReload's
456
+ // separate reload gate) routes through confirmInline instead of the
457
+ // real ctx.ui.confirm -- notify/reload/etc. stay the genuine ctx.
458
+ const inlineCtx: ExtensionCommandContext = { ...ctx, hasUI: true, ui: { ...ctx.ui, confirm: confirmInline } };
459
+
460
+ /** u/x/d and the Enter action menu all funnel through here: approval and
461
+ * reload confirms render inline (via inlineCtx/confirmInline) on this
462
+ * same overlay, never closing it first the way done()-dispatch used to. */
463
+ async function runRowActionInline(action: { type: "update" | "remove" | "disable"; row: Row }): Promise<void> {
464
+ updatingRowName = action.row.name;
465
+ tui.requestRender();
466
+ const outcome = action.type === "disable"
467
+ ? await applyDisableExtensions(action.row, natives, inlineCtx)
468
+ : await applyPackageChoice(action.type === "update" ? `Update to ${action.row.latest}` : "Remove", action.row, natives, inlineCtx);
469
+ updatingRowName = undefined;
470
+ if (outcome === "changed") {
471
+ done(undefined); // ctx.reload() already replaced the session
472
+ return;
473
+ }
474
+ const reloaded = await loadRows(natives);
475
+ if (reloaded.error) ctx.ui.notify(`refresh failed: ${reloaded.error}`, "error");
476
+ else rows = reloaded.rows;
477
+ applyFilter();
478
+ tui.requestRender();
479
+ }
480
+
437
481
  /** U -- runs the whole approve+update+notify+reload flow without ever
438
482
  * closing this panel or replacing the list: each row's own settled
439
483
  * outcome (spinner while in flight, then a real ✓/✗ plus a bounded tail
@@ -443,7 +487,7 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
443
487
  async function runUpdateAllInline(): Promise<void> {
444
488
  const outdated = rows.filter((row) => row.hasUpdate);
445
489
  settled.clear();
446
- const outcome = await approveAndRunUpdateAll(outdated, natives, ctx, (batch, approved) => {
490
+ const outcome = await approveAndRunUpdateAll(outdated, natives, inlineCtx, (batch, approved) => {
447
491
  spinner.start(() => tui.requestRender());
448
492
  return performUpdateAll(batch, natives, approved, (event) => {
449
493
  updatingRowName = event.row.name;
@@ -583,11 +627,17 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
583
627
  return {
584
628
  render(width: number): string[] {
585
629
  envelope.setTitle(panelTitle());
630
+ envelope.setContent(pendingDialog ?? body);
586
631
  return envelope.render(width);
587
632
  },
588
633
  invalidate: () => envelope.invalidate(),
589
634
  handleInput(data: string) {
590
- if (updatingRowName) return; // the installer owns input until it finishes
635
+ if (pendingDialog) {
636
+ pendingDialog.handleInput(data);
637
+ tui.requestRender();
638
+ return;
639
+ }
640
+ if (updatingRowName) return; // the mutation/installer owns input until it finishes
591
641
 
592
642
  if (searchActive) {
593
643
  if (data === "\x1b") {
@@ -632,17 +682,17 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
632
682
  return;
633
683
  case "u": {
634
684
  const row = filtered[selectedIndex];
635
- if (row?.hasUpdate) done({ type: "update", row });
685
+ if (row?.hasUpdate) void runRowActionInline({ type: "update", row });
636
686
  return;
637
687
  }
638
688
  case "x": {
639
689
  const row = filtered[selectedIndex];
640
- if (row) done({ type: "remove", row });
690
+ if (row) void runRowActionInline({ type: "remove", row });
641
691
  return;
642
692
  }
643
693
  case "d": {
644
694
  const row = filtered[selectedIndex];
645
- if (row) done({ type: "disable", row });
695
+ if (row) void runRowActionInline({ type: "disable", row });
646
696
  return;
647
697
  }
648
698
  case "c": {
@@ -655,9 +705,7 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
655
705
  if (!row) return;
656
706
  void (async () => {
657
707
  const choice = await showActionMenu(ctx, row);
658
- if (choice === "update") done({ type: "update", row });
659
- else if (choice === "remove") done({ type: "remove", row });
660
- else if (choice === "disable") done({ type: "disable", row });
708
+ if (choice === "update" || choice === "remove" || choice === "disable") void runRowActionInline({ type: choice, row });
661
709
  else if (choice === "config") done({ type: "config", row });
662
710
  })();
663
711
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Pi package tools, commands, profiles, and TUI for the Packed daemon",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -15,7 +15,7 @@
15
15
  "@danypops/packed": "^0.2.0",
16
16
  "@danypops/vehicle-client": "^0.1.1",
17
17
  "@danypops/vehicle-client-pi": "^0.1.5",
18
- "malevich-tui-components": "^0.8.0"
18
+ "malevich-tui-components": "^0.11.0"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "@earendil-works/pi-coding-agent": "*",
@@ -1,44 +0,0 @@
1
- /**
2
- * spinner.ts — a tiny, testable indeterminate-progress ticker for a
3
- * surface with no discrete step count (a single in-flight subprocess
4
- * call, unlike ProgressBar's own known N-of-M batch position). tick() is
5
- * the pure, synchronously-testable core; start()/stop() wire it to a real
6
- * interval for genuine on-screen animation. pi-tui's own Loader component
7
- * does the identical thing internally but keeps its current frame private
8
- * (it owns a whole Text line), so it can't be embedded inline in a
9
- * caller's own row the way this one is -- hence this small local one.
10
- */
11
- const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
12
- const INTERVAL_MS = 80;
13
-
14
- export class Spinner {
15
- private index = 0;
16
- private timer: ReturnType<typeof setInterval> | undefined;
17
-
18
- /** Current animation frame -- a single braille glyph. */
19
- glyph(): string {
20
- return FRAMES[this.index]!;
21
- }
22
-
23
- /** Advances one frame. Pure and synchronous -- the deterministic unit under test; start() is just this wired to a real timer. */
24
- tick(): void {
25
- this.index = (this.index + 1) % FRAMES.length;
26
- }
27
-
28
- /** Wires tick() to a real interval, calling onTick after each advance so a host can requestRender(). Idempotent -- calling start() again restarts cleanly rather than stacking a second interval. */
29
- start(onTick: () => void): void {
30
- this.stop();
31
- this.timer = setInterval(() => {
32
- this.tick();
33
- onTick();
34
- }, INTERVAL_MS);
35
- }
36
-
37
- /** Safe to call even if never started, or more than once. */
38
- stop(): void {
39
- if (this.timer !== undefined) {
40
- clearInterval(this.timer);
41
- this.timer = undefined;
42
- }
43
- }
44
- }