pi-extmgr 0.1.26 → 0.1.28

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.
@@ -1,30 +1,37 @@
1
1
  /**
2
2
  * Package extension configuration panel.
3
3
  */
4
- import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent";
5
- import { DynamicBorder, getSettingsListTheme } from "@mariozechner/pi-coding-agent";
4
+ import {
5
+ DynamicBorder,
6
+ type ExtensionAPI,
7
+ type ExtensionCommandContext,
8
+ getSettingsListTheme,
9
+ type Theme,
10
+ } from "@mariozechner/pi-coding-agent";
6
11
  import {
7
12
  Container,
8
13
  Key,
9
14
  matchesKey,
15
+ type SettingItem,
10
16
  SettingsList,
11
17
  Spacer,
12
18
  Text,
13
- type SettingItem,
14
19
  } from "@mariozechner/pi-tui";
15
- import type { InstalledPackage, PackageExtensionEntry, State } from "../types/index.js";
20
+ import { UI } from "../constants.js";
16
21
  import {
17
22
  applyPackageExtensionStateChanges,
18
23
  discoverPackageExtensions,
19
24
  validatePackageExtensionSettings,
20
25
  } from "../packages/extensions.js";
21
- import { notify } from "../utils/notify.js";
26
+ import { type InstalledPackage, type PackageExtensionEntry, type State } from "../types/index.js";
27
+ import { fileExists } from "../utils/fs.js";
22
28
  import { logExtensionToggle } from "../utils/history.js";
23
29
  import { requireCustomUI, runCustomUI } from "../utils/mode.js";
30
+ import { notify } from "../utils/notify.js";
24
31
  import { getPackageSourceKind } from "../utils/package-source.js";
25
32
  import { getSettingsListSelectedIndex } from "../utils/settings-list.js";
26
- import { fileExists } from "../utils/fs.js";
27
- import { UI } from "../constants.js";
33
+ import { confirmReload } from "../utils/ui-helpers.js";
34
+ import { runTaskWithLoader } from "./async-task.js";
28
35
  import { getChangeMarker, getPackageIcon, getScopeIcon, getStatusIcon } from "./theme.js";
29
36
 
