@danypops/pi-packed 0.13.2 → 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
 
@@ -19,7 +19,11 @@
19
19
  * per-row selection styling baked into each cell since Table's own
20
20
  * cellStyle is column-wide, not row-wide) inside this panel's own
21
21
  * scroll-window slice -- Table deliberately owns no pagination of its
22
- * 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
23
27
  * data flows through the packed CLI (thin seam).
24
28
  */
25
29
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
@@ -34,6 +38,7 @@ import { showPackedSettings } from "./security-tui.js";
34
38
  import { showResourceConfig, applyResourceToggle } from "./resource-config.js";
35
39
  import { showDiscoverPanel } from "./discover.js";
36
40
  import { menuTheme } from "./menu-theme.js";
41
+ import { confirmReload } from "./reload.js";
37
42
 
38
43
  interface PanelAction {
39
44
  type: "update" | "remove" | "disable" | "config" | "find" | "refresh" | "settings";
@@ -43,8 +48,11 @@ interface PanelAction {
43
48
  /** Outcome of a confirmed row action, resolved after any real mutation and
44
49
  * reload decision -- "changed" means the daemon state changed and Pi has
45
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;
46
54
  * "unchanged"/"cancelled" mean the panel keeps running as-is. */
47
- export type PackageChoiceOutcome = "changed" | "unchanged" | "cancelled";
55
+ export type PackageChoiceOutcome = "changed" | "unchanged" | "cancelled" | "deferred";
48
56
 
49
57
  export async function applyPackageChoice(
50
58
  choice: string | undefined,
@@ -70,6 +78,10 @@ export async function applyPackageChoice(
70
78
  return "unchanged";
71
79
  }
72
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
+ }
73
85
  ctx.ui.notify(`Updated ${row.name}${transition}; reloading Pi resources.`, "info");
74
86
  await ctx.reload();
75
87
  return "changed";
@@ -86,6 +98,10 @@ export async function applyPackageChoice(
86
98
  return "cancelled";
87
99
  }
88
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
+ }
89
105
  ctx.ui.notify(`Removed ${row.name}; reloading Pi resources.`, "info");
90
106
  await ctx.reload();
91
107
  return "changed";
@@ -156,7 +172,12 @@ async function approveAndRunUpdateAll(
156
172
  ctx.ui.notify(failedNames.length > 0 ? `No packages updated; ${failedNames.length} failed.` : "All packages already up to date.", failedNames.length > 0 ? "warning" : "info");
157
173
  return failedNames.length > 0 ? "cancelled" : "unchanged";
158
174
  }
159
- 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");
160
181
  await ctx.reload();
161
182
  return "changed";
162
183
  }
@@ -239,7 +260,12 @@ export async function applyDisableExtensions(row: Row, natives: Natives, ctx: Ex
239
260
  else if (outcome === "cancelled") return toggled > 0 ? "changed" : "cancelled";
240
261
  }
241
262
  if (toggled === 0) return "unchanged";
242
- 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");
243
269
  await ctx.reload();
244
270
  return "changed";
245
271
  }
@@ -287,6 +313,14 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
287
313
  return;
288
314
  }
289
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
+
290
324
  // Panel loop: actions resolve the component, run outside it, then reopen.
291
325
  // U/updateAll is handled entirely inside renderPanel itself (progress
292
326
  // rendered on the same already-open overlay) and never reaches here.
@@ -295,8 +329,7 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
295
329
  if (!action) return; // closed
296
330
 
297
331
  if (action.type === "refresh") {
298
- ({ rows, error } = await loadRows(natives));
299
- if (error) ctx.ui.notify(`refresh failed: ${error}`, "error");
332
+ await refreshRows();
300
333
  continue;
301
334
  }
302
335
 
@@ -321,11 +354,13 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
321
354
  if (action.type === "disable") {
322
355
  const outcome = await applyDisableExtensions(row, natives, ctx);
323
356
  if (outcome === "changed") return;
357
+ if (outcome === "deferred") await refreshRows();
324
358
  continue;
325
359
  }
326
360
 
327
361
  const outcome = await applyPackageChoice(action.type === "update" ? `Update to ${row.latest}` : "Remove", row, natives, ctx);
328
362
  if (outcome === "changed") return; // ctx.reload() already replaced the session
363
+ if (outcome === "deferred") await refreshRows();
329
364
  }
330
365
  }
331
366
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.13.2",
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"],