@danypops/pi-packed 0.8.0 → 0.10.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.
@@ -5,13 +5,15 @@
5
5
  * itself never mutates; only Install/Cancel (behind the standard
6
6
  * approval) does.
7
7
  */
8
- import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
8
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
9
9
  import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
10
- import { Container, Input, SelectList, type SelectItem, type SelectListTheme, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
+ import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
+ import { Menu, type MenuItem } from "malevich-tui-components";
11
12
  import { shouldSearch } from "./discover-model.js";
12
13
  import type { Natives, PackageSummary } from "./packed.js";
13
14
  import { InstallServiceError } from "./packed.js";
14
15
  import { approvePackageOperation } from "./tools.js";
16
+ import { menuTheme } from "./menu-theme.js";
15
17
 
16
18
  const SEARCH_LIMIT = 20;
17
19
 
@@ -51,28 +53,17 @@ export async function applyInstall(result: PackageSummary, natives: Natives, ctx
51
53
  }
52
54
  }
53
55
 
54
- function selectListTheme(theme: Theme): SelectListTheme {
55
- return {
56
- selectedPrefix: (text) => theme.fg("accent", text),
57
- selectedText: (text) => theme.fg("accent", text),
58
- description: (text) => theme.fg("muted", text),
59
- scrollInfo: (text) => theme.fg("muted", text),
60
- noMatch: (text) => theme.fg("muted", text),
61
- };
62
- }
63
-
64
56
  async function showInstallMenu(ctx: ExtensionCommandContext, result: PackageSummary): Promise<boolean> {
65
- const items: SelectItem[] = [{ value: "install", label: `Install ${result.name}@${result.version}` }, { value: "cancel", label: "Cancel" }];
66
- const choice = await ctx.ui.custom<"install" | "cancel" | undefined>(
57
+ return ctx.ui.custom<boolean>(
67
58
  (_tui, theme, _kb, done) => {
68
- const list = new SelectList(items, items.length, selectListTheme(theme));
69
- list.onSelect = (item) => done(item.value as "install" | "cancel");
70
- list.onCancel = () => done(undefined);
71
- return list;
59
+ const items: MenuItem[] = [
60
+ { label: `Install ${result.name}@${result.version}`, action: () => done(true) },
61
+ { label: "Cancel", action: () => done(false) },
62
+ ];
63
+ return new Menu({ items, theme: menuTheme(theme), onClose: () => done(false) });
72
64
  },
73
65
  { overlay: true, overlayOptions: { width: 40, anchor: "center" } },
74
66
  );
75
- return choice === "install";
76
67
  }
77
68
 
78
69
  export async function showDiscoverPanel(ctx: ExtensionCommandContext, natives: Natives): Promise<void> {
@@ -158,15 +149,16 @@ function renderDiscoverPanel(ctx: ExtensionCommandContext, natives: Natives): Pr
158
149
  },
159
150
  };
160
151
 
152
+ const border = () => new DynamicBorder((s) => theme.fg("border", s));
161
153
  const container = new Container();
162
154
  container.addChild(new Spacer(1));
163
- container.addChild(new DynamicBorder());
155
+ container.addChild(border());
164
156
  container.addChild(new Spacer(1));
165
157
  container.addChild(header);
166
158
  container.addChild(new Spacer(1));
167
159
  container.addChild(list);
168
160
  container.addChild(new Spacer(1));
169
- container.addChild(new DynamicBorder());
161
+ container.addChild(border());
170
162
 
171
163
  return {
172
164
  render: (width: number) => container.render(width),
@@ -0,0 +1,14 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import type { MenuTheme } from "malevich-tui-components";
3
+
4
+ /** Maps Pi's own Theme onto Malevich's Menu -- the one place this mapping
5
+ * exists, shared by every ctx.ui.custom overlay menu in this extension. */
6
+ export function menuTheme(theme: Theme): MenuTheme {
7
+ return {
8
+ border: (s) => theme.fg("border", s),
9
+ selected: (s) => theme.fg("accent", s),
10
+ normal: (s) => s,
11
+ dim: (s) => theme.fg("muted", s),
12
+ title: (s) => theme.fg("accent", s),
13
+ };
14
+ }
@@ -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),
@@ -11,9 +11,10 @@
11
11
  * underneath never disappears. All data flows through the packed CLI
