@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 +1 -1
- package/extension/src/tui.ts +88 -64
- package/package.json +2 -2
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
|
|
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).
|
package/extension/src/tui.ts
CHANGED
|
@@ -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
|
|
10
|
-
* (`ctx.ui.custom` with overlay:true
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
333
|
-
//
|
|
334
|
-
|
|
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
|
|
345
|
-
*
|
|
346
|
-
*
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
tui.requestRender();
|
|
360
|
+
updatingBar.setValue(0);
|
|
361
|
+
updatingBar.setMax(batch.length);
|
|
353
362
|
return performUpdateAll(batch, natives, approved, (event) => {
|
|
354
|
-
|
|
355
|
-
if (event.phase === "done")
|
|
363
|
+
updatingRowName = event.row.name;
|
|
364
|
+
if (event.phase === "done") updatingBar.setValue(event.index + 1);
|
|
356
365
|
tui.requestRender();
|
|
357
366
|
});
|
|
358
367
|
});
|
|
359
|
-
|
|
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
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
462
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
18
|
+
"malevich-tui-components": "^0.8.0"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@earendil-works/pi-coding-agent": "*",
|