pi-extmgr 0.1.28 → 0.2.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.
@@ -11,6 +11,7 @@ import {
11
11
  } from "@mariozechner/pi-coding-agent";
12
12
  import { TIMEOUTS } from "../constants.js";
13
13
  import { runTaskWithLoader } from "../ui/async-task.js";
14
+ import { parseChoiceByLabel } from "../utils/command.js";
14
15
  import { normalizePackageSource } from "../utils/format.js";
15
16
  import { fileExists } from "../utils/fs.js";
16
17
  import { logPackageInstall } from "../utils/history.js";
@@ -32,6 +33,17 @@ export interface InstallOptions {
32
33
  scope?: InstallScope;
33
34
  }
34
35
 
36
+ export interface InstallOutcome {
37
+ installed: boolean;
38
+ reloaded: boolean;
39
+ }
40
+
41
+ const INSTALL_SCOPE_CHOICES = {
42
+ global: "Global (~/.pi/agent/settings.json)",
43
+ project: "Project (.pi/settings.json)",
44
+ cancel: "Cancel",
45
+ } as const;
46
+
35
47
  function getProgressMessage(event: ProgressEvent, fallback: string): string {
36
48
  return event.message?.trim() || fallback;
37
49
  }
@@ -44,14 +56,12 @@ async function resolveInstallScope(
44
56
 
45
57
  if (!ctx.hasUI) return "global";
46
58
 
47
- const choice = await ctx.ui.select("Install scope", [
48
- "Global (~/.pi/agent/settings.json)",
49
- "Project (.pi/settings.json)",
50
- "Cancel",
51
- ]);
59
+ const choice = parseChoiceByLabel(
60
+ INSTALL_SCOPE_CHOICES,
61
+ await ctx.ui.select("Install scope", Object.values(INSTALL_SCOPE_CHOICES))
62
+ );
52
63
 
53
- if (!choice || choice === "Cancel") return undefined;
54
- return choice.startsWith("Project") ? "project" : "global";
64
+ return choice === "cancel" ? undefined : choice;
55
65
  }
56
66
 
