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,29 +1,34 @@
1
1
  /**
2
2
  * Package management (update, remove)
3
3
  */
4
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
5
- import type { InstalledPackage } from "../types/index.js";
6
4
  import {
7
- getInstalledPackages,
8
- clearSearchCache,
9
- parseInstalledPackagesOutputAllScopes,
10
- isSourceInstalled,
11
- } from "./discovery.js";
12
- import { waitForCondition } from "../utils/retry.js";
5
+ type ExtensionAPI,
6
+ type ExtensionCommandContext,
7
+ getAgentDir,
8
+ type ProgressEvent,
9
+ } from "@mariozechner/pi-coding-agent";
10
+ import { UI } from "../constants.js";
11
+ import { type InstalledPackage } from "../types/index.js";
12
+ import { runTaskWithLoader } from "../ui/async-task.js";
13
13
  import { formatInstalledPackageLabel } from "../utils/format.js";
14
+ import { logPackageRemove, logPackageUpdate } from "../utils/history.js";
15
+ import { requireUI } from "../utils/mode.js";
16
+ import { notify, error as notifyError, success } from "../utils/notify.js";
14
17
  import { normalizePackageIdentity } from "../utils/package-source.js";
15
- import { logPackageUpdate, logPackageRemove } from "../utils/history.js";
16
18
  import { clearUpdatesAvailable } from "../utils/settings.js";
17
- import { notify, error as notifyError, success } from "../utils/notify.js";
19
+ import { updateExtmgrStatus } from "../utils/status.js";
18
20
  import {
19
21
  confirmAction,
20
22
  confirmReload,
21
- showProgress,
22
23
  formatListOutput,
24
+ showProgress,
23
25
  } from "../utils/ui-helpers.js";
24
- import { requireUI } from "../utils/mode.js";
25
- import { updateExtmgrStatus } from "../utils/status.js";
26
- import { TIMEOUTS, UI } from "../constants.js";
26
+ import { getPackageCatalog } from "./catalog.js";
27
+ import {
28
+ clearSearchCache,
29
+ getInstalledPackages,
30
+ getInstalledPackagesAllScopes,
31
+ } from "./discovery.js";
27
32
 
