@danypops/pi-packed 0.10.0 → 0.11.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
@@ -10,13 +10,13 @@ The extension connects to Packed's authenticated user daemon and starts the pack
10
10
 
11
11
  ## Commands
12
12
 
13
- - `/packed` -- opens on the packages 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.
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.
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).
18
18
  - `f` -- find: search the npm registry for new Pi packages and install one.
19
- - `s` -- settings. `Enter` opens a floating action menu with all of the above. `r` refreshes, `/` filters, `Tab` cycles view modes.
19
+ - `s` -- settings. `Enter` opens a smaller floating action menu with all of the above. `r` refreshes, `/` filters, `Tab` cycles view modes.
20
20
  - `/packed config` -- enable or disable individual extensions, skills, prompt templates, and themes declared by installed packages, per package, at global or project scope.
21
21
  - `/packed setup plan [path] [--prune]` / `/packed setup apply [path] [--prune]` -- preview or apply a reproducible-environment manifest (`pi-setup.json`) against installed packages and profiles.
22
22
  - `/profile [name]` -- switch to a named Pi profile (provider, model, tools, instructions, theme), reading `~/.pi/agent/profiles.json` and a trusted project's own `.pi/profiles.json`. `pi --profile <name>` activates one at startup; `Ctrl+Shift+U` cycles through them.
@@ -6,9 +6,12 @@
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. Enter opens a floating (`ctx.ui.custom` with
10
- * overlay:true) action menu instead of a full-screen prompt, so the list
11
- * underneath never disappears. All data flows through the packed CLI
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
12
15
  * (thin seam).
13
16
  */
14
17
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
@@ -25,7 +28,7 @@ import { showDiscoverPanel } from "./discover.js";
25
28
  import { menuTheme } from "./menu-theme.js";
26
29
 
27
30
  interface PanelAction {
28
- type: "update" | "updateAll" | "remove" | "disable" | "config" | "find" | "refresh" | "settings";
31
+ type: "update" | "remove" | "disable" | "config" | "find" | "refresh" | "settings";
29
32
  row?: Row;
30
33
  }
31
34
 
