magus-cli 7.7.0 → 7.9.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.
- package/README.md +1 -1
- package/package.json +4 -4
- package/src/__tests__/blackbox-parity.test.ts +45 -26
- package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
- package/src/__tests__/blind/plugin-deps/doctor.test.ts +622 -0
- package/src/__tests__/blind/plugin-deps/helpers.ts +340 -0
- package/src/__tests__/blind/plugin-deps/hook.test.ts +328 -0
- package/src/__tests__/blind/plugin-deps/probe.test.ts +694 -0
- package/src/__tests__/blind/plugin-deps/read-only.test.ts +96 -0
- package/src/__tests__/blind/plugin-deps/schema.test.ts +390 -0
- package/src/__tests__/blind-780-cli-tools.test.ts +796 -0
- package/src/__tests__/blind-780-deprecated.test.ts +631 -0
- package/src/__tests__/blind-780-failure-reasons.test.ts +1004 -0
- package/src/__tests__/blind-780-plugin-resolution.test.ts +431 -0
- package/src/__tests__/blind-780-review2.test.ts +1178 -0
- package/src/__tests__/cli-apply-seams.test.ts +2 -67
- package/src/__tests__/cli-tool-commands.test.ts +131 -6
- package/src/__tests__/cli-tools-adapter.test.ts +52 -2
- package/src/__tests__/cli-tools-plugin-managed.test.tsx +83 -0
- package/src/__tests__/cli-update-view.test.ts +3 -3
- package/src/__tests__/deprecated-remove.test.ts +827 -0
- package/src/__tests__/deps-adapter.test.ts +191 -0
- package/src/__tests__/deps-check-hook.test.ts +220 -0
- package/src/__tests__/deps-plan.test.ts +896 -0
- package/src/__tests__/deps-report.test.ts +197 -0
- package/src/__tests__/doctor-bins.test.ts +134 -36
- package/src/__tests__/doctor-json.test.ts +261 -0
- package/src/__tests__/doctor.test.ts +159 -1
- package/src/__tests__/failure-popup-review2.test.tsx +896 -0
- package/src/__tests__/failure-popup.test.tsx +419 -0
- package/src/__tests__/final-fixes-780.test.tsx +740 -0
- package/src/__tests__/fixtures/bins.ts +55 -0
- package/src/__tests__/format-update-outcome.test.ts +6 -32
- package/src/__tests__/gitignore-profile.test.ts +1 -9
- package/src/__tests__/install-plan.test.ts +15 -1
- package/src/__tests__/marketplace-refresh.test.ts +9 -3
- package/src/__tests__/models-screen-state.test.ts +18 -40
- package/src/__tests__/no-shell-spawn.test.ts +77 -0
- package/src/__tests__/npm-package-name.test.ts +6 -9
- package/src/__tests__/plugin-requires.test.ts +138 -153
- package/src/__tests__/plugin-resolution.test.ts +52 -13
- package/src/__tests__/plugin-row-text.test.ts +30 -37
- package/src/__tests__/profile-dependencies.test.ts +7 -0
- package/src/__tests__/profile-startup-old-format.test.ts +0 -3
- package/src/__tests__/profile-startup.test.ts +40 -12
- package/src/__tests__/requires-probe.test.ts +858 -0
- package/src/__tests__/resolver.test.ts +129 -52
- package/src/__tests__/run.test.ts +108 -0
- package/src/__tests__/stale-load-repair.test.ts +31 -23
- package/src/__tests__/tool-probe.test.ts +1 -1
- package/src/__tests__/toolchain.test.ts +123 -52
- package/src/__tests__/update-apply.test.ts +13 -29
- package/src/__tests__/update-plan.test.ts +24 -18
- package/src/__tests__/useStaleLoadModal.test.ts +1 -57
- package/src/__tests__/version-unknown-state.test.ts +15 -17
- package/src/cli/ansi.ts +2 -7
- package/src/cli/deps-view.ts +213 -0
- package/src/cli/doctor.ts +320 -54
- package/src/cli/install.ts +55 -71
- package/src/cli/profile.ts +20 -2
- package/src/cli/router.ts +6 -2
- package/src/cli/update-view.ts +58 -15
- package/src/cli/update.ts +55 -12
- package/src/data/cli-tools.ts +24 -9
- package/src/services/active-closure.ts +3 -3
- package/src/services/catalog-cache-store.ts +8 -6
- package/src/services/claude-cli.ts +106 -19
- package/src/services/claude-settings.ts +88 -384
- package/src/services/cli-tool-commands.ts +104 -3
- package/src/services/deps-plan.ts +962 -0
- package/src/services/deps-report.ts +265 -0
- package/src/services/doctor-bins.ts +115 -43
- package/src/services/install-plan.ts +11 -1
- package/src/services/local-marketplace.ts +12 -206
- package/src/services/marketplace-refresh.ts +4 -4
- package/src/services/plugin-env-needs.ts +69 -0
- package/src/services/plugin-manager.ts +35 -37
- package/src/services/plugin-requires.ts +93 -162
- package/src/services/plugin-resolution.ts +34 -42
- package/src/services/profile-activate.ts +1 -2
- package/src/services/profile-startup.ts +18 -43
- package/src/services/profile-state.ts +72 -2
- package/src/services/resolver.ts +263 -59
- package/src/services/tool-probe.ts +18 -25
- package/src/services/toolchain.ts +117 -111
- package/src/services/update-engine.ts +345 -45
- package/src/services/update-plan.ts +296 -36
- package/src/shared/plugin/hooks/bun-missing.sh +39 -0
- package/src/shared/plugin/hooks/deps-check.ts +224 -0
- package/src/shared/plugin/hooks/lib/requires-probe.ts +1183 -0
- package/src/types/index.ts +60 -16
- package/src/ui/App.tsx +81 -171
- package/src/ui/adapters/cliToolsAdapter.ts +252 -12
- package/src/ui/adapters/depsAdapter.ts +129 -0
- package/src/ui/adapters/failureReport.ts +137 -0
- package/src/ui/adapters/pluginsAdapter.ts +297 -45
- package/src/ui/adapters/startupReports.ts +260 -0
- package/src/ui/components/modals/FailureListModal.tsx +231 -0
- package/src/ui/components/modals/MessageModal.tsx +7 -12
- package/src/ui/components/modals/ModalContainer.tsx +10 -5
- package/src/ui/components/modals/index.ts +1 -0
- package/src/ui/hooks/startupQueue.ts +56 -0
- package/src/ui/hooks/useGitignoreModal.ts +30 -52
- package/src/ui/hooks/useProfileStartupModal.ts +119 -142
- package/src/ui/hooks/useStaleLoadModal.ts +35 -65
- package/src/ui/keymap.ts +14 -1
- package/src/ui/renderers/cliToolRenderers.tsx +135 -2
- package/src/ui/renderers/mcpRenderers.tsx +110 -3
- package/src/ui/renderers/pluginRenderers.tsx +77 -6
- package/src/ui/renderers/pluginRowText.ts +25 -25
- package/src/ui/renderers/pluginStatusText.ts +18 -1
- package/src/ui/screens/AliasScreen.tsx +38 -45
- package/src/ui/screens/CliToolsScreen.tsx +199 -98
- package/src/ui/screens/EnvVarsScreen.tsx +17 -5
- package/src/ui/screens/GitignoreScreen.tsx +48 -67
- package/src/ui/screens/McpRegistryScreen.tsx +67 -38
- package/src/ui/screens/McpScreen.tsx +46 -19
- package/src/ui/screens/ModelsScreen.tsx +46 -86
- package/src/ui/screens/PluginsScreen.tsx +315 -259
- package/src/ui/screens/ProfilesScreen.tsx +79 -40
- package/src/ui/screens/SkillsScreen.tsx +83 -80
- package/src/ui/screens/StylesScreen.tsx +211 -275
- package/src/ui/state/AppContext.tsx +66 -44
- package/src/ui/state/DimensionsContext.tsx +3 -22
- package/src/ui/state/reducer.ts +12 -100
- package/src/ui/state/types.ts +48 -69
- package/src/utils/command-utils.ts +4 -5
- package/src/utils/run.ts +123 -52
- package/src/utils/string-utils.ts +9 -0
- package/src/utils/user-path.ts +60 -0
- package/src/__tests__/go-module.test.ts +0 -43
- package/src/__tests__/notices-rules.test.ts +0 -476
- package/src/__tests__/notices.test.tsx +0 -604
- package/src/__tests__/plugin-setup.test.ts +0 -1016
- package/src/__tests__/shell-script-callers.test.ts +0 -92
- package/src/__tests__/styles-status-line.test.ts +0 -72
- package/src/__tests__/version-snapshot.test.ts +0 -212
- package/src/services/plugin-setup.ts +0 -965
- package/src/services/version-snapshot.ts +0 -189
- package/src/utils/go-module.ts +0 -36
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type SettingsBrowserItem,
|
|
10
10
|
buildSettingsBrowserItems,
|
|
11
11
|
} from "../adapters/settingsAdapter.js";
|
|
12
|
+
import { stepFailureReport } from "../adapters/startupReports.js";
|
|
12
13
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
13
14
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
14
15
|
import { ErrorNotice } from "../components/primitives/index.js";
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
renderSettingDetail,
|
|
19
20
|
renderSettingRow,
|
|
20
21
|
} from "../renderers/settingsRenderers.js";
|
|
21
|
-
import { useApp, useModal } from "../state/AppContext.js";
|
|
22
|
+
import { useApp, useFailurePopup, useModal } from "../state/AppContext.js";
|
|
22
23
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
23
24
|
import { theme } from "../theme.js";
|
|
24
25
|
|
|
@@ -26,8 +27,19 @@ export function SettingsScreen() {
|
|
|
26
27
|
const { state, dispatch } = useApp();
|
|
27
28
|
const { settings } = state;
|
|
28
29
|
const modal = useModal();
|
|
30
|
+
const showFailures = useFailurePopup();
|
|
29
31
|
const dimensions = useDimensions();
|
|
30
32
|
|
|
33
|
+
/** A setting that did not save, at the scope it was being saved to. */
|
|
34
|
+
const reportNotSaved = (
|
|
35
|
+
name: string,
|
|
36
|
+
scope: "user" | "project",
|
|
37
|
+
error: unknown,
|
|
38
|
+
): Promise<void> =>
|
|
39
|
+
showFailures(
|
|
40
|
+
stepFailureReport(`${name} was not saved`, scope.toUpperCase(), error),
|
|
41
|
+
);
|
|
42
|
+
|
|
31
43
|
const fetchData = useCallback(async () => {
|
|
32
44
|
dispatch({ type: "SETTINGS_DATA_LOADING" });
|
|
33
45
|
try {
|
|
@@ -84,7 +96,7 @@ export function SettingsScreen() {
|
|
|
84
96
|
await writeSettingValue(setting, newValue, scope, state.projectPath);
|
|
85
97
|
await fetchData();
|
|
86
98
|
} catch (error) {
|
|
87
|
-
await
|
|
99
|
+
await reportNotSaved(setting.name, scope, error);
|
|
88
100
|
}
|
|
89
101
|
} else if (setting.type === "select" && setting.options) {
|
|
90
102
|
const options = setting.options.map((o) => ({
|
|
@@ -110,7 +122,7 @@ export function SettingsScreen() {
|
|
|
110
122
|
await writeSettingValue(setting, val, scope, state.projectPath);
|
|
111
123
|
await fetchData();
|
|
112
124
|
} catch (error) {
|
|
113
|
-
await
|
|
125
|
+
await reportNotSaved(setting.name, scope, error);
|
|
114
126
|
}
|
|
115
127
|
} else {
|
|
116
128
|
// String type: if already set, clear it; if unset, show input modal
|
|
@@ -119,7 +131,7 @@ export function SettingsScreen() {
|
|
|
119
131
|
await writeSettingValue(setting, undefined, scope, state.projectPath);
|
|
120
132
|
await fetchData();
|
|
121
133
|
} catch (error) {
|
|
122
|
-
await
|
|
134
|
+
await reportNotSaved(setting.name, scope, error);
|
|
123
135
|
}
|
|
124
136
|
} else {
|
|
125
137
|
const newValue = await modal.input(
|
|
@@ -137,7 +149,7 @@ export function SettingsScreen() {
|
|
|
137
149
|
);
|
|
138
150
|
await fetchData();
|
|
139
151
|
} catch (error) {
|
|
140
|
-
await
|
|
152
|
+
await reportNotSaved(setting.name, scope, error);
|
|
141
153
|
}
|
|
142
154
|
}
|
|
143
155
|
}
|
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
loadGitignoreState,
|
|
13
13
|
} from "../../services/gitignore-service.js";
|
|
14
14
|
import type { ResolvedRule, Tier, Violation } from "../../types/gitignore.js";
|
|
15
|
+
import {
|
|
16
|
+
gitignoreFixReport,
|
|
17
|
+
gitignoreTemplateReport,
|
|
18
|
+
stepFailureReport,
|
|
19
|
+
} from "../adapters/startupReports.js";
|
|
15
20
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
16
21
|
import { useScreenKeys } from "../input/KeyRouter.js";
|
|
17
22
|
import { moveDirection } from "../keymap.js";
|
|
@@ -19,7 +24,8 @@ import {
|
|
|
19
24
|
type RuleRow,
|
|
20
25
|
renderRuleRow,
|
|
21
26
|
} from "../renderers/gitignoreRenderers.js";
|
|
22
|
-
import { useApp, useModal
|
|
27
|
+
import { useApp, useModal } from "../state/AppContext.js";
|
|
28
|
+
import type { FailureReport } from "../state/types.js";
|
|
23
29
|
import { theme } from "../theme.js";
|
|
24
30
|
|
|
25
31
|
// Clean rows are grouped by the manifest that defined them. Order = display
|
|
@@ -36,7 +42,6 @@ const SOURCE_LABELS: Record<Tier, string> = {
|
|
|
36
42
|
export function GitignoreScreen() {
|
|
37
43
|
const { state } = useApp();
|
|
38
44
|
const modal = useModal();
|
|
39
|
-
const notify = useNotify();
|
|
40
45
|
const renderer = useRenderer();
|
|
41
46
|
const cwd = state.projectPath;
|
|
42
47
|
|
|
@@ -139,109 +144,83 @@ export function GitignoreScreen() {
|
|
|
139
144
|
const handleFixAll = useCallback(async () => {
|
|
140
145
|
if (!data || toFixRows.length === 0) return;
|
|
141
146
|
try {
|
|
142
|
-
const
|
|
143
|
-
const failures: string[] = [];
|
|
147
|
+
const failures: Array<{ path: string; message: string }> = [];
|
|
144
148
|
for (const { row } of toFixRows) {
|
|
145
149
|
for (const v of row.violations) {
|
|
146
150
|
const r = await applyDestructiveFix(cwd, v, row.rule.pattern);
|
|
147
|
-
(r.applied
|
|
151
|
+
if (!r.applied) failures.push({ path: v.path, message: r.message });
|
|
148
152
|
}
|
|
149
153
|
}
|
|
150
154
|
await reload();
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
[...failures, ...messages].join("\n"),
|
|
155
|
-
"error",
|
|
156
|
-
);
|
|
157
|
-
}
|
|
155
|
+
// Only what did not apply, each with why; the reloaded rows show the rest.
|
|
156
|
+
const report = gitignoreFixReport(failures);
|
|
157
|
+
if (report) await modal.failures(report);
|
|
158
158
|
} catch (err) {
|
|
159
|
-
await modal.
|
|
160
|
-
"
|
|
161
|
-
err instanceof Error ? err.message : String(err),
|
|
162
|
-
"error",
|
|
159
|
+
await modal.failures(
|
|
160
|
+
stepFailureReport("The fixes were not applied", ".gitignore", err),
|
|
163
161
|
);
|
|
164
162
|
}
|
|
165
163
|
}, [cwd, data, toFixRows, reload, modal]);
|
|
166
164
|
|
|
167
165
|
// Fix just the highlighted row.
|
|
168
166
|
const handleFixOne = useCallback(async () => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (selectedRow.violations.length === 0) {
|
|
174
|
-
notify(`${selectedRow.rule.pattern} already matches — nothing to fix`);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
167
|
+
// Nothing highlighted, or a row that already matches: nothing to fix.
|
|
168
|
+
if (!selectedRow || selectedRow.violations.length === 0) return;
|
|
177
169
|
|
|
178
170
|
try {
|
|
179
|
-
const
|
|
171
|
+
const failures: Array<{ path: string; message: string }> = [];
|
|
180
172
|
for (const v of selectedRow.violations) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
);
|
|
173
|
+
const r = await applyDestructiveFix(cwd, v, selectedRow.rule.pattern);
|
|
174
|
+
if (!r.applied) failures.push({ path: v.path, message: r.message });
|
|
184
175
|
}
|
|
185
|
-
const failed = results.filter((r) => !r.applied);
|
|
186
176
|
await reload();
|
|
187
|
-
// Success is silent — the reload removes the fixed row. Only
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
"Some fixes didn't apply",
|
|
192
|
-
results.map((r) => r.message).join("\n"),
|
|
193
|
-
"error",
|
|
194
|
-
);
|
|
195
|
-
}
|
|
177
|
+
// Success is silent — the reload removes the fixed row. Only what did
|
|
178
|
+
// not apply goes into the failure popup.
|
|
179
|
+
const report = gitignoreFixReport(failures);
|
|
180
|
+
if (report) await modal.failures(report);
|
|
196
181
|
} catch (err) {
|
|
197
|
-
await modal.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
182
|
+
await modal.failures(
|
|
183
|
+
stepFailureReport(
|
|
184
|
+
"The fix was not applied",
|
|
185
|
+
selectedRow.rule.pattern,
|
|
186
|
+
err,
|
|
187
|
+
),
|
|
201
188
|
);
|
|
202
189
|
}
|
|
203
|
-
}, [selectedRow, modal,
|
|
190
|
+
}, [selectedRow, modal, cwd, reload]);
|
|
204
191
|
|
|
205
192
|
const handleApplyTemplate = useCallback(async () => {
|
|
206
193
|
const names = Object.keys(BUILTIN_TEMPLATES);
|
|
207
|
-
if (names.length === 0)
|
|
208
|
-
notify("No built-in templates available", "warning");
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
194
|
+
if (names.length === 0) return;
|
|
211
195
|
const selected = await modal.select(
|
|
212
196
|
"Apply template",
|
|
213
197
|
"Merge the template into the project manifest at .claude/gitignore.json.",
|
|
214
198
|
names.map((n) => ({ label: n, value: n })),
|
|
215
199
|
);
|
|
216
200
|
if (!selected) return;
|
|
201
|
+
let report: FailureReport | null = null;
|
|
217
202
|
try {
|
|
203
|
+
// What was merged shows in the list. What was skipped does not, so it
|
|
204
|
+
// is the one thing said — in the failure popup.
|
|
218
205
|
const r = await applyTemplate(cwd, selected);
|
|
219
|
-
|
|
220
|
-
`Applied "${selected}": ${r.added.ignore.length} ignore + ${r.added.track.length} track entries`,
|
|
221
|
-
);
|
|
222
|
-
// Skipped entries are not visible in the list, so they get their own line.
|
|
223
|
-
if (r.conflicts.length > 0) {
|
|
224
|
-
notify(
|
|
225
|
-
`Skipped, conflicting with existing rules: ${r.conflicts.join(", ")}`,
|
|
226
|
-
"warning",
|
|
227
|
-
);
|
|
228
|
-
}
|
|
206
|
+
report = gitignoreTemplateReport(selected, r.conflicts);
|
|
229
207
|
} catch (err) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
208
|
+
report = stepFailureReport(
|
|
209
|
+
`Template "${selected}" was not applied`,
|
|
210
|
+
".claude/gitignore.json",
|
|
211
|
+
err,
|
|
234
212
|
);
|
|
235
213
|
}
|
|
236
214
|
await reload();
|
|
237
|
-
|
|
215
|
+
if (report) await modal.failures(report);
|
|
216
|
+
}, [cwd, modal, reload]);
|
|
238
217
|
|
|
239
218
|
/**
|
|
240
219
|
* Open the project manifest in $EDITOR, creating it first if needed.
|
|
241
220
|
*
|
|
242
221
|
* Every await sits inside the try: this runs fire-and-forget from a key, and a
|
|
243
222
|
* rejection nobody catches ends the app. A manifest that cannot be read or
|
|
244
|
-
* created is said in
|
|
223
|
+
* created is said in the failure popup instead.
|
|
245
224
|
*/
|
|
246
225
|
const handleEditManifest = useCallback(async () => {
|
|
247
226
|
let projectManifest: string;
|
|
@@ -252,10 +231,12 @@ export function GitignoreScreen() {
|
|
|
252
231
|
current.resolved,
|
|
253
232
|
);
|
|
254
233
|
} catch (err) {
|
|
255
|
-
await modal.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
234
|
+
await modal.failures(
|
|
235
|
+
stepFailureReport(
|
|
236
|
+
"The project's rules were not opened for editing",
|
|
237
|
+
".claude/gitignore.json",
|
|
238
|
+
err,
|
|
239
|
+
),
|
|
259
240
|
);
|
|
260
241
|
return;
|
|
261
242
|
}
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import React, { useEffect, useCallback, useState, useRef } from "react";
|
|
2
|
+
import { stepFailureReport } from "../adapters/startupReports.js";
|
|
2
3
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
3
4
|
import { useScreenKeys, useSearchInput } from "../input/KeyRouter.js";
|
|
4
5
|
import { moveDirection } from "../keymap.js";
|
|
5
6
|
import {
|
|
6
7
|
useApp,
|
|
8
|
+
useFailurePopup,
|
|
7
9
|
useModal,
|
|
8
10
|
useNavigation,
|
|
9
|
-
useNotify,
|
|
10
11
|
} from "../state/AppContext.js";
|
|
11
12
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
12
13
|
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
addMcpServer,
|
|
16
|
+
getEnabledMcpServers,
|
|
17
|
+
getInstalledMcpServers,
|
|
18
|
+
setAllowMcp,
|
|
19
|
+
} from "../../services/claude-settings.js";
|
|
14
20
|
import { formatDate, searchMcpServers } from "../../services/mcp-registry.js";
|
|
15
21
|
import type { McpRegistryServer, McpServerConfig } from "../../types/index.js";
|
|
16
22
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
17
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
type McpRegistryListItem,
|
|
25
|
+
renderMcpRegistryRow,
|
|
26
|
+
renderMcpRegistryStatus,
|
|
27
|
+
} from "../renderers/mcpRenderers.js";
|
|
18
28
|
import { theme } from "../theme.js";
|
|
19
29
|
|
|
20
30
|
/**
|
|
@@ -80,7 +90,7 @@ export function McpRegistryScreen() {
|
|
|
80
90
|
const { state, dispatch } = useApp();
|
|
81
91
|
const { mcpRegistry } = state;
|
|
82
92
|
const modal = useModal();
|
|
83
|
-
const
|
|
93
|
+
const showFailures = useFailurePopup();
|
|
84
94
|
const { navigateToScreen } = useNavigation();
|
|
85
95
|
const dimensions = useDimensions();
|
|
86
96
|
|
|
@@ -94,6 +104,33 @@ export function McpRegistryScreen() {
|
|
|
94
104
|
|
|
95
105
|
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
96
106
|
|
|
107
|
+
/**
|
|
108
|
+
* The servers this project has configured, read from the same place the MCP
|
|
109
|
+
* screen reads. A row here shows installed from THIS, and the restart from
|
|
110
|
+
* `mcp.changedThisSession` — the same two states a curated row carries.
|
|
111
|
+
*/
|
|
112
|
+
const [installedNames, setInstalledNames] = useState<ReadonlySet<string>>(
|
|
113
|
+
new Set(),
|
|
114
|
+
);
|
|
115
|
+
const loadInstalled = useCallback(async () => {
|
|
116
|
+
try {
|
|
117
|
+
const [installed, enabled] = await Promise.all([
|
|
118
|
+
getInstalledMcpServers(state.projectPath),
|
|
119
|
+
getEnabledMcpServers(state.projectPath),
|
|
120
|
+
]);
|
|
121
|
+
setInstalledNames(
|
|
122
|
+
new Set([...Object.keys(installed), ...Object.keys(enabled)]),
|
|
123
|
+
);
|
|
124
|
+
} catch {
|
|
125
|
+
// A configuration that cannot be read is the MCP screen's to report,
|
|
126
|
+
// where it replaces the list; here the rows simply carry no marks.
|
|
127
|
+
setInstalledNames(new Set());
|
|
128
|
+
}
|
|
129
|
+
}, [state.projectPath]);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
void loadInstalled();
|
|
132
|
+
}, [loadInstalled]);
|
|
133
|
+
|
|
97
134
|
// Load servers
|
|
98
135
|
const loadServers = useCallback(async (query: string) => {
|
|
99
136
|
setIsLoading(true);
|
|
@@ -196,16 +233,28 @@ export function McpRegistryScreen() {
|
|
|
196
233
|
await setAllowMcp(true, state.projectPath);
|
|
197
234
|
await addMcpServer(server.name, config, state.projectPath);
|
|
198
235
|
modal.hideModal();
|
|
199
|
-
//
|
|
200
|
-
|
|
236
|
+
// Shown on THIS row as its state — installed, and "↻ restart Claude
|
|
237
|
+
// Code" — not as a message. The MCP screen lists curated servers only,
|
|
238
|
+
// so this row is where the user can see it.
|
|
239
|
+
dispatch({ type: "MCP_SERVER_CHANGED", name: server.name });
|
|
240
|
+
await loadInstalled();
|
|
201
241
|
} catch (error) {
|
|
202
242
|
modal.hideModal();
|
|
203
|
-
await
|
|
243
|
+
await showFailures(
|
|
244
|
+
stepFailureReport(`Could not add ${server.name}`, server.name, error),
|
|
245
|
+
);
|
|
204
246
|
}
|
|
205
247
|
};
|
|
206
248
|
|
|
249
|
+
const listItems: McpRegistryListItem[] = servers.map((server) => ({
|
|
250
|
+
server,
|
|
251
|
+
isInstalled: installedNames.has(server.name),
|
|
252
|
+
needsRestart: state.mcp.changedThisSession.includes(server.name),
|
|
253
|
+
}));
|
|
254
|
+
|
|
207
255
|
// Get selected server
|
|
208
|
-
const
|
|
256
|
+
const selectedItem = listItems[mcpRegistry.selectedIndex];
|
|
257
|
+
const selectedServer = selectedItem?.server;
|
|
209
258
|
|
|
210
259
|
const renderDetail = () => {
|
|
211
260
|
if (isLoading) {
|
|
@@ -216,7 +265,7 @@ export function McpRegistryScreen() {
|
|
|
216
265
|
return <text fg={theme.colors.danger}>Error: {error}</text>;
|
|
217
266
|
}
|
|
218
267
|
|
|
219
|
-
if (!selectedServer) {
|
|
268
|
+
if (!selectedServer || !selectedItem) {
|
|
220
269
|
return (
|
|
221
270
|
<text fg={theme.colors.muted}>Select a server to see details</text>
|
|
222
271
|
);
|
|
@@ -238,6 +287,7 @@ export function McpRegistryScreen() {
|
|
|
238
287
|
<box marginTop={1}>
|
|
239
288
|
<text fg={theme.colors.text}>{selectedServer.short_description}</text>
|
|
240
289
|
</box>
|
|
290
|
+
<box marginTop={1}>{renderMcpRegistryStatus(selectedItem)}</box>
|
|
241
291
|
<box flexDirection="row" marginTop={1}>
|
|
242
292
|
<text fg={theme.colors.text}>
|
|
243
293
|
<strong>Version: </strong>
|
|
@@ -266,36 +316,15 @@ export function McpRegistryScreen() {
|
|
|
266
316
|
</text>
|
|
267
317
|
</box>
|
|
268
318
|
)}
|
|
269
|
-
|
|
270
|
-
<
|
|
271
|
-
|
|
319
|
+
{selectedItem.isInstalled ? null : (
|
|
320
|
+
<box marginTop={1}>
|
|
321
|
+
<text fg={theme.colors.muted}>Press Enter to install</text>
|
|
322
|
+
</box>
|
|
323
|
+
)}
|
|
272
324
|
</box>
|
|
273
325
|
);
|
|
274
326
|
};
|
|
275
327
|
|
|
276
|
-
const renderListItem = (
|
|
277
|
-
server: McpRegistryServer,
|
|
278
|
-
_idx: number,
|
|
279
|
-
isSelected: boolean,
|
|
280
|
-
) => {
|
|
281
|
-
const version = server.version ? ` v${server.version}` : "";
|
|
282
|
-
return isSelected ? (
|
|
283
|
-
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
284
|
-
{rowLead(true)}
|
|
285
|
-
{server.name}
|
|
286
|
-
{version}{" "}
|
|
287
|
-
</text>
|
|
288
|
-
) : (
|
|
289
|
-
<text fg={theme.colors.text}>
|
|
290
|
-
<span>{rowLead(false)}</span>
|
|
291
|
-
<span>
|
|
292
|
-
<strong>{server.name}</strong>
|
|
293
|
-
</span>
|
|
294
|
-
<span fg={theme.colors.info}>{version}</span>
|
|
295
|
-
</text>
|
|
296
|
-
);
|
|
297
|
-
};
|
|
298
|
-
|
|
299
328
|
// Status for search placeholder
|
|
300
329
|
const searchPlaceholder = `${servers.length} servers │ / to search`;
|
|
301
330
|
|
|
@@ -319,11 +348,11 @@ export function McpRegistryScreen() {
|
|
|
319
348
|
<text fg={theme.colors.muted}>No servers found</text>
|
|
320
349
|
) : (
|
|
321
350
|
<ScrollableList
|
|
322
|
-
items={
|
|
351
|
+
items={listItems}
|
|
323
352
|
selectedIndex={mcpRegistry.selectedIndex}
|
|
324
|
-
renderItem={
|
|
353
|
+
renderItem={renderMcpRegistryRow}
|
|
325
354
|
maxHeight={dimensions.listPanelHeight}
|
|
326
|
-
getKey={(
|
|
355
|
+
getKey={(item) => item.server.name}
|
|
327
356
|
/>
|
|
328
357
|
)
|
|
329
358
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import React, { useEffect, useCallback, useMemo } from "react";
|
|
2
|
+
import { failureReport } from "../adapters/failureReport.js";
|
|
3
|
+
import { stepFailureReport } from "../adapters/startupReports.js";
|
|
2
4
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
3
5
|
import { useScreenKeys } from "../input/KeyRouter.js";
|
|
4
6
|
import { moveDirection } from "../keymap.js";
|
|
5
7
|
import {
|
|
6
8
|
useApp,
|
|
9
|
+
useFailurePopup,
|
|
7
10
|
useModal,
|
|
8
11
|
useNavigation,
|
|
9
|
-
useNotify,
|
|
10
12
|
} from "../state/AppContext.js";
|
|
11
13
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
12
14
|
|
|
@@ -35,7 +37,7 @@ export function McpScreen() {
|
|
|
35
37
|
const { state, dispatch } = useApp();
|
|
36
38
|
const { mcp } = state;
|
|
37
39
|
const modal = useModal();
|
|
38
|
-
const
|
|
40
|
+
const showFailures = useFailurePopup();
|
|
39
41
|
const { navigateToScreen } = useNavigation();
|
|
40
42
|
const dimensions = useDimensions();
|
|
41
43
|
|
|
@@ -96,22 +98,34 @@ export function McpScreen() {
|
|
|
96
98
|
|
|
97
99
|
for (const server of servers) {
|
|
98
100
|
const isInstalled = mcp.installedServers[server.name] !== undefined;
|
|
99
|
-
items.push({
|
|
101
|
+
items.push({
|
|
102
|
+
kind: "server",
|
|
103
|
+
server,
|
|
104
|
+
isInstalled,
|
|
105
|
+
needsRestart: mcp.changedThisSession.includes(server.name),
|
|
106
|
+
});
|
|
100
107
|
}
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
return items;
|
|
104
|
-
}, [mcp.servers, mcp.installedServers]);
|
|
111
|
+
}, [mcp.servers, mcp.installedServers, mcp.changedThisSession]);
|
|
105
112
|
|
|
106
113
|
useScreenKeys("mcp", {
|
|
107
114
|
// Guarded: `installMcpServer` throws on a catalogue entry with no URL or
|
|
108
115
|
// command, and a rejection nobody catches ends the app.
|
|
109
116
|
toggle: () => {
|
|
117
|
+
const item = allListItems[mcp.selectedIndex];
|
|
118
|
+
if (item?.kind !== "server") return;
|
|
119
|
+
const { name } = item.server;
|
|
110
120
|
handleSelect().catch((error: unknown) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
showFailures(
|
|
122
|
+
stepFailureReport(
|
|
123
|
+
item.isInstalled
|
|
124
|
+
? `Could not remove ${name}`
|
|
125
|
+
: `Could not add ${name}`,
|
|
126
|
+
name,
|
|
127
|
+
error,
|
|
128
|
+
),
|
|
115
129
|
),
|
|
116
130
|
);
|
|
117
131
|
},
|
|
@@ -147,11 +161,19 @@ export function McpScreen() {
|
|
|
147
161
|
try {
|
|
148
162
|
await removeMcpServer(server.name, state.projectPath);
|
|
149
163
|
modal.hideModal();
|
|
150
|
-
// The row turns uninstalled
|
|
164
|
+
// The row turns uninstalled, and says a running Claude Code still
|
|
165
|
+
// has it until it restarts.
|
|
166
|
+
dispatch({ type: "MCP_SERVER_CHANGED", name: server.name });
|
|
151
167
|
fetchData();
|
|
152
168
|
} catch (error) {
|
|
153
169
|
modal.hideModal();
|
|
154
|
-
await
|
|
170
|
+
await showFailures(
|
|
171
|
+
stepFailureReport(
|
|
172
|
+
`Could not remove ${server.name}`,
|
|
173
|
+
server.name,
|
|
174
|
+
error,
|
|
175
|
+
),
|
|
176
|
+
);
|
|
155
177
|
}
|
|
156
178
|
}
|
|
157
179
|
} else {
|
|
@@ -202,10 +224,14 @@ export function McpScreen() {
|
|
|
202
224
|
if (value === null) return;
|
|
203
225
|
|
|
204
226
|
if (field.required && !value) {
|
|
205
|
-
await
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
227
|
+
await showFailures(
|
|
228
|
+
failureReport(`Could not add ${server.name}`, [
|
|
229
|
+
{
|
|
230
|
+
subject: field.label,
|
|
231
|
+
reason: "is required, and was left empty",
|
|
232
|
+
fix: `Press Enter on ${server.name} again and fill it in`,
|
|
233
|
+
},
|
|
234
|
+
]),
|
|
209
235
|
);
|
|
210
236
|
return;
|
|
211
237
|
}
|
|
@@ -221,14 +247,15 @@ export function McpScreen() {
|
|
|
221
247
|
try {
|
|
222
248
|
await addMcpServer(server.name, config, state.projectPath);
|
|
223
249
|
modal.hideModal();
|
|
224
|
-
// The row shows it installed
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
notify(`Added ${server.name} — restart Claude Code to use it`, "warning");
|
|
250
|
+
// The row shows it installed AND that it needs a restart to be used —
|
|
251
|
+
// state on the server, not a message that scrolls away.
|
|
252
|
+
dispatch({ type: "MCP_SERVER_CHANGED", name: server.name });
|
|
228
253
|
fetchData();
|
|
229
254
|
} catch (error) {
|
|
230
255
|
modal.hideModal();
|
|
231
|
-
await
|
|
256
|
+
await showFailures(
|
|
257
|
+
stepFailureReport(`Could not add ${server.name}`, server.name, error),
|
|
258
|
+
);
|
|
232
259
|
}
|
|
233
260
|
};
|
|
234
261
|
|