@danypops/pi-packed 0.11.0 → 0.12.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`'s progress bar renders inline on this same panel, not a separate popup.
14
+ - `u` / `U` -- update the selected package / update every outdated package, one combined confirmation and one reload. `U`'s progress bar appears inline next to the package currently updating -- the list stays visible throughout, nothing swaps to a separate screen.
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).
@@ -6,18 +6,19 @@
6
6
  * only (x remove -- there is no safe "remove every installed package"
7
7
  * bulk analog, so no X is bound). Adds d disable/enable this package's
8
8
  * extensions, c jump to full resource config, f find/install new
9
- * packages, s settings. The panel itself is a floating overlay
10
- * (`ctx.ui.custom` with overlay:true), and Enter opens a second, smaller
11
- * overlay action menu on top of it. U's batch update renders its progress
12
- * bar inline on this same overlay instead of opening a separate one --
13
- * the list flips to a progress view, then back (or closes, if a reload
14
- * already replaced the session). All data flows through the packed CLI
15
- * (thin seam).
9
+ * packages, s settings. The panel itself is a real bordered (rounded)
10
+ * floating overlay (`ctx.ui.custom` with overlay:true, Malevich's
11
+ * Envelope for the box), anchored to the top so its header stays put and
12
+ * only the footer moves as content height changes. Enter opens a second,
13
+ * smaller overlay action menu on top of it. U's batch update stays on
14
+ * this same package list -- progress renders inline next to the row
15
+ * currently being updated, not as a separate screen. All data flows
16
+ * through the packed CLI (thin seam).
16
17
  */
17
18
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
18
19
  import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
19
- import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
20
- import { Menu, ProgressBar, type MenuItem } from "malevich-tui-components";
20
+ import { Container, Input, Spacer, truncateToWidth } from "@earendil-works/pi-tui";
21
+ import { Envelope, Menu, ProgressBar, type MenuItem } from "malevich-tui-components";
21
22
  import { filterRows, mergeRows, nextMode, visibleRows } from "./model.js";
22
23
  import type { Row, ViewMode } from "./model.js";
23
24
  import type { Natives, PackageResources } from "./packed.js";
@@ -329,9 +330,11 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
329
330
  let searchActive = false;
330
331
  let filtered = visibleRows(rows, mode);
331
332
  let selectedIndex = 0;
332
- // Set only while U's batch update is running -- the installer, rendered
333
- // inline on this same still-open overlay instead of a second one.
334
- let installing: ProgressBar | undefined;
333
+ // Set only while U's batch update is running. The list stays fully
334
+ // visible throughout -- this just names which row the shared bar
335
+ // (below) is currently sitting next to.
336
+ let updatingRowName: string | undefined;
337
+ const updatingBar = new ProgressBar({ value: 0, max: 1, width: 10, style: (s) => theme.fg("accent", s) });
335
338
 
336
339
  const maxVisible = 20;
337
340
 
@@ -341,22 +344,23 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
341
344
  }
342
345
 
343
346
  /** U -- runs the whole approve+update+notify+reload flow without ever
344
- * closing this panel: progress renders on the same overlay via
345
- * `installing`, and rows refresh in place afterward unless a reload
346
- * already ended the session. */
347
+ * closing this panel or replacing the list: each step's progress
348
+ * appears inline next to the row currently being updated, via
349
+ * updatingRowName/updatingBar, which list's own render checks. Rows
350
+ * refresh in place afterward unless a reload already ended the
351
+ * session. */
347
352
  async function runUpdateAllInline(): Promise<void> {
348
353
  const outdated = rows.filter((row) => row.hasUpdate);
349
354
  const outcome = await approveAndRunUpdateAll(outdated, natives, ctx, (batch, approved) => {
350
- const bar = new ProgressBar({ value: 0, max: batch.length, label: `${batch[0]?.name ?? ""} (1/${batch.length})`, style: (s) => theme.fg("accent", s) });
351
- installing = bar;
352
- tui.requestRender();
355
+ updatingBar.setValue(0);
356
+ updatingBar.setMax(batch.length);
353
357
  return performUpdateAll(batch, natives, approved, (event) => {
354
- bar.setLabel(`${event.row.name} (${event.index + 1}/${event.total})`);
355
- if (event.phase === "done") bar.setValue(event.index + 1);
358
+ updatingRowName = event.row.name;
359
+ if (event.phase === "done") updatingBar.setValue(event.index + 1);
356
360
  tui.requestRender();
357
361
  });
358
362
  });
359
- installing = undefined;
363
+ updatingRowName = undefined;
360
364
  if (outcome === "changed") {
361
365
  done(undefined); // ctx.reload() already replaced the session
362
366
  return;
@@ -368,12 +372,15 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
368
372
  tui.requestRender();
369
373
  }
370
374
 
375
+ function panelTitle(): string {
376
+ if (updatingRowName) return "Packages · updating…";
377
+ const outdated = rows.filter((r) => r.hasUpdate).length;
378
+ return outdated > 0 ? `Packages · ${outdated} update(s)` : "Packages";
379
+ }
380
+
371
381
  const header = {
372
382
  invalidate() {},
373
383
  render(width: number): string[] {
374
- const title = theme.bold("Packages");
375
- const outdated = rows.filter((r) => r.hasUpdate).length;
376
- const badge = outdated > 0 ? theme.fg("warning", ` ${outdated} update(s)`) : "";
377
384
  const hint = searchActive
378
385
  ? rawKeyHint("esc", "clear")
379
386
  : rawKeyHint("enter", "menu") +
@@ -391,8 +398,7 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
391
398
  rawKeyHint("s", "settings") +
392
399
  theme.fg("muted", " · ") +
393
400
  rawKeyHint("esc", "close");
394
- const spacing = Math.max(1, width - visibleWidth(title) - visibleWidth(badge) - visibleWidth(hint));
395
- const line1 = truncateToWidth(`${title}${badge}${" ".repeat(spacing)}${hint}`, width, "");
401
+ const line1 = truncateToWidth(hint, width, "");
396
402
  const dot = "·";
397
403
  const line2 = truncateToWidth(
398
404
  theme.fg("muted", `view: ${mode} ${dot} / filter ${dot} tab view ${dot} r refresh ${dot} ${rows.length} installed`),
@@ -421,7 +427,10 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
421
427
  const cursor = selected ? theme.fg("accent", "❯") : " ";
422
428
  const name = selected ? theme.bold(row.name) : row.name;
423
429
  const ver = theme.fg("dim", `@${row.version}`);
424
- const upd = row.hasUpdate ? theme.fg("warning", ` ↑${row.latest}`) : "";
430
+ const isUpdating = updatingRowName === row.name;
431
+ const upd = isUpdating
432
+ ? theme.fg("accent", ` ${updatingBar.format(10)} updating…`)
433
+ : row.hasUpdate ? theme.fg("warning", ` ↑${row.latest}`) : "";
425
434
  lines.push(truncateToWidth(`${cursor} ${name}${ver}${upd}`, width, ""));
426
435
  }
427
436
  const hasScroll = start > 0 || end < filtered.length;
@@ -430,38 +439,30 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
430
439
  },
431
440
  };
432
441
 
433
- const border = () => new DynamicBorder((s) => theme.fg("border", s));
434
-
435
- function buildListContainer(): Container {
436
- const container = new Container();
437
- container.addChild(new Spacer(1));
438
- container.addChild(border());
439
- container.addChild(new Spacer(1));
440
- container.addChild(header);
441
- container.addChild(new Spacer(1));
442
- container.addChild(list);
443
- container.addChild(new Spacer(1));
444
- container.addChild(border());
445
- return container;
446
- }
447
-
448
- function buildInstallingContainer(bar: ProgressBar): Container {
449
- const container = new Container();
450
- container.addChild(new Spacer(1));
451
- container.addChild(border());
452
- container.addChild({ invalidate() {}, render: (_width: number) => [theme.bold("Updating packages")] });
453
- container.addChild(new Spacer(1));
454
- container.addChild(bar);
455
- container.addChild(new Spacer(1));
456
- container.addChild(border());
457
- return container;
458
- }
442
+ // Real bordered box (rounded corners), not just a horizontal rule --
443
+ // title carries the live update badge/status instead of a header line.
444
+ const envelope = new Envelope({
445
+ title: panelTitle(),
446
+ borderStyle: "rounded",
447
+ style: (s) => theme.fg("border", s),
448
+ titleStyle: (s) => theme.bold(theme.fg("accent", s)),
449
+ });
450
+ const body = {
451
+ invalidate() { header.invalidate(); list.invalidate(); },
452
+ render(width: number): string[] {
453
+ return [...header.render(width), "", ...list.render(width)];
454
+ },
455
+ };
456
+ envelope.setContent(body);
459
457
 
460
458
  return {
461
- render: (width: number) => (installing ? buildInstallingContainer(installing) : buildListContainer()).render(width),
462
- invalidate: () => (installing ? buildInstallingContainer(installing) : buildListContainer()).invalidate(),
459
+ render(width: number): string[] {
460
+ envelope.setTitle(panelTitle());
461
+ return envelope.render(width);
462
+ },
463
+ invalidate: () => envelope.invalidate(),
463
464
  handleInput(data: string) {
464
- if (installing) return; // the installer owns the panel until it finishes
465
+ if (updatingRowName) return; // the installer owns input until it finishes
465
466
 
466
467
  if (searchActive) {
467
468
  if (data === "\x1b") {
@@ -545,5 +546,5 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
545
546
  tui.requestRender();
546
547
  },
547
548
  };
548
- }, { overlay: true, overlayOptions: { width: "70%", maxHeight: "70%", anchor: "center" } });
549
+ }, { overlay: true, overlayOptions: { width: "70%", maxHeight: "70%", anchor: "top-center", offsetY: 1 } });
549
550
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.11.0",
3
+ "version": "0.12.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.7.0"
18
+ "malevich-tui-components": "^0.8.0"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "@earendil-works/pi-coding-agent": "*",