12
12
  * (thin seam).
13
13
  */
14
- import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
14
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
15
15
  import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
16
- import { Container, Input, SelectList, type SelectItem, type SelectListTheme, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
16
+ import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
17
+ import { Menu, ProgressBar, type MenuItem } from "malevich-tui-components";
17
18
  import { filterRows, mergeRows, nextMode, visibleRows } from "./model.js";
18
19
  import type { Row, ViewMode } from "./model.js";
19
20
  import type { Natives, PackageResources } from "./packed.js";
@@ -21,6 +22,7 @@ import { approvePackageOperation } from "./tools.js";
21
22
  import { showPackedSettings } from "./security-tui.js";
22
23
  import { showResourceConfig, applyResourceToggle } from "./resource-config.js";
23
24
  import { showDiscoverPanel } from "./discover.js";
25
+ import { menuTheme } from "./menu-theme.js";
24
26
 
25
27
  interface PanelAction {
26
28
  type: "update" | "updateAll" | "remove" | "disable" | "config" | "find" | "refresh" | "settings";
@@ -84,6 +86,57 @@ export async function applyPackageChoice(
84
86
  return "cancelled";
85
87
  }
86
88
 
89
+ interface UpdateAllResult { changed: number; failedNames: string[]; }
90
+
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. */
96
+ async function runUpdatesWithProgress(
97
+ outdated: Row[],
98
+ natives: Natives,
99
+ approved: boolean | undefined,
100
+ ctx: ExtensionCommandContext,
101
+ ): Promise<UpdateAllResult> {
102
+ return ctx.ui.custom<UpdateAllResult>(
103
+ (tui, theme, _kb, done) => {
104
+ const bar = new ProgressBar({ value: 0, max: outdated.length, label: `${outdated[0]?.name ?? ""} (1/${outdated.length})`, style: (s) => theme.fg("accent", s) });
105
+ const border = () => new DynamicBorder((s) => theme.fg("border", s));
106
+ const container = new Container();
107
+ container.addChild(new Spacer(1));
108
+ container.addChild(border());
109
+ container.addChild({ invalidate() {}, render: (_width: number) => [theme.bold("Updating packages")] });
110
+ container.addChild(new Spacer(1));
111
+ container.addChild(bar);
112
+ container.addChild(new Spacer(1));
113
+ container.addChild(border());
114
+
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
+ })();
133
+
134
+ return { render: (width: number) => container.render(width), invalidate: () => container.invalidate(), handleInput() {} };
135
+ },
136
+ { overlay: true, overlayOptions: { width: 50, anchor: "center" } },
137
+ );
138
+ }
139
+
87
140
  /** U -- update every outdated row with one combined approval and one
88
141
  * reload, instead of applyPackageChoice's own per-call reload (which
89
142
  * would end the session after the first successful update). */
@@ -103,23 +156,13 @@ export async function applyUpdateAll(rows: Row[], natives: Natives, ctx: Extensi
103
156
  ctx.ui.notify(approval.message ?? "update denied", "warning");
104
157
  return "cancelled";
105
158
  }
106
- ctx.ui.notify(`Updating ${outdated.length} package(s)…`, "info");
107
- let changed = 0;
108
- let failed = 0;
109
- for (const row of outdated) {
110
- try {
111
- const outcome = await natives.update(`npm:${row.name}`, approval.approved);
112
- if (outcome.reloadRequired) changed += 1;
113
- } catch (e) {
114
- failed += 1;
115
- ctx.ui.notify(`${row.name} update failed: ${e instanceof Error ? e.message : e}`, "error");
116
- }
117
- }
159
+ const { changed, failedNames } = await runUpdatesWithProgress(outdated, natives, approval.approved, ctx);
160
+ for (const failure of failedNames) ctx.ui.notify(`update failed: ${failure}`, "error");
118
161
  if (changed === 0) {
119
- ctx.ui.notify(failed > 0 ? `No packages updated; ${failed} failed.` : "All packages already up to date.", failed > 0 ? "warning" : "info");
120
- return failed > 0 ? "cancelled" : "unchanged";
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";
121
164
  }