57
67
  function getExtensionInstallDir(ctx: ExtensionCommandContext, scope: InstallScope): string {
@@ -61,28 +71,6 @@ function getExtensionInstallDir(ctx: ExtensionCommandContext, scope: InstallScop
61
71
  return join(homedir(), ".pi", "agent", "extensions");
62
72
  }
63
73
 
64
- interface GithubUrlInfo {
65
- owner: string;
66
- repo: string;
67
- branch: string;
68
- filePath: string;
69
- }
70
-
71
- /**
72
- * Safely extracts regex match groups with validation
73
- */
74
- function safeExtractGithubMatch(match: RegExpMatchArray | null): GithubUrlInfo | undefined {
75
- if (!match) return undefined;
76
-
77
- const [, owner, repo, branch, filePath] = match;
78
-
79
- if (!owner || !repo || !branch || !filePath) {
80
- return undefined;
81
- }
82
-
83
- return { owner, repo, branch, filePath };
84
- }
85
-
86
74
  async function ensureTarAvailable(
87
75
  pi: ExtensionAPI,
88
76
  ctx: ExtensionCommandContext
@@ -157,36 +145,35 @@ async function cleanupStandaloneTempArtifacts(tempDir: string, extractDir?: stri
157
145
  );
158
146
  }
159
147
 
160
- export async function installPackage(
148
+ async function installPackageInternal(
161
149
  source: string,
162
150
  ctx: ExtensionCommandContext,
163
151
  pi: ExtensionAPI,
164
152
  options?: InstallOptions
165
- ): Promise<void> {
153
+ ): Promise<InstallOutcome> {
166
154
  const scope = await resolveInstallScope(ctx, options?.scope);
167
155
  if (!scope) {
168
156
  notify(ctx, "Installation cancelled.", "info");
169
- return;
157
+ return { installed: false, reloaded: false };
170
158
  }
171
159
 
172
160
  // Check if it's a GitHub URL to a .ts file - handle as direct download
173
161
  const githubTsMatch = source.match(
174
162
  /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+\.ts)$/
175
163
  );
176
- const githubInfo = safeExtractGithubMatch(githubTsMatch);
177
- if (githubInfo) {
178
- const rawUrl = `https://raw.githubusercontent.com/${githubInfo.owner}/${githubInfo.repo}/${githubInfo.branch}/${githubInfo.filePath}`;
179
- const fileName =
180
- githubInfo.filePath.split("/").pop() || `${githubInfo.owner}-${githubInfo.repo}.ts`;
181
- await installFromUrl(rawUrl, fileName, ctx, pi, { scope });
182
- return;
164
+ if (githubTsMatch) {
165
+ const [, owner, repo, branch, filePath] = githubTsMatch;
166
+ if (owner && repo && branch && filePath) {
167
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${filePath}`;
168
+ const fileName = filePath.split("/").pop() || `${owner}-${repo}.ts`;
169
+ return await installFromUrl(rawUrl, fileName, ctx, pi, { scope });
170
+ }
183
171
  }
184
172
 
185
173
  // Check if it's already a raw URL to a .ts file
186
174
  if (source.match(/^https:\/\/raw\.githubusercontent\.com\/.*\.ts$/)) {
187
175
  const fileName = source.split("/").pop() || "extension.ts";
188
- await installFromUrl(source, fileName, ctx, pi, { scope });
189
- return;
176
+ return await installFromUrl(source, fileName, ctx, pi, { scope });
190
177
  }
191
178
 
192
179
  const normalized = normalizePackageSource(source);
@@ -199,7 +186,7 @@ export async function installPackage(
199
186
  );
200
187
  if (!confirmed) {
201
188
  notify(ctx, "Installation cancelled.", "info");
202
- return;
189
+ return { installed: false, reloaded: false };
203
190
  }
204
191
 
205
192
  showProgress(ctx, "Installing", normalized);
@@ -226,7 +213,7 @@ export async function installPackage(
226
213
  logPackageInstall(pi, normalized, normalized, undefined, scope, false, errorMsg);
227
214
  notifyError(ctx, errorMsg);
228
215
  void updateExtmgrStatus(ctx, pi);
229
- return;
216
+ return { installed: false, reloaded: false };
230
217
  }
231
218
 
232
219
  clearSearchCache();
@@ -238,6 +225,26 @@ export async function installPackage(
238
225
  if (!reloaded) {
239
226
  void updateExtmgrStatus(ctx, pi);
240
227
  }
228
+
229
+ return { installed: true, reloaded };
230
+ }
231
+
232
+ export async function installPackage(
233
+ source: string,
234
+ ctx: ExtensionCommandContext,
235
+ pi: ExtensionAPI,
236
+ options?: InstallOptions
237
+ ): Promise<void> {
238
+ await installPackageInternal(source, ctx, pi, options);
239
+ }
240
+
241
+ export async function installPackageWithOutcome(
242
+ source: string,
243
+ ctx: ExtensionCommandContext,
244
+ pi: ExtensionAPI,
245
+ options?: InstallOptions
246
+ ): Promise<InstallOutcome> {
247
+ return installPackageInternal(source, ctx, pi, options);
241
248
  }
242
249
 
243
250
  export async function installFromUrl(
@@ -246,11 +253,11 @@ export async function installFromUrl(
246
253
  ctx: ExtensionCommandContext,
247
254
  pi: ExtensionAPI,
248
255
  options?: InstallOptions
249
- ): Promise<void> {
256
+ ): Promise<InstallOutcome> {
250
257
  const scope = await resolveInstallScope(ctx, options?.scope);
251
258
  if (!scope) {
252
259
  notify(ctx, "Installation cancelled.", "info");
253
- return;
260
+ return { installed: false, reloaded: false };
254
261
  }
255
262
 
256
263
  const extensionDir = getExtensionInstallDir(ctx, scope);
@@ -263,7 +270,7 @@ export async function installFromUrl(
263
270
  );
264
271
  if (!confirmed) {
265
272
  notify(ctx, "Installation cancelled.", "info");
266
- return;
273
+ return { installed: false, reloaded: false };
267
274
  }
268
275
 
269
276
  const result = await tryOperation(
@@ -289,7 +296,7 @@ export async function installFromUrl(
289
296
  if (!result) {
290
297
  logPackageInstall(pi, url, fileName, undefined, scope, false, "Installation failed");
291
298
  void updateExtmgrStatus(ctx, pi);
292
- return;
299
+ return { installed: false, reloaded: false };
293
300
  }
294
301
 
295
302
  const { fileName: name, destPath } = result;
@@ -300,6 +307,8 @@ export async function installFromUrl(
300
307
  if (!reloaded) {
301
308
  void updateExtmgrStatus(ctx, pi);
302
309
  }
310
+
311
+ return { installed: true, reloaded };
303
312
  }
304
313
 
305
314
  /**
@@ -324,16 +333,16 @@ function parsePackageInfo(viewOutput: string): { version: string; tarballUrl: st
324
333
  }
325
334
  }
326
335
 
327
- export async function installPackageLocally(
336
+ async function installPackageLocallyInternal(
328
337
  packageName: string,
329
338
  ctx: ExtensionCommandContext,
330
339
  pi: ExtensionAPI,
331
340
  options?: InstallOptions
332
- ): Promise<void> {
341
+ ): Promise<InstallOutcome> {
333
342
  const scope = await resolveInstallScope(ctx, options?.scope);
334
343
  if (!scope) {
335
344
  notify(ctx, "Installation cancelled.", "info");
336
- return;
345
+ return { installed: false, reloaded: false };
337
346
  }
338
347
 
339
348
  const extensionDir = getExtensionInstallDir(ctx, scope);
@@ -346,7 +355,7 @@ export async function installPackageLocally(
346
355
  );
347
356
  if (!confirmed) {
348
357
  notify(ctx, "Installation cancelled.", "info");
349
- return;
358
+ return { installed: false, reloaded: false };
350
359
  }
351
360
 
352
361
  const result = await tryOperation(
@@ -384,7 +393,7 @@ export async function installPackageLocally(
384
393
  "Failed to fetch package info"
385
394
  );
386
395
  void updateExtmgrStatus(ctx, pi);
387
- return;
396
+ return { installed: false, reloaded: false };
388
397
  }
389
398
  const { version, tarballUrl } = result;
390
399
 
@@ -401,7 +410,7 @@ export async function installPackageLocally(
401
410
  tarAvailability.error
402
411
  );
403
412
  void updateExtmgrStatus(ctx, pi);
404
- return;
413
+ return { installed: false, reloaded: false };
405
414
  }
406
415
 
407
416
  // Download and extract
@@ -439,7 +448,7 @@ export async function installPackageLocally(
439
448
  "Download failed"
440
449
  );
441
450
  void updateExtmgrStatus(ctx, pi);
442
- return;
451
+ return { installed: false, reloaded: false };
443
452
  }
444
453
  const { tarballPath } = extractResult;
445
454
 
@@ -496,7 +505,7 @@ export async function installPackageLocally(
496
505
  "Extraction failed"
497
506
  );
498
507
  void updateExtmgrStatus(ctx, pi);
499
- return;
508
+ return { installed: false, reloaded: false };
500
509
  }
501
510
 
502
511
  // Copy to extensions dir
@@ -527,7 +536,7 @@ export async function installPackageLocally(
527
536
  "Failed to copy extension"
528
537
  );
529
538
  void updateExtmgrStatus(ctx, pi);
530
- return;
539
+ return { installed: false, reloaded: false };
531
540
  }
532
541
 
533
542
  clearSearchCache();
@@ -538,4 +547,24 @@ export async function installPackageLocally(
538
547
  if (!reloaded) {
539
548
  void updateExtmgrStatus(ctx, pi);
540
549
  }
550
+
551
+ return { installed: true, reloaded };
552
+ }
553
+
554
+ export async function installPackageLocally(
555
+ packageName: string,
556
+ ctx: ExtensionCommandContext,
557
+ pi: ExtensionAPI,
558
+ options?: InstallOptions
559
+ ): Promise<void> {
560
+ await installPackageLocallyInternal(packageName, ctx, pi, options);
561
+ }
562
+
563
+ export async function installPackageLocallyWithOutcome(
564
+ packageName: string,
565
+ ctx: ExtensionCommandContext,
566
+ pi: ExtensionAPI,
567
+ options?: InstallOptions
568
+ ): Promise<InstallOutcome> {
569
+ return installPackageLocallyInternal(packageName, ctx, pi, options);
541
570
  }
@@ -10,6 +10,7 @@ import {
10
10
  import { UI } from "../constants.js";
11
11
  import { type InstalledPackage } from "../types/index.js";
12
12
  import { runTaskWithLoader } from "../ui/async-task.js";
13
+ import { parseChoiceByLabel } from "../utils/command.js";
13
14
  import { formatInstalledPackageLabel } from "../utils/format.js";
14
15
  import { logPackageRemove, logPackageUpdate } from "../utils/history.js";
15
16
  import { requireUI } from "../utils/mode.js";
@@ -34,17 +35,13 @@ export interface PackageMutationOutcome {
34
35
  reloaded: boolean;
35
36
  }
36
37
 
37
- const NO_PACKAGE_MUTATION_OUTCOME: PackageMutationOutcome = {
38
- reloaded: false,
39
- };
40
-
41
38
  const BULK_UPDATE_LABEL = "all packages";
42
-
43
- function packageMutationOutcome(
44
- overrides: Partial<PackageMutationOutcome>
45
- ): PackageMutationOutcome {
46
- return { ...NO_PACKAGE_MUTATION_OUTCOME, ...overrides };
47
- }
39
+ const REMOVAL_SCOPE_CHOICES = {
40
+ both: "Both global + project",
41
+ global: "Global only",
42
+ project: "Project only",
43
+ cancel: "Cancel",
44
+ } as const;
48
45
 
49
46
  function getProgressMessage(event: ProgressEvent, fallback: string): string {
50
47
  return event.message?.trim() || fallback;
@@ -70,7 +67,7 @@ async function updatePackageInternal(
70
67
  logPackageUpdate(pi, source, source, undefined, true);
71
68
  clearUpdatesAvailable(pi, ctx, [updateIdentity]);
72
69
  void updateExtmgrStatus(ctx, pi);
73
- return NO_PACKAGE_MUTATION_OUTCOME;
70
+ return { reloaded: false };
74
71
  }
75
72
 
76
73
  await runTaskWithLoader(
@@ -94,7 +91,7 @@ async function updatePackageInternal(
94
91
  logPackageUpdate(pi, source, source, undefined, false, errorMsg);
95
92
  notifyError(ctx, errorMsg);
96
93
  void updateExtmgrStatus(ctx, pi);
97
- return NO_PACKAGE_MUTATION_OUTCOME;
94
+ return { reloaded: false };
98
95
  }
99
96
 
100
97
  logPackageUpdate(pi, source, source, undefined, true);
@@ -105,7 +102,7 @@ async function updatePackageInternal(
105
102
  if (!reloaded) {
106
103
  void updateExtmgrStatus(ctx, pi);
107
104
  }
108
- return packageMutationOutcome({ reloaded });
105
+ return { reloaded };
109
106
  }
110
107
 
111
108
  async function updatePackagesInternal(
@@ -121,7 +118,7 @@ async function updatePackagesInternal(
121
118
  logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
122
119
  clearUpdatesAvailable(pi, ctx);
123
120
  void updateExtmgrStatus(ctx, pi);
124
- return NO_PACKAGE_MUTATION_OUTCOME;
121
+ return { reloaded: false };
125
122
  }
126
123
 
127
124
  await runTaskWithLoader(
@@ -145,7 +142,7 @@ async function updatePackagesInternal(
145
142
  logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, false, errorMsg);
146
143
  notifyError(ctx, errorMsg);
147
144
  void updateExtmgrStatus(ctx, pi);
148
- return NO_PACKAGE_MUTATION_OUTCOME;
145
+ return { reloaded: false };
149
146
  }
150
147
 
151
148
  logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
@@ -156,7 +153,7 @@ async function updatePackagesInternal(
156
153
  if (!reloaded) {
157
154
  void updateExtmgrStatus(ctx, pi);
158
155
  }
159
- return packageMutationOutcome({ reloaded });
156
+ return { reloaded };
160
157
  }
161
158
 
162
159
  export async function updatePackage(
@@ -230,25 +227,15 @@ interface RemovalTarget {
230
227
  name: string;
231
228
  }
232
229
 
233
- function scopeChoiceFromLabel(choice: string | undefined): RemovalScopeChoice {
234
- if (!choice || choice === "Cancel") return "cancel";
235
- if (choice.includes("Both")) return "both";
236
- if (choice.includes("Global")) return "global";
237
- if (choice.includes("Project")) return "project";
238
- return "cancel";
239
- }
240
-
241
230
  async function selectRemovalScope(ctx: ExtensionCommandContext): Promise<RemovalScopeChoice> {
242
231
  if (!ctx.hasUI) return "global";
243
232
 
244
- const choice = await ctx.ui.select("Remove scope", [
245
- "Both global + project",
246
- "Global only",
247
- "Project only",
248
- "Cancel",
249
- ]);
250
-
251
- return scopeChoiceFromLabel(choice);
233
+ return (
234
+ parseChoiceByLabel(
235
+ REMOVAL_SCOPE_CHOICES,
236
+ await ctx.ui.select("Remove scope", Object.values(REMOVAL_SCOPE_CHOICES))
237
+ ) ?? "cancel"
238
+ );
252
239
  }
253
240
 
254
241
  function buildRemovalTargets(
@@ -374,18 +361,18 @@ async function removePackageInternal(
374
361
 
375
362
  if (scopeChoice === "cancel") {
376
363
  notify(ctx, "Removal cancelled.", "info");
377
- return NO_PACKAGE_MUTATION_OUTCOME;
364
+ return { reloaded: false };
378
365
  }
379
366
 
380
367
  if (matching.length === 0) {
381
368
  notify(ctx, `${source} is not installed.`, "info");
382
- return NO_PACKAGE_MUTATION_OUTCOME;
369
+ return { reloaded: false };
383
370
  }
384
371
 
385
372
  const targets = buildRemovalTargets(matching, ctx.hasUI, scopeChoice);
386
373
  if (targets.length === 0) {
387
374
  notify(ctx, "Nothing to remove.", "info");
388
- return NO_PACKAGE_MUTATION_OUTCOME;
375
+ return { reloaded: false };
389
376
  }
390
377
 
391
378
  const confirmed = await confirmAction(
@@ -396,7 +383,7 @@ async function removePackageInternal(
396
383
  );
397
384
  if (!confirmed) {
398
385
  notify(ctx, "Removal cancelled.", "info");
399
- return NO_PACKAGE_MUTATION_OUTCOME;
386
+ return { reloaded: false };
400
387
  }
401
388
 
402
389
  const results = await executeRemovalTargets(targets, ctx, pi);
@@ -424,7 +411,7 @@ async function removePackageInternal(
424
411
 
425
412
  if (successfulRemovalCount === 0) {
426
413
  void updateExtmgrStatus(ctx, pi);
427
- return NO_PACKAGE_MUTATION_OUTCOME;
414
+ return { reloaded: false };
428
415
  }
429
416
 
430
417
  const reloaded = await confirmReload(ctx, "Removal complete.");
@@ -432,7 +419,7 @@ async function removePackageInternal(
432
419
  void updateExtmgrStatus(ctx, pi);
433
420
  }
434
421
 
435
- return packageMutationOutcome({ reloaded });
422
+ return { reloaded };
436
423
  }
437
424
 
438
425
  export async function removePackage(
@@ -19,6 +19,7 @@ export interface NpmPackage {
19
19
  name: string;
20
20
  version?: string | undefined;
21
21
  description?: string | undefined;
22
+ author?: string | undefined;
22
23
  keywords?: string[] | undefined;
23
24
  date?: string | undefined;
24
25
  size?: number | undefined; // Package size in bytes
@@ -64,6 +65,7 @@ export interface PackageUnifiedItem {
64
65
  displayName: string;
65
66
  scope: Scope;
66
67
  source: string;
68
+ resolvedPath?: string | undefined;
67
69
  version?: string | undefined;
68
70
  description?: string | undefined;
69
71
  size?: number | undefined; // Package size in bytes
@@ -97,7 +99,7 @@ export type BrowseAction =
97
99
  | { type: "prev" }
98
100
  | { type: "next" }
99
101
  | { type: "refresh" }
102
+ | { type: "search"; query: string }
103
+ | { type: "install" }
100
104
  | { type: "menu" }
101
- | { type: "main" }
102
- | { type: "help" }
103
105
  | { type: "cancel" };
package/src/ui/footer.ts CHANGED
@@ -4,18 +4,24 @@
4
4
  import { type State, type UnifiedItem } from "../types/index.js";
5
5
 
6
6
  export interface FooterState {
7
- hasLocals: boolean;
8
- hasPackages: boolean;
7
+ selectedType?: UnifiedItem["type"];
8
+ pendingChanges: number;
9
9
  }
10
10
 
11
- /**
12
- * Build footer state from visible items.
13
- */
14
- export function buildFooterState(items: UnifiedItem[]): FooterState {
15
- return {
16
- hasLocals: items.some((i) => i.type === "local"),
17
- hasPackages: items.some((i) => i.type === "package"),
11
+ export function buildFooterState(
12
+ staged: Map<string, State>,
13
+ byId: Map<string, UnifiedItem>,
14
+ selectedItem?: UnifiedItem
15
+ ): FooterState {
16
+ const state: FooterState = {
17
+ pendingChanges: getPendingToggleChangeCount(staged, byId),
18
18
  };
19
+
20
+ if (selectedItem) {
21
+ state.selectedType = selectedItem.type;
22
+ }
23
+
24
+ return state;
19
25
  }
20
26
 
21
27
  export function getPendingToggleChangeCount(
@@ -37,27 +43,41 @@ export function getPendingToggleChangeCount(
37
43
  }
38
44
 
39
45
  /**
40
- * Build keyboard shortcuts text for the footer.
46
+ * Build contextual keyboard shortcuts text for the footer.
41
47
  */
42
48
  export function buildFooterShortcuts(state: FooterState): string {
43
49
  const parts: string[] = [];
44
- parts.push("↑↓ Navigate");
45
-
46
- if (state.hasLocals) parts.push("Space/Enter Toggle");
47
- if (state.hasLocals) parts.push("S Save");
48
- if (state.hasPackages) parts.push("Enter/A Actions");
49
- if (state.hasPackages) parts.push("c Configure");
50
- if (state.hasPackages) parts.push("u Update");
51
- if (state.hasPackages || state.hasLocals) parts.push("X Remove");
52
-
53
- parts.push("i Install");
54
- parts.push("f Search");
55
- parts.push("U Update all");
56
- parts.push("t Auto-update");
57
- parts.push("P Palette");
58
- parts.push("R Browse");
59
- parts.push("? Help");
60
- parts.push("Esc Cancel");
61
-
62
- return parts.join(" | ");
50
+
51
+ if (state.selectedType === "local") {
52
+ parts.push("Space toggle");
53
+ parts.push("Enter/A actions");
54
+ parts.push("V details");
55
+ parts.push("X remove");
56
+ }
57
+
58
+ if (state.selectedType === "package") {
59
+ parts.push("Enter/A actions");
60
+ parts.push("V details");
61
+ parts.push("c configure");
62
+ parts.push("u update");
63
+ parts.push("X remove");
64
+ }
65
+
66
+ if (state.pendingChanges > 0) {
67
+ parts.push(`S save (${state.pendingChanges})`);
68
+ }
69
+
70
+ parts.push("/ search");
71
+ parts.push("Tab filters");
72
+ parts.push("1-5 filters");
73
+ parts.push("i install");
74
+ parts.push("f remote search");
75
+ parts.push("U update all");
76
+ parts.push("t auto-update");
77
+ parts.push("P palette");
78
+ parts.push("R browse");
79
+ parts.push("? help");
80
+ parts.push("Esc clear/cancel");
81
+
82
+ return parts.join(" · ");
63
83
  }
package/src/ui/help.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * Help display
3
3
  */
4
4
  import { type ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
5
+ import { notify } from "../utils/notify.js";
5
6
 
6
7
  export function showHelp(ctx: ExtensionCommandContext): void {
7
8
  const lines = [
@@ -9,25 +10,33 @@ export function showHelp(ctx: ExtensionCommandContext): void {
9
10
  "",
10
11
  "Unified View:",
11
12
  " Local extensions and npm/git packages are displayed together",
13
+ " The list is grouped into Local extensions and Installed packages sections",
14
+ " Rows stay compact; details for the selected item appear below the list",
12
15
  " Local extensions show ● enabled / ○ disabled with G/P scope",
13
- " Packages show 📦 with name@version and G/P scope",
16
+ " Packages show a source-type icon with name@version, scope, and size when known",
14
17
  "",
15
18
  "Navigation:",
16
19
  " ↑↓ Navigate list",
17
- " Space/Enter Toggle local extension enabled/disabled",
20
+ " PageUp/Down Jump through longer lists",
21
+ " Home/End Jump to top or bottom",
22
+ " Space Toggle selected local extension enabled/disabled",
18
23
  " S Save changes to local extensions",
19
- " Enter/A Open actions for selected package",
24
+ " Enter/A Open actions for the selected item",
25
+ " / or Ctrl+F Search visible items",
26
+ " Tab/Shift+Tab Cycle filters",
27
+ " 1-5 Quick filters: All / Local / Packages / Updates / Disabled",
20
28
  " c Configure selected package extensions (reload after save)",
21
29
  " u Update selected package",
30
+ " V View full details for the selected item",
22
31
  " X Remove selected item (package or local extension)",
23
32
  " i Quick install by source",
24
- " f Quick search",
33
+ " f Remote package search",
25
34
  " U Update all packages",
26
35
  " t Auto-update wizard",
27
36
  " P/M Quick actions palette",
28
37
  " R Browse remote packages",
29
38
  " ?/H Show this help",
30
- " Esc Cancel",
39
+ " Esc Clear search or cancel",
31
40
  "",
32
41
  "Extension Sources:",
33
42
  " - ~/.pi/agent/extensions/ (global - G)",
@@ -49,10 +58,5 @@ export function showHelp(ctx: ExtensionCommandContext): void {
49
58
  " /extensions auto-update Show or change update schedule",
50
59
  ];
51
60
 
52
- const output = lines.join("\n");
53
- if (ctx.hasUI) {
54
- ctx.ui.notify(output, "info");
55
- } else {
56
- console.log(output);
57
- }
61
+ notify(ctx, lines.join("\n"), "info");
58
62
  }