magus-cli 7.6.0 → 7.8.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.
Files changed (83) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/blackbox-parity.test.ts +29 -15
  3. package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
  4. package/src/__tests__/blind-780-cli-tools.test.ts +808 -0
  5. package/src/__tests__/blind-780-deprecated.test.ts +631 -0
  6. package/src/__tests__/blind-780-failure-reasons.test.ts +1004 -0
  7. package/src/__tests__/blind-780-plugin-resolution.test.ts +431 -0
  8. package/src/__tests__/blind-780-review2.test.ts +1203 -0
  9. package/src/__tests__/cli-update-view.test.ts +3 -3
  10. package/src/__tests__/deprecated-remove.test.ts +827 -0
  11. package/src/__tests__/doctor.test.ts +159 -1
  12. package/src/__tests__/failure-popup-review2.test.tsx +920 -0
  13. package/src/__tests__/failure-popup.test.tsx +419 -0
  14. package/src/__tests__/final-fixes-780.test.tsx +724 -0
  15. package/src/__tests__/format-update-outcome.test.ts +9 -34
  16. package/src/__tests__/gitignore-profile.test.ts +0 -4
  17. package/src/__tests__/models-screen-state.test.ts +18 -40
  18. package/src/__tests__/plugin-resolution.test.ts +52 -13
  19. package/src/__tests__/plugin-row-text.test.ts +30 -37
  20. package/src/__tests__/profile-dependencies.test.ts +7 -0
  21. package/src/__tests__/profile-startup-old-format.test.ts +0 -3
  22. package/src/__tests__/profile-startup.test.ts +40 -12
  23. package/src/__tests__/stale-load-repair.test.ts +31 -23
  24. package/src/__tests__/update-apply.test.ts +13 -29
  25. package/src/__tests__/update-plan.test.ts +23 -17
  26. package/src/__tests__/useStaleLoadModal.test.ts +1 -42
  27. package/src/__tests__/version-unknown-state.test.ts +15 -17
  28. package/src/cli/doctor.ts +151 -30
  29. package/src/cli/install.ts +5 -6
  30. package/src/cli/update-view.ts +58 -15
  31. package/src/cli/update.ts +27 -5
  32. package/src/services/catalog-cache-store.ts +8 -6
  33. package/src/services/claude-cli.ts +106 -19
  34. package/src/services/claude-settings.ts +84 -36
  35. package/src/services/gitignore-service.ts +0 -19
  36. package/src/services/plugin-env-needs.ts +69 -0
  37. package/src/services/plugin-manager.ts +31 -19
  38. package/src/services/plugin-resolution.ts +34 -42
  39. package/src/services/profile-absorb.ts +9 -0
  40. package/src/services/profile-activate.ts +1 -1
  41. package/src/services/profile-startup.ts +30 -27
  42. package/src/services/profile-state.ts +81 -2
  43. package/src/services/resolver.ts +95 -1
  44. package/src/services/update-engine.ts +326 -31
  45. package/src/services/update-plan.ts +293 -33
  46. package/src/types/index.ts +7 -0
  47. package/src/ui/App.tsx +186 -123
  48. package/src/ui/adapters/cliToolsAdapter.ts +203 -1
  49. package/src/ui/adapters/failureReport.ts +137 -0
  50. package/src/ui/adapters/pluginsAdapter.ts +299 -28
  51. package/src/ui/adapters/startupReports.ts +282 -0
  52. package/src/ui/components/modals/FailureListModal.tsx +231 -0
  53. package/src/ui/components/modals/MessageModal.tsx +7 -13
  54. package/src/ui/components/modals/ModalContainer.tsx +10 -5
  55. package/src/ui/components/modals/index.ts +1 -0
  56. package/src/ui/hooks/startupQueue.ts +56 -0
  57. package/src/ui/hooks/useGitignoreModal.ts +31 -31
  58. package/src/ui/hooks/useProfileStartupModal.ts +127 -122
  59. package/src/ui/hooks/useStaleLoadModal.ts +33 -53
  60. package/src/ui/keymap.ts +14 -1
  61. package/src/ui/renderers/mcpRenderers.tsx +110 -3
  62. package/src/ui/renderers/pluginRenderers.tsx +77 -6
  63. package/src/ui/renderers/pluginRowText.ts +25 -25
  64. package/src/ui/renderers/pluginStatusText.ts +18 -1
  65. package/src/ui/screens/AliasScreen.tsx +38 -44
  66. package/src/ui/screens/CliToolsScreen.tsx +65 -51
  67. package/src/ui/screens/EnvVarsScreen.tsx +21 -6
  68. package/src/ui/screens/GitignoreScreen.tsx +49 -75
  69. package/src/ui/screens/McpRegistryScreen.tsx +72 -40
  70. package/src/ui/screens/McpScreen.tsx +55 -24
  71. package/src/ui/screens/ModelsScreen.tsx +46 -86
  72. package/src/ui/screens/PluginsScreen.tsx +316 -245
  73. package/src/ui/screens/ProfilesScreen.tsx +88 -73
  74. package/src/ui/screens/SkillsScreen.tsx +83 -80
  75. package/src/ui/screens/StylesScreen.tsx +211 -275
  76. package/src/ui/state/AppContext.tsx +72 -7
  77. package/src/ui/state/DimensionsContext.tsx +3 -22
  78. package/src/ui/state/reducer.ts +25 -14
  79. package/src/ui/state/types.ts +61 -31
  80. package/src/utils/user-path.ts +60 -0
  81. package/src/__tests__/styles-status-line.test.ts +0 -72
  82. package/src/__tests__/version-snapshot.test.ts +0 -212
  83. package/src/services/version-snapshot.ts +0 -189