30
37
  export interface PackageConfigRow {
@@ -167,10 +174,14 @@ async function showConfigurePanel(
167
174
  getSettingsListTheme(),
168
175
  (id: string, newValue: string) => {
169
176
  const row = rowById.get(id);
170
- if (!row || !row.available) return;
177
+ if (!row?.available) return;
171
178
 
172
179
  const state = newValue as State;
173
- staged.set(id, state);
180
+ if (state === row.originalState) {
181
+ staged.delete(id);
182
+ } else {
183
+ staged.set(id, state);
184
+ }
174
185
 
175
186
  const settingsItem = settingsItems.find((item) => item.id === id);
176
187
  if (settingsItem) {
@@ -285,35 +296,6 @@ export async function applyPackageExtensionChanges(
285
296
  return { changed: changedRows.length, errors };
286
297
  }
287
298
 
288
- async function promptRestartForPackageConfig(ctx: ExtensionCommandContext): Promise<boolean> {
289
- if (!ctx.hasUI) {
290
- notify(
291
- ctx,
292
- "Restart pi to apply package extension configuration changes. /reload may not be enough.",
293
- "warning"
294
- );
295
- return false;
296
- }
297
-
298
- const restartNow = await ctx.ui.confirm(
299
- "Restart Required",
300
- "Package extension configuration changed.\nA full pi restart is required to apply it.\nExit pi now?"
301
- );
302
-
303
- if (!restartNow) {
304
- notify(
305
- ctx,
306
- "Restart pi manually to apply package extension configuration changes. /reload may not be enough.",
307
- "warning"
308
- );
309
- return false;
310
- }
311
-
312
- notify(ctx, "Shutting down pi. Start it again to apply changes.", "info");
313
- ctx.shutdown();
314
- return true;
315
- }
316
-
317
299
  export async function configurePackageExtensions(
318
300
  pkg: InstalledPackage,
319
301
  ctx: ExtensionCommandContext,
@@ -329,8 +311,32 @@ export async function configurePackageExtensions(
329
311
  return { changed: 0, reloaded: false };
330
312
  }
331
313
 
332
- const discovered = await discoverPackageExtensions([pkg], ctx.cwd);
333
- const rows = await buildPackageConfigRows(discovered);
314
+ let initialData: { rows: PackageConfigRow[] } | undefined;
315
+ try {
316
+ initialData = await runTaskWithLoader(
317
+ ctx,
318
+ {
319
+ title: `Configure ${pkg.name}`,
320
+ message: "Discovering package extensions...",
321
+ cancellable: false,
322
+ },
323
+ async () => {
324
+ const discovered = await discoverPackageExtensions([pkg], ctx.cwd);
325
+ const rows = await buildPackageConfigRows(discovered);
326
+ return { rows };
327
+ }
328
+ );
329
+ } catch (error) {
330
+ notify(ctx, error instanceof Error ? error.message : String(error), "error");
331
+ return { changed: 0, reloaded: false };
332
+ }
333
+
334
+ if (!initialData) {
335
+ notify(ctx, "Package extension configuration requires the full interactive TUI.", "warning");
336
+ return { changed: 0, reloaded: false };
337
+ }
338
+
339
+ const { rows } = initialData;
334
340
 
335
341
  if (rows.length === 0) {
336
342
  notify(ctx, "No configurable extensions discovered for this package.", "info");
@@ -368,24 +374,31 @@ export async function configurePackageExtensions(
368
374
 
369
375
  const apply = await applyPackageExtensionChanges(rows, staged, pkg, ctx.cwd, pi);
370
376
 
377
+ if (apply.changed === 0) {
378
+ if (apply.errors.length > 0) {
379
+ notify(
380
+ ctx,
381
+ `Applied ${apply.changed} change(s), ${apply.errors.length} failed.\n${apply.errors.join("\n")}`,
382
+ "warning"
383
+ );
384
+ continue;
385
+ }
386
+
387
+ notify(ctx, "No changes to apply.", "info");
388
+ return { changed: 0, reloaded: false };
389
+ }
390
+
371
391
  if (apply.errors.length > 0) {
372
392
  notify(
373
393
  ctx,
374
394
  `Applied ${apply.changed} change(s), ${apply.errors.length} failed.\n${apply.errors.join("\n")}`,
375
395
  "warning"
376
396
  );
377
- } else if (apply.changed === 0) {
378
- notify(ctx, "No changes to apply.", "info");
379
- return { changed: 0, reloaded: false };
380
397
  } else {
381
398
  notify(ctx, `Applied ${apply.changed} package extension change(s).`, "info");
382
399
  }
383
400
 
384
- if (apply.changed === 0) {
385
- return { changed: 0, reloaded: false };
386
- }
387
-
388
- const restarted = await promptRestartForPackageConfig(ctx);
389
- return { changed: apply.changed, reloaded: restarted };
401
+ const reloaded = await confirmReload(ctx, "Package extension configuration changed.");
402
+ return { changed: apply.changed, reloaded };
390
403
  }
391
404
  }
package/src/ui/remote.ts CHANGED
@@ -1,23 +1,27 @@
1
1
  /**
2
2
  * Remote package browsing UI
3
3
  */
4
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
5
- import { DynamicBorder } from "@mariozechner/pi-coding-agent";
6
- import { Container, SelectList, Text, type SelectItem } from "@mariozechner/pi-tui";
7
- import type { BrowseAction, NpmPackage } from "../types/index.js";
8
- import { PAGE_SIZE, TIMEOUTS, CACHE_LIMITS } from "../constants.js";
9
- import { truncate, dynamicTruncate, formatBytes } from "../utils/format.js";
10
- import { parseChoiceByLabel, splitCommandArgs } from "../utils/command.js";
11
4
  import {
12
- searchNpmPackages,
5
+ DynamicBorder,
6
+ type ExtensionAPI,
7
+ type ExtensionCommandContext,
8
+ } from "@mariozechner/pi-coding-agent";
9
+ import { Container, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
10
+ import { CACHE_LIMITS, PAGE_SIZE, TIMEOUTS } from "../constants.js";
11
+ import {
13
12
  getSearchCache,
14
- setSearchCache,
15
13
  isCacheValid,
14
+ searchNpmPackages,
15
+ setSearchCache,
16
16
  } from "../packages/discovery.js";
17
17
  import { installPackage, installPackageLocally } from "../packages/install.js";
18
- import { execNpm } from "../utils/npm-exec.js";
19
- import { notify } from "../utils/notify.js";
18
+ import { type BrowseAction, type NpmPackage } from "../types/index.js";
19
+ import { parseChoiceByLabel, splitCommandArgs } from "../utils/command.js";
20
+ import { dynamicTruncate, formatBytes, truncate } from "../utils/format.js";
20
21
  import { requireCustomUI, runCustomUI } from "../utils/mode.js";
22
+ import { notify } from "../utils/notify.js";
23
+ import { execNpm } from "../utils/npm-exec.js";
24
+ import { runTaskWithLoader } from "./async-task.js";
21
25
 
22
26
  interface PackageInfoCacheEntry {
23
27
  timestamp: number;
@@ -109,25 +113,44 @@ const PACKAGE_DETAILS_CHOICES = {
109
113
  back: "Back to results",
110
114
  } as const;
111
115
 
116
+ function createAbortError(): Error {
117
+ const error = new Error("Operation cancelled");
118
+ error.name = "AbortError";
119
+ return error;
120
+ }
121
+
122
+ function throwIfAborted(signal?: AbortSignal): void {
123
+ if (signal?.aborted) {
124
+ throw createAbortError();
125
+ }
126
+ }
127
+
112
128
  function formatCount(value: number | undefined): string {
113
129
  if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
114
130
  return new Intl.NumberFormat().format(value);
115
131
  }
116
132
 
117
- async function fetchWeeklyDownloads(packageName: string): Promise<number | undefined> {
133
+ async function fetchWeeklyDownloads(
134
+ packageName: string,
135
+ signal?: AbortSignal
136
+ ): Promise<number | undefined> {
118
137
  const controller = new AbortController();
119
138
  const timer = setTimeout(() => controller.abort(), TIMEOUTS.weeklyDownloads);
139
+ const combinedSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
120
140
 
121
141
  try {
122
142
  const encoded = encodeURIComponent(packageName);
123
143
  const res = await fetch(`https://api.npmjs.org/downloads/point/last-week/${encoded}`, {
124
- signal: controller.signal,
144
+ signal: combinedSignal,
125
145
  });
126
146
 
127
147
  if (!res.ok) return undefined;
128
148
  const data = (await res.json()) as NpmDownloadsPoint;
129
149
  return typeof data.downloads === "number" ? data.downloads : undefined;
130
- } catch {
150
+ } catch (error) {
151
+ if (signal?.aborted && error instanceof Error && error.name === "AbortError") {
152
+ throw error;
153
+ }
131
154
  return undefined;
132
155
  } finally {
133
156
  clearTimeout(timer);
@@ -137,7 +160,8 @@ async function fetchWeeklyDownloads(packageName: string): Promise<number | undef
137
160
  async function buildPackageInfoText(
138
161
  packageName: string,
139
162
  ctx: ExtensionCommandContext,
140
- pi: ExtensionAPI
163
+ pi: ExtensionAPI,
164
+ signal?: AbortSignal
141
165
  ): Promise<string> {
142
166
  // Check cache first
143
167
  const cached = packageInfoCache.get(packageName);
@@ -148,10 +172,13 @@ async function buildPackageInfoText(
148
172
  const [infoRes, weeklyDownloads] = await Promise.all([
149
173
  execNpm(pi, ["view", packageName, "--json"], ctx, {
150
174
  timeout: TIMEOUTS.npmView,
175
+ ...(signal ? { signal } : {}),
151
176
  }),
152
- fetchWeeklyDownloads(packageName),
177
+ fetchWeeklyDownloads(packageName, signal),
153
178
  ]);
154
179
 
180
+ throwIfAborted(signal);
181
+
155
182
  if (infoRes.code !== 0) {
156
183
  throw new Error(infoRes.stderr || infoRes.stdout || `npm view failed (exit ${infoRes.code})`);
157
184
  }
@@ -179,7 +206,7 @@ async function buildPackageInfoText(
179
206
 
180
207
  const text = lines.join("\n");
181
208
 
182
- // Store in LRU cache
209
+ throwIfAborted(signal);
183
210
  packageInfoCache.set(packageName, { text });
184
211
 
185
212
  return text;
@@ -346,20 +373,34 @@ export async function browseRemotePackages(
346
373
  return;
347
374
  }
348
375
 
349
- // Check cache first
350
- let allPackages: NpmPackage[] = [];
376
+ let allPackages: NpmPackage[] | undefined;
351
377
 
352
- if (isCacheValid(query) && offset > 0) {
378
+ if (isCacheValid(query)) {
353
379
  const cache = getSearchCache();
354
- if (cache) allPackages = cache.results;
355
- } else {
356
- // Show searching notification
357
- ctx.ui.notify(`Searching npm for: ${truncate(query, 40)}...`, "info");
380
+ if (cache) {
381
+ allPackages = cache.results;
382
+ }
383
+ }
358
384
 
359
- // Perform search
360
- allPackages = await searchNpmPackages(query, ctx, pi);
385
+ if (!allPackages) {
386
+ const results = await runTaskWithLoader(
387
+ ctx,
388
+ {
389
+ title: "Remote Packages",
390
+ message: `Searching npm for ${truncate(query, 40)}...`,
391
+ },
392
+ async ({ signal, setMessage }) => {
393
+ setMessage(`Searching npm for ${truncate(query, 40)}...`);
394
+ return searchNpmPackages(query, ctx, { signal });
395
+ }
396
+ );
397
+
398
+ if (!results) {
399
+ notify(ctx, "Remote package search was cancelled.", "info");
400
+ return;
401
+ }
361
402
 
362
- // Cache results for pagination
403
+ allPackages = results;
363
404
  setSearchCache({
364
405
  query,
365
406
  results: allPackages,
@@ -451,7 +492,21 @@ async function showPackageDetails(
451
492
  return;
452
493
  case "viewInfo":
453
494
  try {
454
- const text = await buildPackageInfoText(packageName, ctx, pi);
495
+ const text = await runTaskWithLoader(
496
+ ctx,
497
+ {
498
+ title: packageName,
499
+ message: `Fetching package details for ${packageName}...`,
500
+ },
501
+ ({ signal }) => buildPackageInfoText(packageName, ctx, pi, signal)
502
+ );
503
+
504
+ if (!text) {
505
+ notify(ctx, `Loading ${packageName} details was cancelled.`, "info");
506
+ await showPackageDetails(packageName, ctx, pi, previousQuery, previousOffset);
507
+ return;
508
+ }
509
+
455
510
  ctx.ui.notify(text, "info");
456
511
  } catch (error) {
457
512
  const message = error instanceof Error ? error.message : String(error);
package/src/ui/theme.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Theme utilities for consistent UI styling across dark/light themes
3
3
  */
4
- import type { Theme } from "@mariozechner/pi-coding-agent";
4
+ import { type Theme } from "@mariozechner/pi-coding-agent";
5
5
 
6
6
  /**
7
7
  * Status icons that work across themes
@@ -62,7 +62,7 @@ export function getScopeIcon(
62
62
  */
63
63
  export function getChangeMarker(theme: Theme, hasChanges: boolean): string {
64
64
  if (!hasChanges) return "";
65
- return " " + theme.fg("warning", "*");
65
+ return ` ${theme.fg("warning", "*")}`;
66
66
  }
67
67
 
68
68
  /**