pi-extmgr 0.1.27 → 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,9 +1,9 @@
1
- import type {
2
- ExtensionCommandContext,
3
- ExtensionContext,
4
- Theme,
1
+ import {
2
+ DynamicBorder,
3
+ type ExtensionCommandContext,
4
+ type ExtensionContext,
5
+ type Theme,
5
6
  } from "@mariozechner/pi-coding-agent";
6
- import { DynamicBorder } from "@mariozechner/pi-coding-agent";
7
7
  import { CancellableLoader, Container, Loader, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
8
8
  import { hasCustomUI } from "../utils/mode.js";
9
9
 
@@ -12,6 +12,8 @@ type AnyContext = ExtensionCommandContext | ExtensionContext;
12
12
  const TASK_ABORTED = Symbol("task-aborted");
13
13
  const TASK_FAILED = Symbol("task-failed");
14
14
 
15
+ type TaskSuccess<T> = { type: "ok"; value: T };
16
+
15
17
  export interface TaskControls {
16
18
  signal: AbortSignal;
17
19
  setMessage: (message: string) => void;
@@ -21,6 +23,7 @@ interface LoaderConfig {
21
23
  title: string;
22
24
  message: string;
23
25
  cancellable?: boolean;
26
+ fallbackWithoutLoader?: boolean;
24
27
  }
25
28
 
26
29
  function createLoaderComponent(
@@ -69,82 +72,115 @@ function createLoaderComponent(
69
72
  return { container, loader, signal };
70
73
  }
71
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
+
72
84
  export async function runTaskWithLoader<T>(
73
85
  ctx: AnyContext,
74
86
  config: LoaderConfig,
75
87
  task: (controls: TaskControls) => Promise<T>
76
88
  ): Promise<T | undefined> {
77
89
  if (!hasCustomUI(ctx)) {
78
- return task({
79
- signal: new AbortController().signal,
80
- setMessage: () => undefined,
81
- });
90
+ return runTaskWithoutLoader(task);
82
91
  }
83
92
 
84
93
  let taskError: unknown;
85
-
86
- const result = await ctx.ui.custom<T | typeof TASK_ABORTED | typeof TASK_FAILED>(
87
- (tui, theme, _keybindings, done) => {
88
- let finished = false;
89
- const finish = (value: T | typeof TASK_ABORTED | typeof TASK_FAILED): void => {
90
- if (finished) {
91
- return;
92
- }
93
- finished = true;
94
- done(value);
95
- };
96
-
97
- const { container, loader, signal } = createLoaderComponent(
98
- tui,
99
- theme,
100
- config.title,
101
- config.message,
102
- config.cancellable ?? true,
103
- () => finish(TASK_ABORTED)
104
- );
105
-
106
- void task({
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({
107
131
  signal,
108
132
  setMessage: (message) => {
109
133
  loader.setMessage(message);
110
134
  tui.requestRender();
111
135
  },
112
136
  })
113
- .then((value) => finish(value))
114
- .catch((error) => {
115
- if (signal.aborted) {
116
- finish(TASK_ABORTED);
117
- return;
118
- }
119
-
120
- taskError = error;
121
- finish(TASK_FAILED);
122
- });
123
-
124
- return {
125
- render(width: number) {
126
- return container.render(width);
127
- },
128
- invalidate() {
129
- container.invalidate();
130
- },
131
- handleInput(data: string) {
132
- if (loader instanceof CancellableLoader) {
133
- loader.handleInput(data);
134
- tui.requestRender();
135
- }
136
- },
137
- dispose() {
138
- if (loader instanceof CancellableLoader) {
139
- loader.dispose();
140
- return;
141
- }
137
+ );
142
138
 
143
- loader.stop();
144
- },
145
- };
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?.());
146
178
  }
147
- );
179
+ if (config.fallbackWithoutLoader) {
180
+ return runTaskWithoutLoader(task);
181
+ }
182
+ return undefined;
183
+ }
148
184
 
149
185
  if (result === TASK_ABORTED) {
150
186
  return undefined;
@@ -154,5 +190,5 @@ export async function runTaskWithLoader<T>(
154
190
  throw taskError;
155
191
  }
156
192
 
157
- return result;
193
+ return result.value;
158
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",
@@ -1,31 +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";
24
- import { runTaskWithLoader } from "./async-task.js";
30
+ import { notify } from "../utils/notify.js";
25
31
  import { getPackageSourceKind } from "../utils/package-source.js";
26
32
  import { getSettingsListSelectedIndex } from "../utils/settings-list.js";
27
- import { fileExists } from "../utils/fs.js";
28
- import { UI } from "../constants.js";
33
+ import { confirmReload } from "../utils/ui-helpers.js";
34
+ import { runTaskWithLoader } from "./async-task.js";
29
35
  import { getChangeMarker, getPackageIcon, getScopeIcon, getStatusIcon } from "./theme.js";
30
36
 
31
37
  export interface PackageConfigRow {
@@ -168,10 +174,14 @@ async function showConfigurePanel(
168
174
  getSettingsListTheme(),
169
175
  (id: string, newValue: string) => {
170
176
  const row = rowById.get(id);
171
- if (!row || !row.available) return;
177
+ if (!row?.available) return;
172
178
 
173
179
  const state = newValue as State;
174
- staged.set(id, state);
180
+ if (state === row.originalState) {
181
+ staged.delete(id);
182
+ } else {
183
+ staged.set(id, state);
184
+ }
175
185
 
176
186
  const settingsItem = settingsItems.find((item) => item.id === id);
177
187
  if (settingsItem) {
@@ -286,35 +296,6 @@ export async function applyPackageExtensionChanges(
286
296
  return { changed: changedRows.length, errors };
287
297
  }
288
298
 
289
- async function promptRestartForPackageConfig(ctx: ExtensionCommandContext): Promise<boolean> {
290
- if (!ctx.hasUI) {
291
- notify(
292
- ctx,
293
- "Restart pi to apply package extension configuration changes. /reload may not be enough.",
294
- "warning"
295
- );
296
- return false;
297
- }
298
-
299
- const restartNow = await ctx.ui.confirm(
300
- "Restart Required",
301
- "Package extension configuration changed.\nA full pi restart is required to apply it.\nExit pi now?"
302
- );
303
-
304
- if (!restartNow) {
305
- notify(
306
- ctx,
307
- "Restart pi manually to apply package extension configuration changes. /reload may not be enough.",
308
- "warning"
309
- );
310
- return false;
311
- }
312
-
313
- notify(ctx, "Shutting down pi. Start it again to apply changes.", "info");
314
- ctx.shutdown();
315
- return true;
316
- }
317
-
318
299
  export async function configurePackageExtensions(
319
300
  pkg: InstalledPackage,
320
301
  ctx: ExtensionCommandContext,
@@ -393,24 +374,31 @@ export async function configurePackageExtensions(
393
374
 
394
375
  const apply = await applyPackageExtensionChanges(rows, staged, pkg, ctx.cwd, pi);
395
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
+
396
391
  if (apply.errors.length > 0) {
397
392
  notify(
398
393
  ctx,
399
394
  `Applied ${apply.changed} change(s), ${apply.errors.length} failed.\n${apply.errors.join("\n")}`,
400
395
  "warning"
401
396
  );
402
- } else if (apply.changed === 0) {
403
- notify(ctx, "No changes to apply.", "info");
404
- return { changed: 0, reloaded: false };
405
397
  } else {
406
398
  notify(ctx, `Applied ${apply.changed} package extension change(s).`, "info");
407
399
  }
408
400
 
409
- if (apply.changed === 0) {
410
- return { changed: 0, reloaded: false };
411
- }
412
-
413
- const restarted = await promptRestartForPackageConfig(ctx);
414
- return { changed: apply.changed, reloaded: restarted };
401
+ const reloaded = await confirmReload(ctx, "Package extension configuration changed.");
402
+ return { changed: apply.changed, reloaded };
415
403
  }
416
404
  }
package/src/ui/remote.ts CHANGED
@@ -1,23 +1,26 @@
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";
21
24
  import { runTaskWithLoader } from "./async-task.js";
22
25
 
23
26
  interface PackageInfoCacheEntry {
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
  /**