@@ -88,11 +91,73 @@ export async function applyPackageChoice(
88
91
 
89
92
  interface UpdateAllResult { changed: number; failedNames: string[]; }
90
93
 
91
- /** Floats a live progress bar over the still-open panel while the batch
92
- * runs sequentially -- the lazy.nvim-style "Updating N package(s)" window,
93
- * instead of one static toast with no visible progress until it's all
94
- * done. Resolves on its own once the last package finishes; nothing the
95
- * user presses closes it early. */
94
+ interface UpdateProgressEvent { row: Row; index: number; total: number; phase: "start" | "done"; }
95
+
96
+ /** The core sequential-update loop, with no UI of its own -- reports each
97
+ * step via onProgress so any host surface (a floating overlay, or the
98
+ * packages panel's own body) can render it however fits. */
99
+ async function performUpdateAll(
100
+ outdated: Row[],
101
+ natives: Natives,
102
+ approved: boolean | undefined,
103
+ onProgress?: (event: UpdateProgressEvent) => void,
104
+ ): Promise<UpdateAllResult> {
105
+ let changed = 0;
106
+ const failedNames: string[] = [];
107
+ for (let i = 0; i < outdated.length; i++) {
108
+ const row = outdated[i]!;
109
+ onProgress?.({ row, index: i, total: outdated.length, phase: "start" });
110
+ try {
111
+ const outcome = await natives.update(`npm:${row.name}`, approved);
112
+ if (outcome.reloadRequired) changed += 1;
113
+ } catch (e) {
114
+ failedNames.push(`${row.name}: ${e instanceof Error ? e.message : e}`);
115
+ }
116
+ onProgress?.({ row, index: i, total: outdated.length, phase: "done" });
117
+ }
118
+ return { changed, failedNames };
119
+ }
120
+
121
+ /** Approves once for the whole batch, runs it via whatever runBatch does
122
+ * (a floating overlay for applyUpdateAll's own public API, or renderPanel's
123
+ * embedded progress bar), then reports the combined result -- shared so
124
+ * both surfaces stay behaviorally identical. */
125
+ async function approveAndRunUpdateAll(
126
+ outdated: Row[],
127
+ natives: Natives,
128
+ ctx: ExtensionCommandContext,
129
+ runBatch: (outdated: Row[], approved: boolean | undefined) => Promise<UpdateAllResult>,
130
+ ): Promise<PackageChoiceOutcome> {
131
+ if (outdated.length === 0) {
132
+ ctx.ui.notify("Nothing to update.", "info");
133
+ return "unchanged";
134
+ }
135
+ const approval = await approvePackageOperation(
136
+ "update",
137
+ `pi update --extension ${outdated.map((row) => `npm:${row.name}`).join(" ")}`,
138
+ natives,
139
+ ctx,
140
+ );
141
+ if (!approval.allowed) {
142
+ ctx.ui.notify(approval.message ?? "update denied", "warning");
143
+ return "cancelled";
144
+ }
145
+ const { changed, failedNames } = await runBatch(outdated, approval.approved);
146
+ for (const failure of failedNames) ctx.ui.notify(`update failed: ${failure}`, "error");
147
+ if (changed === 0) {
148
+ ctx.ui.notify(failedNames.length > 0 ? `No packages updated; ${failedNames.length} failed.` : "All packages already up to date.", failedNames.length > 0 ? "warning" : "info");
149
+ return failedNames.length > 0 ? "cancelled" : "unchanged";
150
+ }
151
+ ctx.ui.notify(`Updated ${changed} package(s)${failedNames.length > 0 ? `, ${failedNames.length} failed` : ""}; reloading Pi resources.`, "info");
152
+ await ctx.reload();
153
+ return "changed";
154
+ }
155
+
156
+ /** Floats its own progress-bar overlay over the still-open panel -- kept
157
+ * for applyUpdateAll's own public API (and anything calling it directly,
158
+ * outside the packages panel). renderPanel's own U key does not use this;
159
+ * it renders the same progress bar inline on its own already-open overlay
160
+ * instead of stacking a second one. */
96
161
  async function runUpdatesWithProgress(
97
162
  outdated: Row[],
98
163
  natives: Natives,
@@ -112,24 +177,11 @@ async function runUpdatesWithProgress(
112
177
  container.addChild(new Spacer(1));
113
178
  container.addChild(border());
114
179
 
115
- void (async () => {
116
- let changed = 0;
117
- const failedNames: string[] = [];
118
- for (let i = 0; i < outdated.length; i++) {
119
- const row = outdated[i]!;
120
- bar.setLabel(`${row.name} (${i + 1}/${outdated.length})`);
121
- tui.requestRender();
122
- try {
123
- const outcome = await natives.update(`npm:${row.name}`, approved);
124
- if (outcome.reloadRequired) changed += 1;
125
- } catch (e) {
126
- failedNames.push(`${row.name}: ${e instanceof Error ? e.message : e}`);
127
- }
128
- bar.setValue(i + 1);
129
- tui.requestRender();
130
- }
131
- done({ changed, failedNames });
132
- })();
180
+ performUpdateAll(outdated, natives, approved, (event) => {
181
+ bar.setLabel(`${event.row.name} (${event.index + 1}/${event.total})`);
182
+ if (event.phase === "done") bar.setValue(event.index + 1);
183
+ tui.requestRender();
184
+ }).then(done);
133
185
 
134
186
  return { render: (width: number) => container.render(width), invalidate: () => container.invalidate(), handleInput() {} };
135
187
  },
@@ -139,32 +191,12 @@ async function runUpdatesWithProgress(
139
191
 
140
192
  /** U -- update every outdated row with one combined approval and one
141
193
  * reload, instead of applyPackageChoice's own per-call reload (which
142
- * would end the session after the first successful update). */
194
+ * would end the session after the first successful update). Public API:
195
+ * opens its own progress overlay. renderPanel's own U key instead renders
196
+ * progress inline via approveAndRunUpdateAll + performUpdateAll directly. */
143
197
  export async function applyUpdateAll(rows: Row[], natives: Natives, ctx: ExtensionCommandContext): Promise<PackageChoiceOutcome> {
144
198
  const outdated = rows.filter((row) => row.hasUpdate);
145
- if (outdated.length === 0) {
146
- ctx.ui.notify("Nothing to update.", "info");
147
- return "unchanged";
148
- }
149
- const approval = await approvePackageOperation(
150
- "update",
151
- `pi update --extension ${outdated.map((row) => `npm:${row.name}`).join(" ")}`,
152
- natives,
153
- ctx,
154
- );
155
- if (!approval.allowed) {
156
- ctx.ui.notify(approval.message ?? "update denied", "warning");
157
- return "cancelled";
158
- }
159
- const { changed, failedNames } = await runUpdatesWithProgress(outdated, natives, approval.approved, ctx);
160
- for (const failure of failedNames) ctx.ui.notify(`update failed: ${failure}`, "error");
161
- if (changed === 0) {
162
- ctx.ui.notify(failedNames.length > 0 ? `No packages updated; ${failedNames.length} failed.` : "All packages already up to date.", failedNames.length > 0 ? "warning" : "info");
163
- return failedNames.length > 0 ? "cancelled" : "unchanged";
164
- }
165
- ctx.ui.notify(`Updated ${changed} package(s)${failedNames.length > 0 ? `, ${failedNames.length} failed` : ""}; reloading Pi resources.`, "info");
166
- await ctx.reload();
167
- return "changed";
199
+ return approveAndRunUpdateAll(outdated, natives, ctx, (batch, approved) => runUpdatesWithProgress(batch, natives, approved, ctx));
168
200
  }
169
201
 
170
202
  /** d -- toggles every declared extension of this package on or off in one
@@ -248,8 +280,10 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
248
280
  }
249
281
 
250
282
  // Panel loop: actions resolve the component, run outside it, then reopen.
283
+ // U/updateAll is handled entirely inside renderPanel itself (progress
284
+ // rendered on the same already-open overlay) and never reaches here.
251
285
  for (;;) {
252
- const action = await renderPanel(ctx, rows);
286
+ const action = await renderPanel(ctx, natives, rows);
253
287
  if (!action) return; // closed
254
288
 
255
289
  if (action.type === "refresh") {
@@ -268,12 +302,6 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
268
302
  continue; // a successful install already reloaded; a no-op returns here
269
303
  }
270
304
 
271
- if (action.type === "updateAll") {
272
- const outcome = await applyUpdateAll(rows, natives, ctx);
273
- if (outcome === "changed") return; // ctx.reload() already replaced the session
274
- continue;
275
- }
276
-
277
305
  if (action.type === "config") {
278
306
  await showResourceConfig(ctx, natives, action.row?.name);
279
307
  continue; // showResourceConfig already handles its own reload prompt
@@ -293,13 +321,17 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
293
321
  }
294
322
  }
295
323
 
296
- function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAction | undefined> {
324
+ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows: Row[]): Promise<PanelAction | undefined> {
297
325
  return ctx.ui.custom<PanelAction | undefined>((tui, theme, _kb, done) => {
326
+ let rows = initialRows;
298
327
  let mode: ViewMode = "all";
299
328
  const searchInput = new Input();
300
329
  let searchActive = false;
301
330
  let filtered = visibleRows(rows, mode);
302
331
  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;
303
335
 
304
336
  const maxVisible = 20;
305
337
 
@@ -308,6 +340,34 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
308
340
  selectedIndex = 0;
309
341
  }
310
342
 
343
+ /** 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
+ async function runUpdateAllInline(): Promise<void> {
348
+ const outdated = rows.filter((row) => row.hasUpdate);
349
+ 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();
353
+ 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);
356
+ tui.requestRender();
357
+ });
358
+ });
359
+ installing = undefined;
360
+ if (outcome === "changed") {
361
+ done(undefined); // ctx.reload() already replaced the session
362
+ return;
363
+ }
364
+ const reloaded = await loadRows(natives);
365
+ if (reloaded.error) ctx.ui.notify(`refresh failed: ${reloaded.error}`, "error");
366
+ else rows = reloaded.rows;
367
+ applyFilter();
368
+ tui.requestRender();
369
+ }
370
+
311
371
  const header = {
312
372
  invalidate() {},
313
373
  render(width: number): string[] {
@@ -371,20 +431,38 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
371
431
  };
372
432
 
373
433
  const border = () => new DynamicBorder((s) => theme.fg("border", s));
374
- const container = new Container();
375
- container.addChild(new Spacer(1));
376
- container.addChild(border());
377
- container.addChild(new Spacer(1));
378
- container.addChild(header);
379
- container.addChild(new Spacer(1));
380
- container.addChild(list);
381
- container.addChild(new Spacer(1));
382
- container.addChild(border());
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
+ }
383
459
 
384
460
  return {
385
- render: (width: number) => container.render(width),
386
- invalidate: () => container.invalidate(),
461
+ render: (width: number) => (installing ? buildInstallingContainer(installing) : buildListContainer()).render(width),
462
+ invalidate: () => (installing ? buildInstallingContainer(installing) : buildListContainer()).invalidate(),
387
463
  handleInput(data: string) {
464
+ if (installing) return; // the installer owns the panel until it finishes
465
+
388
466
  if (searchActive) {
389
467
  if (data === "\x1b") {
390
468
  searchActive = false;
@@ -424,7 +502,7 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
424
502
  done({ type: "find" });
425
503
  return;
426
504
  case "U":
427
- done({ type: "updateAll" });
505
+ void runUpdateAllInline();
428
506
  return;
429
507
  case "u": {
430
508
  const row = filtered[selectedIndex];
@@ -467,5 +545,5 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
467
545
  tui.requestRender();
468
546
  },
469
547
  };
470
- });
548
+ }, { overlay: true, overlayOptions: { width: "70%", maxHeight: "70%", anchor: "center" } });
471
549
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Pi package tools, commands, profiles, and TUI for the Packed daemon",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],