@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 +1 -1
- package/extension/src/menu-theme.ts +14 -1
- package/extension/src/tui.ts +95 -47
- package/package.json +2 -2
- package/extension/src/spinner.ts +0 -44
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
|
+
}
|
package/extension/src/tui.ts
CHANGED
|
@@ -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
|
-
* (
|
|
18
|
-
* embeddable one) renders inline next to the row
|
|
19
|
-
* settling into a real ✓/✗ plus a bounded tail of
|
|
20
|
-
* captured stdout/stderr once it finishes, never a
|
|
21
|
-
* single subprocess call has no knowable percentage).
|
|
22
|
-
* Malevich's Table (real column-aligned
|
|
23
|
-
* per-row selection styling baked into each
|
|
24
|
-
* cellStyle is column-wide, not row-wide) inside
|
|
25
|
-
* scroll-window slice -- Table deliberately owns no
|
|
26
|
-
* own, so the visible-window-around-selectedIndex math
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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: "
|
|
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
|
-
//
|
|
370
|
-
//
|
|
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
|
-
|
|
391
|
-
|
|
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,
|
|
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 (
|
|
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)
|
|
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)
|
|
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)
|
|
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")
|
|
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.
|
|
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.
|
|
18
|
+
"malevich-tui-components": "^0.11.0"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@earendil-works/pi-coding-agent": "*",
|
package/extension/src/spinner.ts
DELETED
|
@@ -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
|
-
}
|