@danypops/pi-packed 0.13.1 → 0.14.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
@@ -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 appears inline next to the package currently updating -- the list stays visible throughout, nothing swaps to a separate screen.
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. 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`.
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).
@@ -14,3 +14,28 @@ export function reloadWarning(operation: PackageOperation): string {
14
14
  ? "This will require a Pi reload (/reload) to deactivate it."
15
15
  : "This will likely require a Pi reload (/reload) to activate its resources.";
16
16
  }
17
+
18
+ export interface ReloadConfirmContext {
19
+ hasUI: boolean;
20
+ ui: { confirm(title: string, message: string): Promise<boolean> };
21
+ }
22
+
23
+ /**
24
+ * The second, separate decision point: reloadWarning above is a still-just-
25
+ * likely warning shown before a mutation runs; this is asked only once the
26
+ * mutation has actually succeeded and a reload is now definitely needed,
27
+ * not merely predicted. Declining defers it -- the mutation itself already
28
+ * happened (the package really did install/update/toggle); only Pi's own
29
+ * currently-loaded resources are stale until /reload runs later. No-UI
30
+ * contexts reload immediately: there's no one to ask, and staying silently
31
+ * stale forever is worse than reloading.
32
+ *
33
+ * Extracted from resource-config.ts's own pre-existing pendingReload gate
34
+ * (the first surface to implement this pattern) so every mutation surface
35
+ * shares identical wording and behavior instead of drifting apart --
36
+ * reload.ts's whole reason for existing.
37
+ */
38
+ export async function confirmReload(ctx: ReloadConfirmContext): Promise<boolean> {
39
+ if (!ctx.hasUI) return true;
40
+ return ctx.ui.confirm("Reload Pi now?", "This change only takes effect after a reload.");
41
+ }
@@ -14,6 +14,7 @@ import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earend
14
14
  import type { PackageResources, ResourceField } from "./packed.js";
15
15
  import type { Natives } from "./packed.js";
16
16
  import { packagePermissionDecision } from "./permission.js";
17
+ import { confirmReload } from "./reload.js";
17
18
 
18
19
  type Scope = "global" | "project";
19
20
  const RESOURCE_FIELDS = ["extensions", "skills", "prompts", "themes"] as const satisfies readonly ResourceField[];
@@ -132,8 +133,7 @@ export async function showResourceConfig(ctx: ExtensionCommandContext, natives:
132
133
  }
133
134
 
134
135
  if (!pendingReload) return;
135
- const confirmed = await ctx.ui.confirm("Reload Pi now?", "Extension changes only take effect after a reload.");
136
- if (confirmed) await ctx.reload();
136
+ if (await confirmReload(ctx)) await ctx.reload();
137
137
  else ctx.ui.notify("Extension changes pending -- run /reload when ready.", "warning");
138
138
  }
139
139
 
@@ -8,16 +8,22 @@
8
8
  * extensions, c jump to full resource config, f find/install new
9
9
  * packages, s settings. The panel itself is a real bordered (rounded)
10
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
11
+ * Envelope for the box), positioned at 40% of terminal height (pi-tui's
12
+ * own row percentage, not a fixed row count -- scales with the real
13
+ * terminal, unlike the anchor:"top-center"+offsetY this replaced, which
14
+ * was pinned 1 row below the absolute top on any terminal size). Enter
15
+ * opens a second, smaller overlay action menu on top of it. U's batch update stays on
14
16
  * this same package list -- progress renders inline next to the row
15
17
  * currently being updated, not as a separate screen. Rows render through
16
18
  * Malevich's Table (real column-aligned Package/Version/status cells,
17
19
  * per-row selection styling baked into each cell since Table's own
18
20
  * cellStyle is column-wide, not row-wide) inside this panel's own
19
21
  * scroll-window slice -- Table deliberately owns no pagination of its
20
- * own, so the visible-window-around-selectedIndex math stays here. All
22
+ * own, so the visible-window-around-selectedIndex math stays here. Every
23
+ * mutation that actually changes something asks confirmReload separately
24
+ * from the earlier mutation-approval confirm -- declining defers the
25
+ * reload (the mutation itself already happened) and keeps the panel open
26
+ * with refreshed rows instead of ending the session. All
21
27
  * data flows through the packed CLI (thin seam).
22
28
  */
23
29
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
@@ -32,6 +38,7 @@ import { showPackedSettings } from "./security-tui.js";
32
38
  import { showResourceConfig, applyResourceToggle } from "./resource-config.js";
33
39
  import { showDiscoverPanel } from "./discover.js";
34
40
  import { menuTheme } from "./menu-theme.js";
41
+ import { confirmReload } from "./reload.js";
35
42
 
36
43
  interface PanelAction {
37
44
  type: "update" | "remove" | "disable" | "config" | "find" | "refresh" | "settings";
@@ -41,8 +48,11 @@ interface PanelAction {
41
48
  /** Outcome of a confirmed row action, resolved after any real mutation and
42
49
  * reload decision -- "changed" means the daemon state changed and Pi has
43
50
  * already been reloaded (the caller should stop showing the stale panel);
51
+ * "deferred" means the mutation itself genuinely happened but the user
52
+ * declined confirmReload's separate reload gate -- Pi's session is still
53
+ * alive and the panel should refresh its rows and keep running, not close;
44
54
  * "unchanged"/"cancelled" mean the panel keeps running as-is. */
45
- export type PackageChoiceOutcome = "changed" | "unchanged" | "cancelled";
55
+ export type PackageChoiceOutcome = "changed" | "unchanged" | "cancelled" | "deferred";
46
56
 
47
57
  export async function applyPackageChoice(
48
58
  choice: string | undefined,
@@ -68,6 +78,10 @@ export async function applyPackageChoice(
68
78
  return "unchanged";
69
79
  }
70
80
  const transition = outcome.previousVersion && outcome.currentVersion ? ` (${outcome.previousVersion} → ${outcome.currentVersion})` : "";
81
+ if (!(await confirmReload(ctx))) {
82
+ ctx.ui.notify(`Updated ${row.name}${transition}; reload pending -- run /reload when ready.`, "warning");
83
+ return "deferred";
84
+ }
71
85
  ctx.ui.notify(`Updated ${row.name}${transition}; reloading Pi resources.`, "info");
72
86
  await ctx.reload();
73
87
  return "changed";
@@ -84,6 +98,10 @@ export async function applyPackageChoice(
84
98
  return "cancelled";
85
99
  }
86
100
  await natives.remove(row.name, approval.approved);
101
+ if (!(await confirmReload(ctx))) {
102
+ ctx.ui.notify(`Removed ${row.name}; reload pending -- run /reload when ready.`, "warning");
103
+ return "deferred";
104
+ }
87
105
  ctx.ui.notify(`Removed ${row.name}; reloading Pi resources.`, "info");
88
106
  await ctx.reload();
89
107
  return "changed";
@@ -154,7 +172,12 @@ async function approveAndRunUpdateAll(
154
172
  ctx.ui.notify(failedNames.length > 0 ? `No packages updated; ${failedNames.length} failed.` : "All packages already up to date.", failedNames.length > 0 ? "warning" : "info");
155
173
  return failedNames.length > 0 ? "cancelled" : "unchanged";
156
174
  }
157
- ctx.ui.notify(`Updated ${changed} package(s)${failedNames.length > 0 ? `, ${failedNames.length} failed` : ""}; reloading Pi resources.`, "info");
175
+ const failedSuffix = failedNames.length > 0 ? `, ${failedNames.length} failed` : "";
176
+ if (!(await confirmReload(ctx))) {
177
+ ctx.ui.notify(`Updated ${changed} package(s)${failedSuffix}; reload pending -- run /reload when ready.`, "warning");
178
+ return "deferred";
179
+ }
180
+ ctx.ui.notify(`Updated ${changed} package(s)${failedSuffix}; reloading Pi resources.`, "info");
158
181
  await ctx.reload();
159
182
  return "changed";
160
183
  }
@@ -237,7 +260,12 @@ export async function applyDisableExtensions(row: Row, natives: Natives, ctx: Ex
237
260
  else if (outcome === "cancelled") return toggled > 0 ? "changed" : "cancelled";
238
261
  }
239
262
  if (toggled === 0) return "unchanged";
240
- ctx.ui.notify(`${disabling ? "Disabled" : "Enabled"} ${toggled} extension(s) for ${row.name}; reloading Pi resources.`, "info");
263
+ const verb = disabling ? "Disabled" : "Enabled";
264
+ if (!(await confirmReload(ctx))) {
265
+ ctx.ui.notify(`${verb} ${toggled} extension(s) for ${row.name}; reload pending -- run /reload when ready.`, "warning");
266
+ return "deferred";
267
+ }
268
+ ctx.ui.notify(`${verb} ${toggled} extension(s) for ${row.name}; reloading Pi resources.`, "info");
241
269
  await ctx.reload();
242
270
  return "changed";
243
271
  }
@@ -285,6 +313,14 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
285
313
  return;
286
314
  }
287
315
 
316
+ // A deferred reload means the mutation itself genuinely happened but
317
+ // ctx.reload() was declined -- the session is still alive, so refresh
318
+ // rows (real on-disk versions) and keep the panel open, same as "refresh".
319
+ async function refreshRows(): Promise<void> {
320
+ ({ rows, error } = await loadRows(natives));
321
+ if (error) ctx.ui.notify(`refresh failed: ${error}`, "error");
322
+ }
323
+
288
324
  // Panel loop: actions resolve the component, run outside it, then reopen.
289
325
  // U/updateAll is handled entirely inside renderPanel itself (progress
290
326
  // rendered on the same already-open overlay) and never reaches here.
@@ -293,8 +329,7 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
293
329
  if (!action) return; // closed
294
330
 
295
331
  if (action.type === "refresh") {
296
- ({ rows, error } = await loadRows(natives));
297
- if (error) ctx.ui.notify(`refresh failed: ${error}`, "error");
332
+ await refreshRows();
298
333
  continue;
299
334
  }
300
335
 
@@ -319,11 +354,13 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
319
354
  if (action.type === "disable") {
320
355
  const outcome = await applyDisableExtensions(row, natives, ctx);
321
356
  if (outcome === "changed") return;
357
+ if (outcome === "deferred") await refreshRows();
322
358
  continue;
323
359
  }
324
360
 
325
361
  const outcome = await applyPackageChoice(action.type === "update" ? `Update to ${row.latest}` : "Remove", row, natives, ctx);
326
362
  if (outcome === "changed") return; // ctx.reload() already replaced the session
363
+ if (outcome === "deferred") await refreshRows();
327
364
  }
328
365
  }
329
366
 
@@ -576,5 +613,14 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
576
613
  tui.requestRender();
577
614
  },
578
615
  };
579
- }, { overlay: true, overlayOptions: { width: "70%", maxHeight: "70%", anchor: "top-center", offsetY: 1 } });
616
+ // row is a real percentage of terminal height (0%=top, 100%=bottom) --
617
+ // unlike anchor:"top-center"+offsetY (a fixed row count, effectively
618
+ // pinned to the very top on any terminal size), this scales with the
619
+ // actual screen. anchor:"center" still governs horizontal centering
620
+ // (row overrides only the vertical resolution). Note this isn't fully
621
+ // content-height-independent -- pi-tui's own row-percentage formula
622
+ // interpolates over the range of positions that still fit the current
623
+ // content height, so a shorter/taller row count shifts this somewhat;
624
+ // only exactly 0%/100% are perfectly jitter-free in that library.
625
+ }, { overlay: true, overlayOptions: { width: "70%", maxHeight: "70%", anchor: "center", row: "40%" } });
580
626
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "Pi package tools, commands, profiles, and TUI for the Packed daemon",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],