@danypops/pi-packed 0.9.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.
@@ -149,15 +149,16 @@ function renderDiscoverPanel(ctx: ExtensionCommandContext, natives: Natives): Pr
149
149
  },
150
150
  };
151
151
 
152
+ const border = () => new DynamicBorder((s) => theme.fg("border", s));
152
153
  const container = new Container();
153
154
  container.addChild(new Spacer(1));
154
- container.addChild(new DynamicBorder());
155
+ container.addChild(border());
155
156
  container.addChild(new Spacer(1));
156
157
  container.addChild(header);
157
158
  container.addChild(new Spacer(1));
158
159
  container.addChild(list);
159
160
  container.addChild(new Spacer(1));
160
- container.addChild(new DynamicBorder());
161
+ container.addChild(border());
161
162
 
162
163
  return {
163
164
  render: (width: number) => container.render(width),
@@ -190,15 +190,16 @@ function renderConfig(ctx: ExtensionCommandContext, data: { global: PackageResou
190
190
  },
191
191
  };
192
192
 
193
+ const border = () => new DynamicBorder((s) => theme.fg("border", s));
193
194
  const container = new Container();
194
195
  container.addChild(new Spacer(1));
195
- container.addChild(new DynamicBorder());
196
+ container.addChild(border());
196
197
  container.addChild(new Spacer(1));
197
198
  container.addChild(header);
198
199
  container.addChild(new Spacer(1));
199
200
  container.addChild(list);
200
201
  container.addChild(new Spacer(1));
201
- container.addChild(new DynamicBorder());
202
+ container.addChild(border());
202
203
 
203
204
  return {
204
205
  render: (width: number) => container.render(width),
@@ -6,15 +6,18 @@
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";
15
18
  import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
16
19
  import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
17
- import { Menu, type MenuItem } from "malevich-tui-components";
20
+ import { Menu, ProgressBar, type MenuItem } from "malevich-tui-components";
18
21
  import { filterRows, mergeRows, nextMode, visibleRows } from "./model.js";
19
22
  import type { Row, ViewMode } from "./model.js";
20
23
  import type { Natives, PackageResources } from "./packed.js";
@@ -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
 
@@ -86,11 +89,45 @@ export async function applyPackageChoice(
86
89
  return "cancelled";
87
90
  }
88
91
 
89
- /** U -- update every outdated row with one combined approval and one
90
- * reload, instead of applyPackageChoice's own per-call reload (which
91
- * would end the session after the first successful update). */
92
- export async function applyUpdateAll(rows: Row[], natives: Natives, ctx: ExtensionCommandContext): Promise<PackageChoiceOutcome> {
93
- const outdated = rows.filter((row) => row.hasUpdate);
92
+ interface UpdateAllResult { changed: number; failedNames: string[]; }
93
+
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> {
94
131
  if (outdated.length === 0) {
95
132
  ctx.ui.notify("Nothing to update.", "info");
96
133
  return "unchanged";
@@ -105,27 +142,63 @@ export async function applyUpdateAll(rows: Row[], natives: Natives, ctx: Extensi
105
142
  ctx.ui.notify(approval.message ?? "update denied", "warning");
106
143
  return "cancelled";
107
144
  }
108
- ctx.ui.notify(`Updating ${outdated.length} package(s)…`, "info");
109
- let changed = 0;
110
- let failed = 0;
111
- for (const row of outdated) {
112
- try {
113
- const outcome = await natives.update(`npm:${row.name}`, approval.approved);
114
- if (outcome.reloadRequired) changed += 1;
115
- } catch (e) {
116
- failed += 1;
117
- ctx.ui.notify(`${row.name} update failed: ${e instanceof Error ? e.message : e}`, "error");
118
- }
119
- }
145
+ const { changed, failedNames } = await runBatch(outdated, approval.approved);
146
+ for (const failure of failedNames) ctx.ui.notify(`update failed: ${failure}`, "error");
120
147
  if (changed === 0) {
121
- ctx.ui.notify(failed > 0 ? `No packages updated; ${failed} failed.` : "All packages already up to date.", failed > 0 ? "warning" : "info");
122
- return failed > 0 ? "cancelled" : "unchanged";
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";
123
150
  }
124
- ctx.ui.notify(`Updated ${changed} package(s)${failed > 0 ? `, ${failed} failed` : ""}; reloading Pi resources.`, "info");
151
+ ctx.ui.notify(`Updated ${changed} package(s)${failedNames.length > 0 ? `, ${failedNames.length} failed` : ""}; reloading Pi resources.`, "info");
125
152
  await ctx.reload();
126
153
  return "changed";
127
154
  }
128
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. */
161
+ async function runUpdatesWithProgress(
162
+ outdated: Row[],
163
+ natives: Natives,
164
+ approved: boolean | undefined,
165
+ ctx: ExtensionCommandContext,
166
+ ): Promise<UpdateAllResult> {
167
+ return ctx.ui.custom<UpdateAllResult>(
168
+ (tui, theme, _kb, done) => {
169
+ const bar = new ProgressBar({ value: 0, max: outdated.length, label: `${outdated[0]?.name ?? ""} (1/${outdated.length})`, style: (s) => theme.fg("accent", s) });
170
+ const border = () => new DynamicBorder((s) => theme.fg("border", s));
171
+ const container = new Container();
172
+ container.addChild(new Spacer(1));
173
+ container.addChild(border());
174
+ container.addChild({ invalidate() {}, render: (_width: number) => [theme.bold("Updating packages")] });
175
+ container.addChild(new Spacer(1));
176
+ container.addChild(bar);
177
+ container.addChild(new Spacer(1));
178
+ container.addChild(border());
179
+
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);
185
+
186
+ return { render: (width: number) => container.render(width), invalidate: () => container.invalidate(), handleInput() {} };
187
+ },
188
+ { overlay: true, overlayOptions: { width: 50, anchor: "center" } },
189
+ );
190
+ }
191
+
192
+ /** U -- update every outdated row with one combined approval and one
193
+ * reload, instead of applyPackageChoice's own per-call reload (which
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. */
197
+ export async function applyUpdateAll(rows: Row[], natives: Natives, ctx: ExtensionCommandContext): Promise<PackageChoiceOutcome> {
198
+ const outdated = rows.filter((row) => row.hasUpdate);
199
+ return approveAndRunUpdateAll(outdated, natives, ctx, (batch, approved) => runUpdatesWithProgress(batch, natives, approved, ctx));
200
+ }
201
+
129
202
  /** d -- toggles every declared extension of this package on or off in one
130
203
  * step (disables if any are enabled, otherwise re-enables all). Reuses
131
204
  * applyResourceToggle per item for its own tested approval/mutation path
@@ -207,8 +280,10 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
207
280
  }
208
281
 
209
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.
210
285
  for (;;) {
211
- const action = await renderPanel(ctx, rows);
286
+ const action = await renderPanel(ctx, natives, rows);
212
287
  if (!action) return; // closed
213
288
 
214
289
  if (action.type === "refresh") {
@@ -227,12 +302,6 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
227
302
  continue; // a successful install already reloaded; a no-op returns here
228
303
  }
229
304
 
230
- if (action.type === "updateAll") {
231
- const outcome = await applyUpdateAll(rows, natives, ctx);
232
- if (outcome === "changed") return; // ctx.reload() already replaced the session
233
- continue;
234
- }
235
-
236
305
  if (action.type === "config") {
237
306
  await showResourceConfig(ctx, natives, action.row?.name);
238
307
  continue; // showResourceConfig already handles its own reload prompt
@@ -252,13 +321,17 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
252
321
  }
253
322
  }
254
323
 
255
- function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAction | undefined> {
324
+ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows: Row[]): Promise<PanelAction | undefined> {
256
325
  return ctx.ui.custom<PanelAction | undefined>((tui, theme, _kb, done) => {
326
+ let rows = initialRows;
257
327
  let mode: ViewMode = "all";
258
328
  const searchInput = new Input();
259
329
  let searchActive = false;
260
330
  let filtered = visibleRows(rows, mode);
261
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;
262
335
 
263
336
  const maxVisible = 20;
264
337
 
@@ -267,6 +340,34 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
267
340
  selectedIndex = 0;
268
341
  }
269
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
+
270
371
  const header = {
271
372
  invalidate() {},
272
373
  render(width: number): string[] {
@@ -329,20 +430,39 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
329
430
  },
330
431
  };
331
432
 
332
- const container = new Container();
333
- container.addChild(new Spacer(1));
334
- container.addChild(new DynamicBorder());
335
- container.addChild(new Spacer(1));
336
- container.addChild(header);
337
- container.addChild(new Spacer(1));
338
- container.addChild(list);
339
- container.addChild(new Spacer(1));
340
- container.addChild(new DynamicBorder());
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
+ }
341
459
 
342
460
  return {
343
- render: (width: number) => container.render(width),
344
- invalidate: () => container.invalidate(),
461
+ render: (width: number) => (installing ? buildInstallingContainer(installing) : buildListContainer()).render(width),
462
+ invalidate: () => (installing ? buildInstallingContainer(installing) : buildListContainer()).invalidate(),
345
463
  handleInput(data: string) {
464
+ if (installing) return; // the installer owns the panel until it finishes
465
+
346
466
  if (searchActive) {
347
467
  if (data === "\x1b") {
348
468
  searchActive = false;
@@ -382,7 +502,7 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
382
502
  done({ type: "find" });
383
503
  return;
384
504
  case "U":
385
- done({ type: "updateAll" });
505
+ void runUpdateAllInline();
386
506
  return;
387
507
  case "u": {
388
508
  const row = filtered[selectedIndex];
@@ -425,5 +545,5 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
425
545
  tui.requestRender();
426
546
  },
427
547
  };
428
- });
548
+ }, { overlay: true, overlayOptions: { width: "70%", maxHeight: "70%", anchor: "center" } });
429
549
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.9.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"],