@danypops/pi-packed 0.11.0 → 0.13.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,24 @@
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. Rows render through
16
+ * Malevich's Table (real column-aligned Package/Version/status cells,
17
+ * per-row selection styling baked into each cell since Table's own
18
+ * cellStyle is column-wide, not row-wide) inside this panel's own
19
+ * scroll-window slice -- Table deliberately owns no pagination of its
20
+ * own, so the visible-window-around-selectedIndex math stays here. All
21
+ * data flows through the packed CLI (thin seam).
16
22
  */
17
23
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
18
24
  import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
19
25
  import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
20
- import { Menu, ProgressBar, type MenuItem } from "malevich-tui-components";
26
+ import { Envelope, Menu, ProgressBar, Table, type MenuItem, type TableColumn, type TextMeasure } from "malevich-tui-components";
21
27
  import { filterRows, mergeRows, nextMode, visibleRows } from "./model.js";
22
28
  import type { Row, ViewMode } from "./model.js";
23
29
  import type { Natives, PackageResources } from "./packed.js";
@@ -329,9 +335,11 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
329
335
  let searchActive = false;
330
336
  let filtered = visibleRows(rows, mode);
331
337
  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;
338
+ // Set only while U's batch update is running. The list stays fully
339
+ // visible throughout -- this just names which row the shared bar
340
+ // (below) is currently sitting next to.
341
+ let updatingRowName: string | undefined;
342
+ const updatingBar = new ProgressBar({ value: 0, max: 1, width: 10, style: (s) => theme.fg("accent", s) });
335
343
 
336
344
  const maxVisible = 20;
337
345
 
@@ -341,22 +349,23 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
341
349
  }
342
350
 
343
351
  /** 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. */
352
+ * closing this panel or replacing the list: each step's progress
353
+ * appears inline next to the row currently being updated, via
354
+ * updatingRowName/updatingBar, which list's own render checks. Rows
355
+ * refresh in place afterward unless a reload already ended the
356
+ * session. */
347
357
  async function runUpdateAllInline(): Promise<void> {
348
358
  const outdated = rows.filter((row) => row.hasUpdate);
349
359
  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();
360
+ updatingBar.setValue(0);
361
+ updatingBar.setMax(batch.length);
353
362
  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);
363
+ updatingRowName = event.row.name;
364
+ if (event.phase === "done") updatingBar.setValue(event.index + 1);
356
365
  tui.requestRender();
357
366
  });
358
367
  });
359
- installing = undefined;
368
+ updatingRowName = undefined;
360
369
  if (outcome === "changed") {
361
370
  done(undefined); // ctx.reload() already replaced the session
362
371
  return;
@@ -368,12 +377,15 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
368
377
  tui.requestRender();
369
378
  }
370
379
 
380
+ function panelTitle(): string {
381
+ if (updatingRowName) return "Packages · updating…";
382
+ const outdated = rows.filter((r) => r.hasUpdate).length;
383
+ return outdated > 0 ? `Packages · ${outdated} update(s)` : "Packages";
384
+ }
385
+
371
386
  const header = {
372
387
  invalidate() {},
373
388
  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
389
  const hint = searchActive
378
390
  ? rawKeyHint("esc", "clear")
379
391
  : rawKeyHint("enter", "menu") +
@@ -391,8 +403,7 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
391
403
  rawKeyHint("s", "settings") +
392
404
  theme.fg("muted", " · ") +
393
405
  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, "");
406
+ const line1 = truncateToWidth(hint, width, "");
396
407
  const dot = "·";
397
408
  const line2 = truncateToWidth(
398
409
  theme.fg("muted", `view: ${mode} ${dot} / filter ${dot} tab view ${dot} r refresh ${dot} ${rows.length} installed`),
@@ -403,6 +414,19 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
403
414
  },
404
415
  };
405
416
 
