@danypops/pi-packed 0.10.0 → 0.12.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 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).
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,15 +6,19 @@
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
12
- * (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. All data flows
16
+ * through the packed CLI (thin seam).
13
17
  */
14
18
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
15
19
  import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
16
- import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
17
- import { Menu, ProgressBar, type MenuItem } from "malevich-tui-components";
20
+ import { Container, Input, Spacer, truncateToWidth } from "@earendil-works/pi-tui";
21
+ import { Envelope, Menu, ProgressBar, type MenuItem } from "malevich-tui-components";
18
22
  import { filterRows, mergeRows, nextMode, visibleRows } from "./model.js";
19
23
  import type { Row, ViewMode } from "./model.js";
20
24
  import type { Natives, PackageResources } from "./packed.js";
@@ -25,7 +29,7 @@ import { showDiscoverPanel } from "./discover.js";
25
29
  import { menuTheme } from "./menu-theme.js";
26
30
 
27
31
  interface PanelAction {
28
- type: "update" | "updateAll" | "remove" | "disable" | "config" | "find" | "refresh" | "settings";
32
+ type: "update" | "remove" | "disable" | "config" | "find" | "refresh" | "settings";
29
33
  row?: Row;
30
34
  }
31
35
 
@@ -88,11 +92,73 @@ export async function applyPackageChoice(
88
92
 
89
93
  interface UpdateAllResult { changed: number; failedNames: string[]; }
90
94
 
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. */
95
+ interface UpdateProgressEvent { row: Row; index: number; total: number; phase: "start" | "done"; }
96
+
97
+ /** The core sequential-update loop, with no UI of its own -- reports each
98
+ * step via onProgress so any host surface (a floating overlay, or the
99
+ * packages panel's own body) can render it however fits. */
100
+ async function performUpdateAll(
101
+ outdated: Row[],
102
+ natives: Natives,
103
+ approved: boolean | undefined,
104
+ onProgress?: (event: UpdateProgressEvent) => void,
105
+ ): Promise<UpdateAllResult> {
106
+ let changed = 0;
107
+ const failedNames: string[] = [];
108
+ for (let i = 0; i < outdated.length; i++) {
109
+ const row = outdated[i]!;
110
+ onProgress?.({ row, index: i, total: outdated.length, phase: "start" });
111
+ try {
112
+ const outcome = await natives.update(`npm:${row.name}`, approved);
113
+ if (outcome.reloadRequired) changed += 1;
114
+ } catch (e) {
115
+ failedNames.push(`${row.name}: ${e instanceof Error ? e.message : e}`);
116
+ }
117
+ onProgress?.({ row, index: i, total: outdated.length, phase: "done" });
118
+ }
119
+ return { changed, failedNames };
120
+ }
121
+
122
+ /** Approves once for the whole batch, runs it via whatever runBatch does
123
+ * (a floating overlay for applyUpdateAll's own public API, or renderPanel's
124
+ * embedded progress bar), then reports the combined result -- shared so
125
+ * both surfaces stay behaviorally identical. */
126
+ async function approveAndRunUpdateAll(
127
+ outdated: Row[],
128
+ natives: Natives,
129
+ ctx: ExtensionCommandContext,
130
+ runBatch: (outdated: Row[], approved: boolean | undefined) => Promise<UpdateAllResult>,
131
+ ): Promise<PackageChoiceOutcome> {
132
+ if (outdated.length === 0) {
133
+ ctx.ui.notify("Nothing to update.", "info");
134
+ return "unchanged";
135
+ }
136
+ const approval = await approvePackageOperation(
137
+ "update",
138
+ `pi update --extension ${outdated.map((row) => `npm:${row.name}`).join(" ")}`,
139
+ natives,
140
+ ctx,
141
+ );
142
+ if (!approval.allowed) {
143
+ ctx.ui.notify(approval.message ?? "update denied", "warning");
144
+ return "cancelled";
145
+ }
146
+ const { changed, failedNames } = await runBatch(outdated, approval.approved);
147
+ for (const failure of failedNames) ctx.ui.notify(`update failed: ${failure}`, "error");
148
+ if (changed === 0) {
149
+ ctx.ui.notify(failedNames.length > 0 ? `No packages updated; ${failedNames.length} failed.` : "All packages already up to date.", failedNames.length > 0 ? "warning" : "info");
150
+ return failedNames.length > 0 ? "cancelled" : "unchanged";
151
+ }
152
+ ctx.ui.notify(`Updated ${changed} package(s)${failedNames.length > 0 ? `, ${failedNames.length} failed` : ""}; reloading Pi resources.`, "info");
153
+ await ctx.reload();
154
+ return "changed";
155
+ }
156
+
157
+ /** Floats its own progress-bar overlay over the still-open panel -- kept
158
+ * for applyUpdateAll's own public API (and anything calling it directly,
159
+ * outside the packages panel). renderPanel's own U key does not use this;
160
+ * it renders the same progress bar inline on its own already-open overlay
161
+ * instead of stacking a second one. */
96
162
  async function runUpdatesWithProgress(
97
163
  outdated: Row[],
98
164
  natives: Natives,
@@ -112,24 +178,11 @@ async function runUpdatesWithProgress(
112
178
  container.addChild(new Spacer(1));
113
179
  container.addChild(border());
114
180
 
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
- })();
181
+ performUpdateAll(outdated, natives, approved, (event) => {
182
+ bar.setLabel(`${event.row.name} (${event.index + 1}/${event.total})`);
183
+ if (event.phase === "done") bar.setValue(event.index + 1);
184
+ tui.requestRender();
185
+ }).then(done);
133
186
 
134
187
  return { render: (width: number) => container.render(width), invalidate: () => container.invalidate(), handleInput() {} };
135
188
  },
@@ -139,32 +192,12 @@ async function runUpdatesWithProgress(
139
192
 
140
193
  /** U -- update every outdated row with one combined approval and one
141
194
  * reload, instead of applyPackageChoice's own per-call reload (which
142
- * would end the session after the first successful update). */
195
+ * would end the session after the first successful update). Public API:
196
+ * opens its own progress overlay. renderPanel's own U key instead renders
197
+ * progress inline via approveAndRunUpdateAll + performUpdateAll directly. */
143
198
  export async function applyUpdateAll(rows: Row[], natives: Natives, ctx: ExtensionCommandContext): Promise<PackageChoiceOutcome> {
144
199
  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";
200
+ return approveAndRunUpdateAll(outdated, natives, ctx, (batch, approved) => runUpdatesWithProgress(batch, natives, approved, ctx));
168
201
  }
169
202
 
170
203
  /** d -- toggles every declared extension of this package on or off in one
@@ -248,8 +281,10 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
248
281
  }
249
282
 
250
283
  // Panel loop: actions resolve the component, run outside it, then reopen.
284
+ // U/updateAll is handled entirely inside renderPanel itself (progress
285
+ // rendered on the same already-open overlay) and never reaches here.
251
286
  for (;;) {
252
- const action = await renderPanel(ctx, rows);
287
+ const action = await renderPanel(ctx, natives, rows);
253
288
  if (!action) return; // closed
254
289
 
255
290
  if (action.type === "refresh") {
@@ -268,12 +303,6 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
268
303
  continue; // a successful install already reloaded; a no-op returns here
269
304
  }
270
305
 
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
306
  if (action.type === "config") {
278
307
  await showResourceConfig(ctx, natives, action.row?.name);
279
308
  continue; // showResourceConfig already handles its own reload prompt
@@ -293,13 +322,19 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
293
322
  }
294
323
  }
295
324
 
296
- function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAction | undefined> {
325
+ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows: Row[]): Promise<PanelAction | undefined> {
297
326
  return ctx.ui.custom<PanelAction | undefined>((tui, theme, _kb, done) => {
327
+ let rows = initialRows;
298
328
  let mode: ViewMode = "all";
299
329
  const searchInput = new Input();
300
330
  let searchActive = false;
301
331
  let filtered = visibleRows(rows, mode);
302
332
  let selectedIndex = 0;
333
+ // Set only while U's batch update is running. The list stays fully
334
+ // visible throughout -- this just names which row the shared bar
335
+ // (below) is currently sitting next to.
336
+ let updatingRowName: string | undefined;
337
+ const updatingBar = new ProgressBar({ value: 0, max: 1, width: 10, style: (s) => theme.fg("accent", s) });
303
338
 
304
339
  const maxVisible = 20;
305
340
 
@@ -308,12 +343,44 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
308
343
  selectedIndex = 0;
309
344
  }
310
345
 
346
+ /** U -- runs the whole approve+update+notify+reload flow without ever
347
+ * closing this panel or replacing the list: each step's progress
348
+ * appears inline next to the row currently being updated, via
349
+ * updatingRowName/updatingBar, which list's own render checks. Rows
350
+ * refresh in place afterward unless a reload already ended the
351
+ * session. */
352
+ async function runUpdateAllInline(): Promise<void> {
353
+ const outdated = rows.filter((row) => row.hasUpdate);
354
+ const outcome = await approveAndRunUpdateAll(outdated, natives, ctx, (batch, approved) => {
355
+ updatingBar.setValue(0);
356
+ updatingBar.setMax(batch.length);
357
+ return performUpdateAll(batch, natives, approved, (event) => {
358
+ updatingRowName = event.row.name;
359
+ if (event.phase === "done") updatingBar.setValue(event.index + 1);
360
+ tui.requestRender();
361
+ });
362
+ });
363
+ updatingRowName = undefined;
364
+ if (outcome === "changed") {
365
+ done(undefined); // ctx.reload() already replaced the session
366
+ return;
367
+ }
368
+ const reloaded = await loadRows(natives);
369
+ if (reloaded.error) ctx.ui.notify(`refresh failed: ${reloaded.error}`, "error");
370
+ else rows = reloaded.rows;
371
+ applyFilter();
372
+ tui.requestRender();
373
+ }
374
+
375
+ function panelTitle(): string {
376
+ if (updatingRowName) return "Packages · updating…";
377
+ const outdated = rows.filter((r) => r.hasUpdate).length;
378
+ return outdated > 0 ? `Packages · ${outdated} update(s)` : "Packages";
379
+ }
380
+
311
381
  const header = {
312
382
  invalidate() {},
313
383
  render(width: number): string[] {
314
- const title = theme.bold("Packages");
315
- const outdated = rows.filter((r) => r.hasUpdate).length;
316
- const badge = outdated > 0 ? theme.fg("warning", ` ${outdated} update(s)`) : "";
317
384
  const hint = searchActive
318
385
  ? rawKeyHint("esc", "clear")
319
386
  : rawKeyHint("enter", "menu") +
@@ -331,8 +398,7 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
331
398
  rawKeyHint("s", "settings") +
332
399
  theme.fg("muted", " · ") +
333
400
  rawKeyHint("esc", "close");
334
- const spacing = Math.max(1, width - visibleWidth(title) - visibleWidth(badge) - visibleWidth(hint));
335
- const line1 = truncateToWidth(`${title}${badge}${" ".repeat(spacing)}${hint}`, width, "");
401
+ const line1 = truncateToWidth(hint, width, "");
336
402
  const dot = "·";
337
403
  const line2 = truncateToWidth(
338
404
  theme.fg("muted", `view: ${mode} ${dot} / filter ${dot} tab view ${dot} r refresh ${dot} ${rows.length} installed`),
@@ -361,7 +427,10 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
361
427
  const cursor = selected ? theme.fg("accent", "❯") : " ";
362
428
  const name = selected ? theme.bold(row.name) : row.name;
363
429
  const ver = theme.fg("dim", `@${row.version}`);
364
- const upd = row.hasUpdate ? theme.fg("warning", ` ↑${row.latest}`) : "";
430
+ const isUpdating = updatingRowName === row.name;
431
+ const upd = isUpdating
432
+ ? theme.fg("accent", ` ${updatingBar.format(10)} updating…`)
433
+ : row.hasUpdate ? theme.fg("warning", ` ↑${row.latest}`) : "";
365
434
  lines.push(truncateToWidth(`${cursor} ${name}${ver}${upd}`, width, ""));
366
435
  }
367
436
  const hasScroll = start > 0 || end < filtered.length;
@@ -370,21 +439,31 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
370
439
  },
371
440
  };
372
441
 
373
- 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());
442
+ // Real bordered box (rounded corners), not just a horizontal rule --
443
+ // title carries the live update badge/status instead of a header line.
444
+ const envelope = new Envelope({
445
+ title: panelTitle(),
446
+ borderStyle: "rounded",
447
+ style: (s) => theme.fg("border", s),
448
+ titleStyle: (s) => theme.bold(theme.fg("accent", s)),
449
+ });
450
+ const body = {
451
+ invalidate() { header.invalidate(); list.invalidate(); },
452
+ render(width: number): string[] {
453
+ return [...header.render(width), "", ...list.render(width)];
454
+ },
455
+ };
456
+ envelope.setContent(body);
383
457
 
384
458
  return {
385
- render: (width: number) => container.render(width),
386
- invalidate: () => container.invalidate(),
459
+ render(width: number): string[] {
460
+ envelope.setTitle(panelTitle());
461
+ return envelope.render(width);
462
+ },
463
+ invalidate: () => envelope.invalidate(),
387
464
  handleInput(data: string) {
465
+ if (updatingRowName) return; // the installer owns input until it finishes
466
+
388
467
  if (searchActive) {
389
468
  if (data === "\x1b") {
390
469
  searchActive = false;
@@ -424,7 +503,7 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
424
503
  done({ type: "find" });
425
504
  return;
426
505
  case "U":
427
- done({ type: "updateAll" });
506
+ void runUpdateAllInline();
428
507
  return;
429
508
  case "u": {
430
509
  const row = filtered[selectedIndex];
@@ -467,5 +546,5 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
467
546
  tui.requestRender();
468
547
  },
469
548
  };
470
- });
549
+ }, { overlay: true, overlayOptions: { width: "70%", maxHeight: "70%", anchor: "top-center", offsetY: 1 } });
471
550
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.10.0",
3
+ "version": "0.12.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": "*",