28
33
  export interface PackageMutationOutcome {
29
34
  reloaded: boolean;
@@ -41,9 +46,8 @@ function packageMutationOutcome(
41
46
  return { ...NO_PACKAGE_MUTATION_OUTCOME, ...overrides };
42
47
  }
43
48
 
44
- function isUpToDateOutput(stdout: string): boolean {
45
- const pinnedAsStatus = /^\s*pinned\b(?!\s+dependency\b)(?:\s*$|\s*[:(-])/im.test(stdout);
46
- return /already\s+up\s+to\s+date/i.test(stdout) || pinnedAsStatus;
49
+ function getProgressMessage(event: ProgressEvent, fallback: string): string {
50
+ return event.message?.trim() || fallback;
47
51
  }
48
52
 
49
53
  async function updatePackageInternal(
@@ -53,29 +57,46 @@ async function updatePackageInternal(
53
57
  ): Promise<PackageMutationOutcome> {
54
58
  showProgress(ctx, "Updating", source);
55
59
 
56
- const res = await pi.exec("pi", ["update", source], {
57
- timeout: TIMEOUTS.packageUpdate,
58
- cwd: ctx.cwd,
59
- });
60
+ const updateIdentity = normalizePackageIdentity(source, { cwd: ctx.cwd });
61
+
62
+ try {
63
+ const updates = await getPackageCatalog(ctx.cwd).checkForAvailableUpdates();
64
+ const hasUpdate = updates.some(
65
+ (update) => normalizePackageIdentity(update.source) === updateIdentity
66
+ );
67
+
68
+ if (!hasUpdate) {
69
+ notify(ctx, `${source} is already up to date (or pinned).`, "info");
70
+ logPackageUpdate(pi, source, source, undefined, true);
71
+ clearUpdatesAvailable(pi, ctx, [updateIdentity]);
72
+ void updateExtmgrStatus(ctx, pi);
73
+ return NO_PACKAGE_MUTATION_OUTCOME;
74
+ }
60
75
 
61
- if (res.code !== 0) {
62
- const errorMsg = `Update failed: ${res.stderr || res.stdout || `exit ${res.code}`}`;
76
+ await runTaskWithLoader(
77
+ ctx,
78
+ {
79
+ title: "Update Package",
80
+ message: `Updating ${source}...`,
81
+ cancellable: false,
82
+ fallbackWithoutLoader: true,
83
+ },
84
+ async ({ setMessage }) => {
85
+ await getPackageCatalog(ctx.cwd).update(source, (event) => {
86
+ setMessage(getProgressMessage(event, `Updating ${source}...`));
87
+ });
88
+ return undefined;
89
+ }
90
+ );
91
+ } catch (error) {
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ const errorMsg = `Update failed: ${message}`;
63
94
  logPackageUpdate(pi, source, source, undefined, false, errorMsg);
64
95
  notifyError(ctx, errorMsg);
65
96
  void updateExtmgrStatus(ctx, pi);
66
97
  return NO_PACKAGE_MUTATION_OUTCOME;
67
98
  }
68
99
 
69
- const updateIdentity = normalizePackageIdentity(source);
70
- const stdout = res.stdout || "";
71
- if (isUpToDateOutput(stdout)) {
72
- notify(ctx, `${source} is already up to date (or pinned).`, "info");
73
- logPackageUpdate(pi, source, source, undefined, true);
74
- clearUpdatesAvailable(pi, ctx, [updateIdentity]);
75
- void updateExtmgrStatus(ctx, pi);
76
- return NO_PACKAGE_MUTATION_OUTCOME;
77
- }
78
-
79
100
  logPackageUpdate(pi, source, source, undefined, true);
80
101
  success(ctx, `Updated ${source}`);
81
102
  clearUpdatesAvailable(pi, ctx, [updateIdentity]);
@@ -93,25 +114,40 @@ async function updatePackagesInternal(
93
114
  ): Promise<PackageMutationOutcome> {
94
115
  showProgress(ctx, "Updating", "all packages");
95
116
 
96
- const res = await pi.exec("pi", ["update"], { timeout: TIMEOUTS.packageUpdateAll, cwd: ctx.cwd });
117
+ try {
118
+ const updates = await getPackageCatalog(ctx.cwd).checkForAvailableUpdates();
119
+ if (updates.length === 0) {
120
+ notify(ctx, "All packages are already up to date.", "info");
121
+ logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
122
+ clearUpdatesAvailable(pi, ctx);
123
+ void updateExtmgrStatus(ctx, pi);
124
+ return NO_PACKAGE_MUTATION_OUTCOME;
125
+ }
97
126
 
98
- if (res.code !== 0) {
99
- const errorMsg = `Update failed: ${res.stderr || res.stdout || `exit ${res.code}`}`;
127
+ await runTaskWithLoader(
128
+ ctx,
129
+ {
130
+ title: "Update Packages",
131
+ message: "Updating all packages...",
132
+ cancellable: false,
133
+ fallbackWithoutLoader: true,
134
+ },
135
+ async ({ setMessage }) => {
136
+ await getPackageCatalog(ctx.cwd).update(undefined, (event) => {
137
+ setMessage(getProgressMessage(event, "Updating all packages..."));
138
+ });
139
+ return undefined;
140
+ }
141
+ );
142
+ } catch (error) {
143
+ const message = error instanceof Error ? error.message : String(error);
144
+ const errorMsg = `Update failed: ${message}`;
100
145
  logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, false, errorMsg);
101
146
  notifyError(ctx, errorMsg);
102
147
  void updateExtmgrStatus(ctx, pi);
103
148
  return NO_PACKAGE_MUTATION_OUTCOME;
104
149
  }
105
150
 
106
- const stdout = res.stdout || "";
107
- if (isUpToDateOutput(stdout) || stdout.trim() === "") {
108
- notify(ctx, "All packages are already up to date.", "info");
109
- logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
110
- clearUpdatesAvailable(pi, ctx);
111
- void updateExtmgrStatus(ctx, pi);
112
- return NO_PACKAGE_MUTATION_OUTCOME;
113
- }
114
-
115
151
  logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
116
152
  success(ctx, "Packages updated");
117
153
  clearUpdatesAvailable(pi, ctx);
@@ -153,17 +189,37 @@ export async function updatePackagesWithOutcome(
153
189
  return updatePackagesInternal(ctx, pi);
154
190
  }
155
191
 
156
- function packageIdentity(source: string): string {
157
- return normalizePackageIdentity(source);
192
+ function packageIdentity(
193
+ source: string,
194
+ options?: { resolvedPath?: string; cwd?: string }
195
+ ): string {
196
+ return normalizePackageIdentity(source, options);
158
197
  }
159
198
 
160
- async function getInstalledPackagesAllScopes(
161
- ctx: ExtensionCommandContext,
162
- pi: ExtensionAPI
199
+ function packageSourceIdentities(source: string, ctx: ExtensionCommandContext): Set<string> {
200
+ return new Set([
201
+ packageIdentity(source, { cwd: ctx.cwd }),
202
+ packageIdentity(source, { cwd: getAgentDir() }),
203
+ ]);
204
+ }
205
+
206
+ function installedPackageMatchesSource(
207
+ pkg: InstalledPackage,
208
+ identities: Set<string>,
209
+ ctx: ExtensionCommandContext
210
+ ): boolean {
211
+ return identities.has(
212
+ packageIdentity(pkg.source, {
213
+ ...(pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : {}),
214
+ cwd: pkg.scope === "project" ? ctx.cwd : getAgentDir(),
215
+ })
216
+ );
217
+ }
218
+
219
+ async function getInstalledPackagesAllScopesForRemoval(
220
+ ctx: ExtensionCommandContext
163
221
  ): Promise<InstalledPackage[]> {
164
- const res = await pi.exec("pi", ["list"], { timeout: TIMEOUTS.listPackages, cwd: ctx.cwd });
165
- if (res.code !== 0) return [];
166
- return parseInstalledPackagesOutputAllScopes(res.stdout || "");
222
+ return getInstalledPackagesAllScopes(ctx);
167
223
  }
168
224
 
169
225
  type RemovalScopeChoice = "both" | "global" | "project" | "cancel";
@@ -197,14 +253,9 @@ async function selectRemovalScope(ctx: ExtensionCommandContext): Promise<Removal
197
253
 
198
254
  function buildRemovalTargets(
199
255
  matching: InstalledPackage[],
200
- source: string,
201
256
  hasUI: boolean,
202
257
  scopeChoice: RemovalScopeChoice
203
258
  ): RemovalTarget[] {
204
- if (matching.length === 0) {
205
- return [{ scope: "global", source, name: source }];
206
- }
207
-
208
259
  const byScope = new Map(matching.map((pkg) => [pkg.scope, pkg] as const));
209
260
  const addTarget = (scope: "global" | "project") => {
210
261
  const pkg = byScope.get(scope);
@@ -219,7 +270,6 @@ function buildRemovalTargets(
219
270
  return addTarget("global");
220
271
  case "project":
221
272
  return addTarget("project");
222
- case "cancel":
223
273
  default:
224
274
  return [];
225
275
  }
@@ -253,18 +303,31 @@ async function executeRemovalTargets(
253
303
  for (const target of targets) {
254
304
  showProgress(ctx, "Removing", `${target.source} (${target.scope})`);
255
305
 
256
- const args = ["remove", ...(target.scope === "project" ? ["-l"] : []), target.source];
257
- const res = await pi.exec("pi", args, { timeout: TIMEOUTS.packageRemove, cwd: ctx.cwd });
258
-
259
- if (res.code !== 0) {
260
- const errorMsg = `Remove failed (${target.scope}): ${res.stderr || res.stdout || `exit ${res.code}`}`;
306
+ try {
307
+ await runTaskWithLoader(
308
+ ctx,
309
+ {
310
+ title: "Remove Package",
311
+ message: `Removing ${target.source}...`,
312
+ cancellable: false,
313
+ fallbackWithoutLoader: true,
314
+ },
315
+ async ({ setMessage }) => {
316
+ await getPackageCatalog(ctx.cwd).remove(target.source, target.scope, (event) => {
317
+ setMessage(getProgressMessage(event, `Removing ${target.source}...`));
318
+ });
319
+ return undefined;
320
+ }
321
+ );
322
+
323
+ logPackageRemove(pi, target.source, target.name, true);
324
+ results.push({ target, success: true });
325
+ } catch (error) {
326
+ const message = error instanceof Error ? error.message : String(error);
327
+ const errorMsg = `Remove failed (${target.scope}): ${message}`;
261
328
  logPackageRemove(pi, target.source, target.name, false, errorMsg);
262
329
  results.push({ target, success: false, error: errorMsg });
263
- continue;
264
330
  }
265
-
266
- logPackageRemove(pi, target.source, target.name, true);
267
- results.push({ target, success: true });
268
331
  }
269
332
 
270
333
  return results;
@@ -300,9 +363,9 @@ async function removePackageInternal(
300
363
  ctx: ExtensionCommandContext,
301
364
  pi: ExtensionAPI
302
365
  ): Promise<PackageMutationOutcome> {
303
- const installed = await getInstalledPackagesAllScopes(ctx, pi);
304
- const identity = packageIdentity(source);
305
- const matching = installed.filter((p) => packageIdentity(p.source) === identity);
366
+ const installed = await getInstalledPackagesAllScopesForRemoval(ctx);
367
+ const identities = packageSourceIdentities(source, ctx);
368
+ const matching = installed.filter((pkg) => installedPackageMatchesSource(pkg, identities, ctx));
306
369
 
307
370
  const hasBothScopes =
308
371
  matching.some((pkg) => pkg.scope === "global") &&
@@ -314,7 +377,12 @@ async function removePackageInternal(
314
377
  return NO_PACKAGE_MUTATION_OUTCOME;
315
378
  }
316
379
 
317
- const targets = buildRemovalTargets(matching, source, ctx.hasUI, scopeChoice);
380
+ if (matching.length === 0) {
381
+ notify(ctx, `${source} is not installed.`, "info");
382
+ return NO_PACKAGE_MUTATION_OUTCOME;
383
+ }
384
+
385
+ const targets = buildRemovalTargets(matching, ctx.hasUI, scopeChoice);
318
386
  if (targets.length === 0) {
319
387
  notify(ctx, "Nothing to remove.", "info");
320
388
  return NO_PACKAGE_MUTATION_OUTCOME;
@@ -343,39 +411,17 @@ async function removePackageInternal(
343
411
  .filter((result) => result.success)
344
412
  .map((result) => result.target);
345
413
 
346
- const remaining = (await getInstalledPackagesAllScopes(ctx, pi)).filter(
347
- (p) => packageIdentity(p.source) === identity
414
+ const remaining = (await getInstalledPackagesAllScopesForRemoval(ctx)).filter((pkg) =>
415
+ installedPackageMatchesSource(pkg, identities, ctx)
348
416
  );
349
417
  notifyRemovalSummary(source, remaining, failures, ctx);
350
418
 
351
419
  if (failures.length === 0 && remaining.length === 0) {
352
- clearUpdatesAvailable(pi, ctx, [identity]);
420
+ clearUpdatesAvailable(pi, ctx, identities);
353
421
  }
354
422
 
355
423
  const successfulRemovalCount = successfulTargets.length;
356
424
 
357
- // Wait for successfully removed targets to disappear from their target scopes before reloading.
358
- if (successfulTargets.length > 0) {
359
- notify(ctx, "Waiting for removal to complete...", "info");
360
- const isRemoved = await waitForCondition(
361
- async () => {
362
- const installedChecks = await Promise.all(
363
- successfulTargets.map((target) =>
364
- isSourceInstalled(target.source, ctx, pi, {
365
- scope: target.scope,
366
- })
367
- )
368
- );
369
- return installedChecks.every((installedInScope) => !installedInScope);
370
- },
371
- { maxAttempts: 10, delayMs: 100, backoff: "exponential" }
372
- );
373
-
374
- if (!isRemoved) {
375
- notify(ctx, "Extension may still be active. Restart pi manually if needed.", "warning");
376
- }
377
- }
378
-
379
425
  if (successfulRemovalCount === 0) {
380
426
  void updateExtmgrStatus(ctx, pi);
381
427
  return NO_PACKAGE_MUTATION_OUTCOME;
@@ -431,9 +477,9 @@ export async function promptRemove(ctx: ExtensionCommandContext, pi: ExtensionAP
431
477
 
432
478
  export async function showInstalledPackagesList(
433
479
  ctx: ExtensionCommandContext,
434
- pi: ExtensionAPI
480
+ _pi: ExtensionAPI
435
481
  ): Promise<void> {
436
- const packages = await getInstalledPackages(ctx, pi);
482
+ const packages = await getInstalledPackagesAllScopes(ctx);
437
483
 
438
484
  if (packages.length === 0) {
439
485
  notify(ctx, "No packages installed.", "info");
@@ -46,25 +46,32 @@ export interface PackageExtensionEntry {
46
46
  state: State;
47
47
  }
48
48
 
49
- export interface UnifiedItem {
50
- type: "local" | "package";
49
+ export interface LocalUnifiedItem {
50
+ type: "local";
51
51
  id: string;
52
52
  displayName: string;
53
53
  summary: string;
54
54
  scope: Scope;
55
- // Local extension fields
56
- state?: State | undefined;
57
- activePath?: string | undefined;
58
- disabledPath?: string | undefined;
59
- originalState?: State | undefined;
60
- // Package fields
61
- source?: string | undefined;
55
+ state: State;
56
+ activePath: string;
57
+ disabledPath: string;
58
+ originalState: State;
59
+ }
60
+
61
+ export interface PackageUnifiedItem {
62
+ type: "package";
63
+ id: string;
64
+ displayName: string;
65
+ scope: Scope;
66
+ source: string;
62
67
  version?: string | undefined;
63
68
  description?: string | undefined;
64
69
  size?: number | undefined; // Package size in bytes
65
70
  updateAvailable?: boolean | undefined;
66
71
  }
67
72
 
73
+ export type UnifiedItem = LocalUnifiedItem | PackageUnifiedItem;
74
+
68
75
  export interface SearchCache {
69
76
  query: string;
70
77
  results: NpmPackage[];
@@ -0,0 +1,194 @@
1
+ import {
2
+ DynamicBorder,
3
+ type ExtensionCommandContext,
4
+ type ExtensionContext,
5
+ type Theme,
6
+ } from "@mariozechner/pi-coding-agent";
7
+ import { CancellableLoader, Container, Loader, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
8
+ import { hasCustomUI } from "../utils/mode.js";
9
+
10
+ type AnyContext = ExtensionCommandContext | ExtensionContext;
11
+
12
+ const TASK_ABORTED = Symbol("task-aborted");
13
+ const TASK_FAILED = Symbol("task-failed");
14
+
15
+ type TaskSuccess<T> = { type: "ok"; value: T };
16
+
17
+ export interface TaskControls {
18
+ signal: AbortSignal;
19
+ setMessage: (message: string) => void;
20
+ }
21
+
22
+ interface LoaderConfig {
23
+ title: string;
24
+ message: string;
25
+ cancellable?: boolean;
26
+ fallbackWithoutLoader?: boolean;
27
+ }
28
+
29
+ function createLoaderComponent(
30
+ tui: TUI,
31
+ theme: Theme,
32
+ title: string,
33
+ message: string,
34
+ cancellable: boolean,
35
+ onCancel: () => void
36
+ ): {
37
+ container: Container;
38
+ loader: Loader | CancellableLoader;
39
+ signal: AbortSignal;
40
+ } {
41
+ const container = new Container();
42
+ const borderColor = (text: string) => theme.fg("accent", text);
43
+ const loader = cancellable
44
+ ? new CancellableLoader(
45
+ tui,
46
+ (text) => theme.fg("accent", text),
47
+ (text) => theme.fg("muted", text),
48
+ message
49
+ )
50
+ : new Loader(
51
+ tui,
52
+ (text) => theme.fg("accent", text),
53
+ (text) => theme.fg("muted", text),
54
+ message
55
+ );
56
+
57
+ container.addChild(new DynamicBorder(borderColor));
58
+ container.addChild(new Text(theme.fg("accent", theme.bold(title)), 1, 0));
59
+ container.addChild(loader);
60
+
61
+ if (cancellable) {
62
+ (loader as CancellableLoader).onAbort = onCancel;
63
+ container.addChild(new Spacer(1));
64
+ container.addChild(new Text(theme.fg("dim", "Esc cancel"), 1, 0));
65
+ }
66
+
67
+ container.addChild(new Spacer(1));
68
+ container.addChild(new DynamicBorder(borderColor));
69
+
70
+ const signal = cancellable ? (loader as CancellableLoader).signal : new AbortController().signal;
71
+
72
+ return { container, loader, signal };
73
+ }
74
+
75
+ function runTaskWithoutLoader<T>(task: (controls: TaskControls) => Promise<T>): Promise<T> {
76
+ return Promise.resolve().then(() =>
77
+ task({
78
+ signal: new AbortController().signal,
79
+ setMessage: () => undefined,
80
+ })
81
+ );
82
+ }
83
+
84
+ export async function runTaskWithLoader<T>(
85
+ ctx: AnyContext,
86
+ config: LoaderConfig,
87
+ task: (controls: TaskControls) => Promise<T>
88
+ ): Promise<T | undefined> {
89
+ if (!hasCustomUI(ctx)) {
90
+ return runTaskWithoutLoader(task);
91
+ }
92
+
93
+ let taskError: unknown;
94
+ let startedTask: Promise<T> | undefined;
95
+ let cleanupStartedTaskUI: (() => void) | undefined;
96
+
97
+ const result = await ctx.ui.custom<
98
+ TaskSuccess<T> | typeof TASK_ABORTED | typeof TASK_FAILED | undefined
99
+ >((tui, theme, _keybindings, done) => {
100
+ let finished = false;
101
+ const finish = (
102
+ value: TaskSuccess<T> | typeof TASK_ABORTED | typeof TASK_FAILED | undefined
103
+ ): void => {
104
+ if (finished) {
105
+ return;
106
+ }
107
+ finished = true;
108
+ done(value);
109
+ };
110
+
111
+ const { container, loader, signal } = createLoaderComponent(
112
+ tui,
113
+ theme,
114
+ config.title,
115
+ config.message,
116
+ config.cancellable ?? true,
117
+ () => finish(TASK_ABORTED)
118
+ );
119
+
120
+ cleanupStartedTaskUI = () => {
121
+ if (loader instanceof CancellableLoader) {
122
+ loader.dispose();
123
+ return;
124
+ }
125
+
126
+ loader.stop();
127
+ };
128
+
129
+ startedTask = Promise.resolve().then(() =>
130
+ task({
131
+ signal,
132
+ setMessage: (message) => {
133
+ loader.setMessage(message);
134
+ tui.requestRender();
135
+ },
136
+ })
137
+ );
138
+
139
+ void startedTask
140
+ .then((value) => finish({ type: "ok", value }))
141
+ .catch((error) => {
142
+ if (signal.aborted) {
143
+ finish(TASK_ABORTED);
144
+ return;
145
+ }
146
+
147
+ taskError = error;
148
+ finish(TASK_FAILED);
149
+ });
150
+
151
+ return {
152
+ render(width: number) {
153
+ return container.render(width);
154
+ },
155
+ invalidate() {
156
+ container.invalidate();
157
+ },
158
+ handleInput(data: string) {
159
+ if (loader instanceof CancellableLoader) {
160
+ loader.handleInput(data);
161
+ tui.requestRender();
162
+ }
163
+ },
164
+ dispose() {
165
+ if (loader instanceof CancellableLoader) {
166
+ loader.dispose();
167
+ return;
168
+ }
169
+
170
+ loader.stop();
171
+ },
172
+ };
173
+ });
174
+
175
+ if (result === undefined) {
176
+ if (startedTask) {
177
+ return startedTask.finally(() => cleanupStartedTaskUI?.());
178
+ }
179
+ if (config.fallbackWithoutLoader) {
180
+ return runTaskWithoutLoader(task);
181
+ }
182
+ return undefined;
183
+ }
184
+
185
+ if (result === TASK_ABORTED) {
186
+ return undefined;
187
+ }
188
+
189
+ if (result === TASK_FAILED) {
190
+ throw taskError;
191
+ }
192
+
193
+ return result.value;
194
+ }
package/src/ui/footer.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * Footer helpers for the unified extension manager UI
3
3
  */
4
- import type { UnifiedItem, State } from "../types/index.js";
4
+ import { type State, type UnifiedItem } from "../types/index.js";
5
5
 
6
6
  export interface FooterState {
7
- hasToggleRows: boolean;
8
7
  hasLocals: boolean;
9
8
  hasPackages: boolean;
10
9
  }
@@ -13,11 +12,8 @@ export interface FooterState {
13
12
  * Build footer state from visible items.
14
13
  */
15
14
  export function buildFooterState(items: UnifiedItem[]): FooterState {
16
- const hasLocals = items.some((i) => i.type === "local");
17
-
18
15
  return {
19
- hasToggleRows: hasLocals,
20
- hasLocals,
16
+ hasLocals: items.some((i) => i.type === "local"),
21
17
  hasPackages: items.some((i) => i.type === "package"),
22
18
  };
23
19
  }
@@ -47,8 +43,8 @@ export function buildFooterShortcuts(state: FooterState): string {
47
43
  const parts: string[] = [];
48
44
  parts.push("↑↓ Navigate");
49
45
 
50
- if (state.hasToggleRows) parts.push("Space/Enter Toggle");
51
- if (state.hasToggleRows) parts.push("S Save");
46
+ if (state.hasLocals) parts.push("Space/Enter Toggle");
47
+ if (state.hasLocals) parts.push("S Save");
52
48
  if (state.hasPackages) parts.push("Enter/A Actions");
53
49
  if (state.hasPackages) parts.push("c Configure");
54
50
  if (state.hasPackages) parts.push("u Update");
package/src/ui/help.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Help display
3
3
  */
4
- import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
4
+ import { type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
5
5
 
6
6
  export function showHelp(ctx: ExtensionCommandContext): void {
7
7
  const lines = [
@@ -17,7 +17,7 @@ export function showHelp(ctx: ExtensionCommandContext): void {
17
17
  " Space/Enter Toggle local extension enabled/disabled",
18
18
  " S Save changes to local extensions",
19
19
  " Enter/A Open actions for selected package",
20
- " c Configure selected package extensions (restart required after save)",
20
+ " c Configure selected package extensions (reload after save)",
21
21
  " u Update selected package",
22
22
  " X Remove selected item (package or local extension)",
23
23
  " i Quick install by source",