417
+ // pi-tui's own visibleWidth/truncateToWidth are ANSI-aware (they strip
418
+ // escape codes for measurement, matching the pair Malevich's TextMeasure
419
+ // port expects) -- rows below bake selection/status styling directly
420
+ // into each cell's text since Table's own cellStyle is column-wide, not
421
+ // per-row.
422
+ const measure: TextMeasure = { visibleWidth, truncateToWidth };
423
+ const columns: TableColumn[] = [
424
+ { header: "Package", key: "name" },
425
+ { header: "Version", key: "version" },
426
+ { header: "", key: "status" },
427
+ ];
428
+ const table = new Table({ columns, rows: [], measure, headerStyle: (s) => theme.fg("muted", s) });
429
+
406
430
  const list = {
407
431
  invalidate() {},
408
432
  render(width: number): string[] {
@@ -413,55 +437,55 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
413
437
  lines.push(theme.fg("muted", " No packages"));
414
438
  return lines;
415
439
  }
440
+ // No scrolling of its own (Malevich's Table is deliberately
441
+ // unopinionated about pagination) -- the visible window around
442
+ // selectedIndex stays this panel's own job, same as before.
416
443
  const start = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), filtered.length - maxVisible));
417
444
  const end = Math.min(start + maxVisible, filtered.length);
418
- for (let i = start; i < end; i++) {
419
- const row = filtered[i]!;
420
- const selected = i === selectedIndex;
421
- const cursor = selected ? theme.fg("accent", "❯") : " ";
422
- const name = selected ? theme.bold(row.name) : row.name;
423
- const ver = theme.fg("dim", `@${row.version}`);
424
- const upd = row.hasUpdate ? theme.fg("warning", ` ↑${row.latest}`) : "";
425
- lines.push(truncateToWidth(`${cursor} ${name}${ver}${upd}`, width, ""));
426
- }
445
+ table.setRows(
446
+ filtered.slice(start, end).map((row, offset) => {
447
+ const i = start + offset;
448
+ const selected = i === selectedIndex;
449
+ const cursor = selected ? theme.fg("accent", "❯ ") : " ";
450
+ const name = selected ? theme.bold(row.name) : row.name;
451
+ const isUpdating = updatingRowName === row.name;
452
+ const status = isUpdating
453
+ ? theme.fg("accent", `${updatingBar.format(10)} updating…`)
454
+ : row.hasUpdate ? theme.fg("warning", `↑${row.latest}`) : "";
455
+ return { name: `${cursor}${name}`, version: theme.fg("dim", row.version), status };
456
+ }),
457
+ );
458
+ lines.push(...table.render(width));
427
459
  const hasScroll = start > 0 || end < filtered.length;
428
460
  lines.push(theme.fg("dim", ` ${hasScroll ? `${selectedIndex + 1}/${filtered.length} ` : ""}${mode}`));
429
461
  return lines;
430
462
  },
431
463
  };
432
464
 
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
- }
465
+ // Real bordered box (rounded corners), not just a horizontal rule --
466
+ // title carries the live update badge/status instead of a header line.
467
+ const envelope = new Envelope({
468
+ title: panelTitle(),
469
+ borderStyle: "rounded",
470
+ style: (s) => theme.fg("border", s),
471
+ titleStyle: (s) => theme.bold(theme.fg("accent", s)),
472
+ });
473
+ const body = {
474
+ invalidate() { header.invalidate(); list.invalidate(); },
475
+ render(width: number): string[] {
476
+ return [...header.render(width), "", ...list.render(width)];
477
+ },
478
+ };
479
+ envelope.setContent(body);
459
480
 
460
481
  return {
461
- render: (width: number) => (installing ? buildInstallingContainer(installing) : buildListContainer()).render(width),
462
- invalidate: () => (installing ? buildInstallingContainer(installing) : buildListContainer()).invalidate(),
482
+ render(width: number): string[] {
483
+ envelope.setTitle(panelTitle());
484
+ return envelope.render(width);
485
+ },
486
+ invalidate: () => envelope.invalidate(),
463
487
  handleInput(data: string) {
464
- if (installing) return; // the installer owns the panel until it finishes
488
+ if (updatingRowName) return; // the installer owns input until it finishes
465
489
 
466
490
  if (searchActive) {
467
491
  if (data === "\x1b") {
@@ -545,5 +569,5 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
545
569
  tui.requestRender();
546
570
  },
547
571
  };
548
- }, { overlay: true, overlayOptions: { width: "70%", maxHeight: "70%", anchor: "center" } });
572
+ }, { overlay: true, overlayOptions: { width: "70%", maxHeight: "70%", anchor: "top-center", offsetY: 1 } });
549
573
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.11.0",
3
+ "version": "0.13.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": "*",