122
- ctx.ui.notify(`Updated ${changed} package(s)${failed > 0 ? `, ${failed} failed` : ""}; reloading Pi resources.`, "info");
165
+ ctx.ui.notify(`Updated ${changed} package(s)${failedNames.length > 0 ? `, ${failedNames.length} failed` : ""}; reloading Pi resources.`, "info");
123
166
  await ctx.reload();
124
167
  return "changed";
125
168
  }
@@ -173,32 +216,20 @@ async function loadRows(natives: Natives): Promise<{ rows: Row[]; error?: string
173
216
  }
174
217
  }
175
218
 
176
- function selectListTheme(theme: Theme): SelectListTheme {
177
- return {
178
- selectedPrefix: (text) => theme.fg("accent", text),
179
- selectedText: (text) => theme.fg("accent", text),
180
- description: (text) => theme.fg("muted", text),
181
- scrollInfo: (text) => theme.fg("muted", text),
182
- noMatch: (text) => theme.fg("muted", text),
183
- };
184
- }
185
-
186
219
  /** Enter's action menu, floated on top of the still-open packages panel
187
220
  * via ctx.ui.custom's own overlay:true -- no full-screen teardown, unlike
188
221
  * the ctx.ui.select this replaced. */
189
222
  async function showActionMenu(ctx: ExtensionCommandContext, row: Row): Promise<"update" | "remove" | "disable" | "config" | undefined> {
190
- const items: SelectItem[] = [
191
- ...(row.hasUpdate ? [{ value: "update", label: `Update to ${row.latest}` }] : []),
192
- { value: "disable", label: "Disable/enable extensions" },
193
- { value: "config", label: "Configure resources" },
194
- { value: "remove", label: "Remove" },
195
- ];
196
- return ctx.ui.custom<"update" | "remove" | "disable" | "config" | undefined>(
223
+ type Choice = "update" | "remove" | "disable" | "config";
224
+ return ctx.ui.custom<Choice | undefined>(
197
225
  (_tui, theme, _kb, done) => {
198
- const list = new SelectList(items, items.length, selectListTheme(theme));
199
- list.onSelect = (item) => done(item.value as "update" | "remove" | "disable" | "config");
200
- list.onCancel = () => done(undefined);
201
- return list;
226
+ const items: MenuItem[] = [
227
+ ...(row.hasUpdate ? [{ label: `Update to ${row.latest}`, action: () => done("update" as Choice) }] : []),
228
+ { label: "Disable/enable extensions", action: () => done("disable") },
229
+ { label: "Configure resources", action: () => done("config") },
230
+ { label: "Remove", action: () => done("remove") },
231
+ ];
232
+ return new Menu({ items, theme: menuTheme(theme), onClose: () => done(undefined) });
202
233
  },
203
234
  { overlay: true, overlayOptions: { width: 40, anchor: "center" } },
204
235
  );
@@ -339,15 +370,16 @@ function renderPanel(ctx: ExtensionCommandContext, rows: Row[]): Promise<PanelAc
339
370
  },
340
371
  };
341
372
 
373
+ const border = () => new DynamicBorder((s) => theme.fg("border", s));
342
374
  const container = new Container();
343
375
  container.addChild(new Spacer(1));
344
- container.addChild(new DynamicBorder());
376
+ container.addChild(border());
345
377
  container.addChild(new Spacer(1));
346
378
  container.addChild(header);
347
379
  container.addChild(new Spacer(1));
348
380
  container.addChild(list);
349
381
  container.addChild(new Spacer(1));
350
- container.addChild(new DynamicBorder());
382
+ container.addChild(border());
351
383
 
352
384
  return {
353
385
  render: (width: number) => container.render(width),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Pi package tools, commands, profiles, and TUI for the Packed daemon",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -14,7 +14,8 @@
14
14
  "dependencies": {
15
15
  "@danypops/packed": "^0.2.0",
16
16
  "@danypops/vehicle-client": "^0.1.1",
17
- "@danypops/vehicle-client-pi": "^0.1.5"
17
+ "@danypops/vehicle-client-pi": "^0.1.5",
18
+ "malevich-tui-components": "^0.7.0"
18
19
  },
19
20
  "peerDependencies": {
20
21
  "@earendil-works/pi-coding-agent": "*",