@@ -17,11 +17,7 @@
17
17
 
18
18
  import { useCallback, useRef } from "react";
19
19
  import { getManifestPath } from "../../services/manifest.js";
20
- import {
21
- type ActivationResult,
22
- type ProfileChoice,
23
- activationNotes,
24
- } from "../../services/profile-activate.js";
20
+ import type { ProfileChoice } from "../../services/profile-activate.js";
25
21
  import type { DisplacedArtifact } from "../../services/profile-live.js";
26
22
  import {
27
23
  type ProfileStartupNeed,
@@ -37,8 +33,17 @@ import {
37
33
  } from "../../services/profile-startup.js";
38
34
  import { applyProfileChoice } from "../../services/profile-state.js";
39
35
  import { SettingsParseError } from "../../services/settings-file.js";
40
- import { useApp, useNavigation } from "../state/AppContext.js";
41
- import type { ModalState } from "../state/types.js";
36
+ import { exceptionReason } from "../adapters/pluginsAdapter.js";
37
+ import {
38
+ activationFailureReport,
39
+ profileProblemsReport,
40
+ stepFailureReport,
41
+ } from "../adapters/startupReports.js";
42
+ import { useApp } from "../state/AppContext.js";
43
+ import type { FailureReport, ModalState } from "../state/types.js";
44
+
45
+ /** The headline for every way "Update it now" can fail. */
46
+ const FORMAT_NOT_UPDATED = "The profile format was not updated";
42
47
 
43
48
  export interface BuildProfileModalArgs {
44
49
  need: ProfileStartupNeed;
@@ -85,40 +90,6 @@ export function buildProfileStartupModal(
85
90
  };
86
91
  }
87
92
 
88
- /** What changed, phrased for a message modal rather than a terminal. */
89
- export function describeOutcome(
90
- profileId: string,
91
- displaced: DisplacedArtifact[],
92
- projectPath: string,
93
- /** True when this happened without asking, following the committed default. */
94
- automatic = false,
95
- /** Anything else activation reported (see `activationNotes`). */
96
- notes: string[] = [],
97
- ): string {
98
- const lines = [
99
- automatic
100
- ? `This project declares "${profileId}" as its profile, so magus activated it.`
101
- : `"${profileId}" is now the active profile.`,
102
- ];
103
- if (displaced.length > 0) {
104
- lines.push(
105
- "",
106
- "These were moved aside to make room for the profile's files, and are kept:",
107
- ...displaced.map((d) => {
108
- const from = d.link.startsWith(projectPath)
109
- ? d.link.slice(projectPath.length + 1)
110
- : d.link;
111
- const to = d.savedTo.startsWith(projectPath)
112
- ? d.savedTo.slice(projectPath.length + 1)
113
- : d.savedTo;
114
- return ` ${from} → ${to}`;
115
- }),
116
- );
117
- }
118
- if (notes.length > 0) lines.push("", ...notes);
119
- return lines.join("\n");
120
- }
121
-
122
93
  /** Project-relative, so a path does not wrap over two lines. */
123
94
  function rel(p: string, projectPath: string): string {
124
95
  return p.startsWith(projectPath) ? p.slice(projectPath.length + 1) : p;
@@ -144,33 +115,59 @@ export function describeFailure(
144
115
  moved: DisplacedArtifact[],
145
116
  projectPath: string,
146
117
  declaredDefault: string | null,
147
- ): string {
148
- const reason = error instanceof Error ? error.message : String(error);
118
+ choice: ProfileChoice,
119
+ ): FailureReport {
120
+ const profile =
121
+ choice.kind === "adopt" ? (choice.name ?? choice.id) : choice.id;
122
+ const headline =
123
+ choice.kind === "adopt"
124
+ ? `Profile "${profile}" was not created`
125
+ : `Profile "${profile}" was not activated`;
126
+ const e = exceptionReason(
127
+ error instanceof Error ? error.message : String(error),
128
+ );
149
129
  if (moved.length === 0) {
150
- return `${reason}\n\nNothing was moved, so this project is exactly as it was.`;
130
+ return {
131
+ headline,
132
+ rows: [
133
+ {
134
+ subject: profile,
135
+ reason: `${e.reason}; nothing was moved, so this project is exactly as it was`,
136
+ },
137
+ ],
138
+ fix: e.fix ?? null,
139
+ };
151
140
  }
152
- return [
153
- reason,
154
- "",
155
- "This did not finish, and these were already moved aside. They are kept:",
156
- ...moved.map(
157
- (d) => ` ${rel(d.link, projectPath)} → ${rel(d.savedTo, projectPath)}`,
158
- ),
159
- "",
141
+ return {
142
+ headline,
143
+ rows: [
144
+ {
145
+ subject: profile,
146
+ reason: `${e.reason}; this did not finish, and these files were already moved aside`,
147
+ },
148
+ ...moved.map((d) => ({
149
+ subject: rel(d.link, projectPath),
150
+ reason: `is kept at ${rel(d.savedTo, projectPath)}`,
151
+ })),
152
+ ],
160
153
  // The trap the reviewer found: re-choosing "save what you have" after a
161
154
  // partial failure runs adoption again, and adoption reads settings.json —
162
155
  // which is now in the backup. It would capture an empty plugin set into a
163
156
  // second profile and call that the user's setup.
164
- declaredDefault !== null
165
- ? `Activating ${declaredDefault} again is safe. Do NOT save-what-you-have — the files above have moved, so it would capture an empty set.`
166
- : "Activating a profile again is safe. Do NOT save-what-you-have — the files above have moved, so it would capture an empty set.",
167
- ].join("\n");
157
+ fix:
158
+ declaredDefault !== null
159
+ ? `Activating ${declaredDefault} again is safe. Do NOT save-what-you-have — the files above have moved, so it would capture an empty set.`
160
+ : "Activating a profile again is safe. Do NOT save-what-you-have — the files above have moved, so it would capture an empty set.",
161
+ };
168
162
  }
169
163
 
170
164
  export interface ProfileStartupModalCallbacks {
171
165
  /** Leave magus: the modal's only non-action. */
172
166
  onQuit: () => void;
173
- /** Called once a choice has activated a profile — see App's gitignore re-check. */
167
+ /**
168
+ * Called once a choice has activated a profile, or a format update has
169
+ * rewritten it — the app dispatches PROFILE_ACTIVATED on it.
170
+ */
174
171
  onActivated?: () => void;
175
172
  /**
176
173
  * The question is ANSWERED: a profile is active, or the user chose to quit.
@@ -185,7 +182,8 @@ export interface ProfileStartupModalCallbacks {
185
182
  * question on screen NOW, which is not always the one magus opened with: "Update
186
183
  * it now" asks a new one, and a manifest that stopped parsing turns the question
187
184
  * into the unparseable one. `working` is true while an activation or a format
188
- * update is running — its result modal takes the slot when it settles.
185
+ * update is running — when it settles it empties the slot (success) or puts its
186
+ * error modal there.
189
187
  */
190
188
  interface ProfileFlow {
191
189
  need: ProfileStartupNeed;
@@ -198,7 +196,6 @@ export function useProfileStartupModal({
198
196
  onAnswered,
199
197
  }: ProfileStartupModalCallbacks) {
200
198
  const { state, dispatch } = useApp();
201
- const { navigateToScreen } = useNavigation();
202
199
  const projectPath = state.projectPath;
203
200
  // A ref, not state: `show` is recreated on every render, and the flow must
204
201
  // survive that — a re-show has to know what was last asked, and that work is
@@ -227,6 +224,26 @@ export function useProfileStartupModal({
227
224
  answered();
228
225
  onQuit();
229
226
  };
227
+ /**
228
+ * End a flow that activated a profile: empty the slot, or — when the
229
+ * activation could not do something — put the failure popup in it.
230
+ * Either way the question is already answered, so dismissing the popup
231
+ * lets the next startup question in.
232
+ */
233
+ const settle = (report: FailureReport | null): void => {
234
+ if (!report) {
235
+ dispatch({ type: "HIDE_MODAL" });
236
+ return;
237
+ }
238
+ dispatch({
239
+ type: "SHOW_MODAL",
240
+ modal: {
241
+ type: "failures",
242
+ report,
243
+ onDismiss: () => dispatch({ type: "HIDE_MODAL" }),
244
+ },
245
+ });
246
+ };
230
247
 
231
248
  /**
232
249
  * Apply a choice, reporting the outcome either way. `current` is the
@@ -235,7 +252,6 @@ export function useProfileStartupModal({
235
252
  */
236
253
  const apply = (
237
254
  choice: ProfileChoice,
238
- automatic: boolean,
239
255
  current: ProfileStartupNeed = need,
240
256
  ): void => {
241
257
  flowRef.current = { need: current, working: true };
@@ -263,34 +279,17 @@ export function useProfileStartupModal({
263
279
  void applyProfileChoice(choice, current.manifest, projectPath, {
264
280
  onDisplace: (artifact) => moved.push(artifact),
265
281
  })
266
- .then((result: ActivationResult) => {
282
+ .then((result) => {
267
283
  // Answered here, on success — NOT when the question was shown.
268
284
  // Clearing it on show let a screen's modal replace it and the
269
285
  // question never came back.
270
286
  answered();
271
287
  onActivated?.();
272
- const { profileId, displaced } = result;
273
- dispatch({
274
- type: "SHOW_MODAL",
275
- modal: {
276
- type: "message",
277
- title: "Profile active",
278
- message: describeOutcome(
279
- profileId,
280
- displaced,
281
- projectPath,
282
- automatic,
283
- activationNotes(result),
284
- ),
285
- variant: "success",
286
- onDismiss: () => {
287
- dispatch({ type: "HIDE_MODAL" });
288
- // The Profiles tab is where the result is visible, and
289
- // where the user will want to look next.
290
- navigateToScreen("profiles");
291
- },
292
- },
293
- });
288
+ // No result modal and no jump to the Profiles tab: the user
289
+ // stays where they are, and the screen reloads under them —
290
+ // unless the activation could not do something, which the one
291
+ // failure popup says once it has settled.
292
+ settle(activationFailureReport([result]));
294
293
  })
295
294
  .catch((error: unknown) => {
296
295
  // The manifest stopped parsing between launch and this answer —
@@ -308,18 +307,14 @@ export function useProfileStartupModal({
308
307
  dispatch({
309
308
  type: "SHOW_MODAL",
310
309
  modal: {
311
- type: "message",
312
- title:
313
- choice.kind === "adopt"
314
- ? "Could not create the profile"
315
- : "Could not activate the profile",
316
- message: describeFailure(
310
+ type: "failures",
311
+ report: describeFailure(
317
312
  error,
318
313
  moved,
319
314
  projectPath,
320
315
  current.declaredDefault,
316
+ choice,
321
317
  ),
322
- variant: "error",
323
318
  // A failure leaves the project without an active profile, so
324
319
  // dismissing returns to the question — including when the
325
320
  // attempt was automatic, which is the one case where there
@@ -344,24 +339,47 @@ export function useProfileStartupModal({
344
339
  void prepareProfileStartup(projectPath, { convert: true })
345
340
  .then((result) => {
346
341
  const outcome = formatUpdateOutcome(result);
342
+ const report = activationFailureReport(result.activations ?? []);
347
343
  if (outcome.kind === "ask") {
348
344
  // Converted, but no profile is active yet: a new question,
349
- // still unanswered.
345
+ // still unanswered — asked once what the conversion could not
346
+ // do has been read.
350
347
  onActivated?.();
348
+ if (report) {
349
+ flowRef.current = { need: outcome.need, working: false };
350
+ dispatch({
351
+ type: "SHOW_MODAL",
352
+ modal: {
353
+ type: "failures",
354
+ report,
355
+ onDismiss: () => ask(outcome.need),
356
+ },
357
+ });
358
+ return;
359
+ }
351
360
  ask(outcome.need);
352
361
  return;
353
362
  }
354
363
  if (outcome.kind === "failed") {
355
364
  // Refused, so still the old format: back to the question,
356
365
  // as the CLI stops on the same lines.
366
+ // Each refusal in the failure popup, the file it is about
367
+ // named, and what to do about it.
368
+ // A "failed" outcome always carries problems; the fallback
369
+ // only keeps the popup's type honest.
357
370
  flowRef.current = { need, working: false };
371
+ const refused =
372
+ profileProblemsReport(result.problems, FORMAT_NOT_UPDATED) ??
373
+ stepFailureReport(
374
+ FORMAT_NOT_UPDATED,
375
+ "profile",
376
+ outcome.message,
377
+ );
358
378
  dispatch({
359
379
  type: "SHOW_MODAL",
360
380
  modal: {
361
- type: "message",
362
- title: outcome.title,
363
- message: outcome.message,
364
- variant: "error",
381
+ type: "failures",
382
+ report: refused,
365
383
  onDismiss: () => ask(need),
366
384
  },
367
385
  });
@@ -370,29 +388,20 @@ export function useProfileStartupModal({
370
388
  // Nothing left to ask: the converted profile is active.
371
389
  answered();
372
390
  onActivated?.();
373
- dispatch({
374
- type: "SHOW_MODAL",
375
- modal: {
376
- type: "message",
377
- title: outcome.title,
378
- message: outcome.message,
379
- variant: "success",
380
- onDismiss: () => {
381
- dispatch({ type: "HIDE_MODAL" });
382
- navigateToScreen("profiles");
383
- },
384
- },
385
- });
391
+ settle(report);
386
392
  })
387
393
  .catch((error: unknown) => {
388
394
  flowRef.current = { need, working: false };
389
395
  dispatch({
390
396
  type: "SHOW_MODAL",
391
397
  modal: {
392
- type: "message",
393
- title: "Could not update the profile format",
394
- message: `${error instanceof Error ? error.message : String(error)}\n\nYour profiles are kept. Open magus again to retry.`,
395
- variant: "error",
398
+ type: "failures",
399
+ report: stepFailureReport(
400
+ FORMAT_NOT_UPDATED,
401
+ "profile",
402
+ error,
403
+ "Your profiles are kept. Open magus again to retry",
404
+ ),
396
405
  onDismiss: () => ask(need),
397
406
  },
398
407
  });
@@ -427,11 +436,7 @@ export function useProfileStartupModal({
427
436
  askName(suggestion, current, value, check.reason);
428
437
  return;
429
438
  }
430
- apply(
431
- { kind: "adopt", id: check.id, name: check.name },
432
- false,
433
- current,
434
- );
439
+ apply({ kind: "adopt", id: check.id, name: check.name }, current);
435
440
  },
436
441
  onCancel: () => ask(current),
437
442
  },
@@ -447,7 +452,7 @@ export function useProfileStartupModal({
447
452
  onChoose: (choice) =>
448
453
  choice.kind === "adopt"
449
454
  ? askName(choice.id, current)
450
- : apply(choice, false, current),
455
+ : apply(choice, current),
451
456
  onUpdateFormat: updateFormat,
452
457
  onQuit: quit,
453
458
  }),
@@ -466,15 +471,15 @@ export function useProfileStartupModal({
466
471
 
467
472
  // The repo already answered and there is nothing of the user's to
468
473
  // replace: asking here would ask someone to re-decide what their team
469
- // decided, with nothing at stake. Reported in the outcome modal, so it
470
- // is never invisible.
474
+ // decided, with nothing at stake. The Profiles tab names the profile
475
+ // in use.
471
476
  if (need.autoActivate && need.declaredDefault !== null) {
472
- apply({ kind: "activate", id: need.declaredDefault }, true);
477
+ apply({ kind: "activate", id: need.declaredDefault });
473
478
  return;
474
479
  }
475
480
  ask();
476
481
  },
477
- [dispatch, navigateToScreen, projectPath, onQuit, onActivated, onAnswered],
482
+ [dispatch, projectPath, onQuit, onActivated, onAnswered],
478
483
  );
479
484
 
480
485
  return { show };
@@ -1,6 +1,7 @@
1
1
  import { useCallback } from "react";
2
+ import { stepFailureReport } from "../adapters/startupReports.js";
2
3
  import { useApp, useNavigation } from "../state/AppContext.js";
3
- import type { ModalState } from "../state/types.js";
4
+ import type { FailureReport, ModalState } from "../state/types.js";
4
5
 
5
6
  /**
6
7
  * Startup broken-plugin modal: magus launches, finds that Claude Code will load
@@ -75,8 +76,16 @@ export function useStaleLoadModal() {
75
76
  const show = useCallback(
76
77
  (args: {
77
78
  brokenCount: number;
78
- /** Runs the repair. Resolves to what to tell the user afterwards. */
79
- onFix: () => Promise<string> | string;
79
+ /**
80
+ * Runs the repair and answers what it could not fix — null when every
81
+ * plugin now loads its installed version. The Plugins screen shows
82
+ * what now loads; the failures go into the one failure popup.
83
+ */
84
+ onFix: () =>
85
+ | Promise<FailureReport | null | undefined>
86
+ | FailureReport
87
+ | null
88
+ | undefined;
80
89
  }) => {
81
90
  if (args.brokenCount === 0) return;
82
91
  const dismiss = () => dispatch({ type: "HIDE_MODAL" });
@@ -87,38 +96,34 @@ export function useStaleLoadModal() {
87
96
  type: "SHOW_MODAL",
88
97
  modal: { type: "loading", message: "Repairing plugins…" },
89
98
  });
90
- // The outcome is REPORTED, never assumed. `repairStaleLoad` re-asks
91
- // the loader after writing and can legitimately answer "still not
92
- // loading the target"; swallowing that would leave the user believing
93
- // a broken plugin was fixed.
99
+ // The outcome is never assumed: `repairStaleLoad` re-asks the loader
100
+ // after writing, and a plugin it could not fix still reads BROKEN on
101
+ // the Plugins screen.
94
102
  //
95
103
  // Called inside `.then` so a synchronous throw reaches `.catch`
96
104
  // too; otherwise the loading modal stays up and swallows every key.
105
+ const fail = (report: FailureReport) =>
106
+ dispatch({
107
+ type: "SHOW_MODAL",
108
+ modal: { type: "failures", report, onDismiss: dismiss },
109
+ });
97
110
  void Promise.resolve()
98
111
  .then(() => args.onFix())
99
- .then((summary) => {
100
- dispatch({
101
- type: "SHOW_MODAL",
102
- modal: {
103
- type: "message",
104
- title: "Plugin repair",
105
- message: summary,
106
- variant: "info",
107
- onDismiss: dismiss,
108
- },
109
- });
112
+ .then((report) => {
113
+ // The loading modal must go — it swallows every key — and the
114
+ // next startup question is waiting for the slot. A repair
115
+ // that did not take puts the failure popup there instead.
116
+ if (report) fail(report);
117
+ else dispatch({ type: "HIDE_MODAL" });
110
118
  })
111
119
  .catch((err) => {
112
- dispatch({
113
- type: "SHOW_MODAL",
114
- modal: {
115
- type: "message",
116
- title: "Plugin repair failed",
117
- message: err instanceof Error ? err.message : String(err),
118
- variant: "error",
119
- onDismiss: dismiss,
120
- },
121
- });
120
+ fail(
121
+ stepFailureReport(
122
+ "The plugins were not repaired",
123
+ "repair",
124
+ err,
125
+ ),
126
+ );
122
127
  });
123
128
  },
124
129
  onOpenTab: () => {
@@ -134,28 +139,3 @@ export function useStaleLoadModal() {
134
139
 
135
140
  return { show };
136
141
  }
137
-
138
- /**
139
- * One line per repair attempt, for the modal that follows the fix.
140
- *
141
- * Exported and pure so the wording is tested rather than inspected by eye. Each
142
- * line says what the loader resolves NOW, because that — not the row count — is
143
- * the thing the user came here to change.
144
- */
145
- export function staleLoadRepairSummary(
146
- results: ReadonlyArray<{
147
- pluginId: string;
148
- target: string;
149
- ok: boolean;
150
- error?: string;
151
- }>,
152
- ): string {
153
- if (results.length === 0) return "Nothing to repair.";
154
- return results
155
- .map((r) =>
156
- r.ok
157
- ? `✓ ${r.pluginId} now loads v${r.target}`
158
- : `✗ ${r.pluginId} — ${r.error ?? "repair did not take effect"}`,
159
- )
160
- .join("\n");
161
- }
package/src/ui/keymap.ts CHANGED
@@ -166,6 +166,13 @@ const PLUGINS = [
166
166
  "Remove a deprecated plugin from every scope",
167
167
  30,
168
168
  ),
169
+ bind(
170
+ "set-env",
171
+ ["e"],
172
+ "settings",
173
+ "Set the variables a plugin's MCP server needs",
174
+ 35,
175
+ ),
169
176
  bind("add-marketplace", ["n"], "add marketplace", "Add a marketplace", 30),
170
177
  bind("team-help", ["t"], "team", "How to share marketplaces with a team", 10),
171
178
  refresh("Refresh the local marketplace caches"),
@@ -374,7 +381,13 @@ const ALIAS = [
374
381
  "Turn the flag on or off; in the flag editor, the row",
375
382
  90,
376
383
  ),
377
- bind("write", ["w"], "write", "Write the alias to your shell's rc file", 85),
384
+ bind(
385
+ "write",
386
+ ["w"],
387
+ "write",
388
+ "Write the alias to your shell's rc file; a new shell picks it up",
389
+ 85,
390
+ ),
378
391
  bind("edit", ["e"], "edit", "Edit the flag's value", 80),
379
392
  bind("reset", ["r"], "reset", "Reset the row to its default", 50),
380
393
  bind("add", ["a"], "add", "Flag editor: add a value", 80),