magus-cli 7.7.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.
- package/package.json +4 -4
- package/src/__tests__/blackbox-parity.test.ts +29 -15
- package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
- package/src/__tests__/blind-780-cli-tools.test.ts +808 -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 +1203 -0
- package/src/__tests__/cli-update-view.test.ts +3 -3
- package/src/__tests__/deprecated-remove.test.ts +827 -0
- package/src/__tests__/doctor.test.ts +159 -1
- package/src/__tests__/failure-popup-review2.test.tsx +920 -0
- package/src/__tests__/failure-popup.test.tsx +419 -0
- package/src/__tests__/final-fixes-780.test.tsx +724 -0
- package/src/__tests__/format-update-outcome.test.ts +6 -32
- package/src/__tests__/gitignore-profile.test.ts +1 -9
- package/src/__tests__/models-screen-state.test.ts +18 -40
- 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__/stale-load-repair.test.ts +31 -23
- package/src/__tests__/update-apply.test.ts +13 -29
- package/src/__tests__/update-plan.test.ts +23 -17
- package/src/__tests__/useStaleLoadModal.test.ts +1 -57
- package/src/__tests__/version-unknown-state.test.ts +15 -17
- package/src/cli/doctor.ts +151 -30
- package/src/cli/install.ts +5 -6
- package/src/cli/update-view.ts +58 -15
- package/src/cli/update.ts +27 -5
- package/src/services/catalog-cache-store.ts +8 -6
- package/src/services/claude-cli.ts +106 -19
- package/src/services/claude-settings.ts +84 -36
- package/src/services/plugin-env-needs.ts +69 -0
- package/src/services/plugin-manager.ts +31 -19
- 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 +95 -1
- package/src/services/update-engine.ts +326 -31
- package/src/services/update-plan.ts +293 -33
- package/src/types/index.ts +7 -0
- package/src/ui/App.tsx +90 -146
- package/src/ui/adapters/cliToolsAdapter.ts +203 -1
- package/src/ui/adapters/failureReport.ts +137 -0
- package/src/ui/adapters/pluginsAdapter.ts +297 -45
- package/src/ui/adapters/startupReports.ts +282 -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/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 +65 -54
- 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 +313 -221
- 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/user-path.ts +60 -0
- package/src/__tests__/notices-rules.test.ts +0 -476
- package/src/__tests__/notices.test.tsx +0 -604
- package/src/__tests__/styles-status-line.test.ts +0 -72
- package/src/__tests__/version-snapshot.test.ts +0 -212
- package/src/services/version-snapshot.ts +0 -189
|
@@ -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
|
|
|
@@ -7,11 +7,13 @@ import {
|
|
|
7
7
|
readModelsConfig,
|
|
8
8
|
readModelsStatus,
|
|
9
9
|
} from "../../services/models-manager.js";
|
|
10
|
+
import { failureReport } from "../adapters/failureReport.js";
|
|
10
11
|
import {
|
|
11
12
|
type ModelsBrowserItem,
|
|
12
13
|
buildModelsItems,
|
|
13
14
|
firstSelectableIndex,
|
|
14
15
|
} from "../adapters/modelsAdapter.js";
|
|
16
|
+
import { stepFailureReport } from "../adapters/startupReports.js";
|
|
15
17
|
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
16
18
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
17
19
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
@@ -23,7 +25,7 @@ import {
|
|
|
23
25
|
renderModelRow,
|
|
24
26
|
renderModelsSummary,
|
|
25
27
|
} from "../renderers/modelRenderers.js";
|
|
26
|
-
import { useApp } from "../state/AppContext.js";
|
|
28
|
+
import { useApp, useFailurePopup } from "../state/AppContext.js";
|
|
27
29
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
28
30
|
import { asyncValue } from "../state/types.js";
|
|
29
31
|
import { theme } from "../theme.js";
|
|
@@ -134,22 +136,15 @@ export function ModelsScreen() {
|
|
|
134
136
|
};
|
|
135
137
|
}, [dispatch]);
|
|
136
138
|
|
|
137
|
-
// ──
|
|
139
|
+
// ── Feedback ──────────────────────────────────────────────────────────────
|
|
138
140
|
|
|
139
141
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* the NEXT action starts instead.
|
|
142
|
+
* No status line and no messages. A success shows as the screen's state —
|
|
143
|
+
* the reloaded list, "Model tiers: on", "writing…" while a write runs. What
|
|
144
|
+
* did not happen goes into the one failure popup, with why and, when there
|
|
145
|
+
* is one, what to do. A key pressed where it does nothing does nothing.
|
|
145
146
|
*/
|
|
146
|
-
const
|
|
147
|
-
(text: string, tone: "success" | "error" = "success") => {
|
|
148
|
-
dispatch({ type: "MODELS_STATUS_SET", status: { text, tone } });
|
|
149
|
-
},
|
|
150
|
-
[dispatch],
|
|
151
|
-
);
|
|
152
|
-
const statusMsg = modelsState.status;
|
|
147
|
+
const showFailures = useFailurePopup();
|
|
153
148
|
|
|
154
149
|
// ── Derived ───────────────────────────────────────────────────────────────
|
|
155
150
|
|
|
@@ -189,18 +184,17 @@ export function ModelsScreen() {
|
|
|
189
184
|
*
|
|
190
185
|
* No `ensureManifest` is passed. That helper prompts on the terminal, and
|
|
191
186
|
* this process's terminal belongs to OpenTUI — a project with no profiles
|
|
192
|
-
* gets a plain `.claude/models.json
|
|
187
|
+
* gets a plain `.claude/models.json`.
|
|
188
|
+
*
|
|
189
|
+
* A press while a write is in flight does nothing: the header already says
|
|
190
|
+
* "writing…". Neither does one with no preset under the cursor — the only
|
|
191
|
+
* other row is the status header, which the cursor never lands on, so that
|
|
192
|
+
* is an empty filtered list.
|
|
193
193
|
*/
|
|
194
194
|
const handleApply = useCallback(async () => {
|
|
195
|
-
if (modelsState.isApplying)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
if (!selectedItem || selectedItem.kind !== "preset") {
|
|
200
|
-
showStatus("Move to a preset row and press a", "error");
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
const { config, custom } = selectedItem;
|
|
195
|
+
if (modelsState.isApplying) return;
|
|
196
|
+
if (!selectedItem || selectedItem.kind !== "preset") return;
|
|
197
|
+
const { config, custom, label } = selectedItem;
|
|
204
198
|
|
|
205
199
|
dispatch({ type: "MODELS_APPLY_START" });
|
|
206
200
|
try {
|
|
@@ -213,24 +207,24 @@ export function ModelsScreen() {
|
|
|
213
207
|
projectPath: state.projectPath,
|
|
214
208
|
preset: custom ? config : config.preset,
|
|
215
209
|
});
|
|
210
|
+
// Applied: the reloaded list and the header show it. Said only when
|
|
211
|
+
// the profile did NOT record it — the next magus install renders the
|
|
212
|
+
// live settings from the profile and drops the routing.
|
|
216
213
|
await fetchData();
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
result.profile && !result.recordedInManifest ? "error" : "success",
|
|
229
|
-
);
|
|
214
|
+
if (result.profile && !result.recordedInManifest) {
|
|
215
|
+
await showFailures(
|
|
216
|
+
failureReport(`${label} is active, but not saved in the profile`, [
|
|
217
|
+
{
|
|
218
|
+
subject: `profile ${result.profile}`,
|
|
219
|
+
reason:
|
|
220
|
+
"the routing is not recorded in it, so the next magus install drops it",
|
|
221
|
+
},
|
|
222
|
+
]),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
230
225
|
} catch (error) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
"error",
|
|
226
|
+
await showFailures(
|
|
227
|
+
stepFailureReport("No model routing was applied", label, error),
|
|
234
228
|
);
|
|
235
229
|
} finally {
|
|
236
230
|
dispatch({ type: "MODELS_APPLY_END" });
|
|
@@ -240,26 +234,24 @@ export function ModelsScreen() {
|
|
|
240
234
|
selectedItem,
|
|
241
235
|
state.projectPath,
|
|
242
236
|
fetchData,
|
|
243
|
-
|
|
237
|
+
showFailures,
|
|
244
238
|
dispatch,
|
|
245
239
|
]);
|
|
246
240
|
|
|
241
|
+
// Off: the reloaded header reads "Model tiers: off".
|
|
247
242
|
const handleClear = useCallback(async () => {
|
|
248
|
-
if (modelsState.isApplying)
|
|
249
|
-
showStatus("Already writing the routing — one at a time", "error");
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
243
|
+
if (modelsState.isApplying) return;
|
|
252
244
|
dispatch({ type: "MODELS_APPLY_START" });
|
|
253
245
|
try {
|
|
254
246
|
await clearModels(state.projectPath);
|
|
255
247
|
await fetchData();
|
|
256
|
-
showStatus(
|
|
257
|
-
"Model tiers off — subagents inherit the session model again. The hook stays registered and is a no-op without a config.",
|
|
258
|
-
);
|
|
259
248
|
} catch (error) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
249
|
+
await showFailures(
|
|
250
|
+
stepFailureReport(
|
|
251
|
+
"Model tiers were not turned off",
|
|
252
|
+
"model routing",
|
|
253
|
+
error,
|
|
254
|
+
),
|
|
263
255
|
);
|
|
264
256
|
} finally {
|
|
265
257
|
dispatch({ type: "MODELS_APPLY_END" });
|
|
@@ -268,7 +260,7 @@ export function ModelsScreen() {
|
|
|
268
260
|
modelsState.isApplying,
|
|
269
261
|
state.projectPath,
|
|
270
262
|
fetchData,
|
|
271
|
-
|
|
263
|
+
showFailures,
|
|
272
264
|
dispatch,
|
|
273
265
|
]);
|
|
274
266
|
|
|
@@ -301,67 +293,36 @@ export function ModelsScreen() {
|
|
|
301
293
|
blur: () => dispatch({ type: "SET_SEARCHING", isSearching: false }),
|
|
302
294
|
});
|
|
303
295
|
|
|
304
|
-
/**
|
|
305
|
-
* The previous message belongs to the previous ACTION, so every key this
|
|
306
|
-
* screen acts on clears it first. Keys another layer takes — a digit or Tab
|
|
307
|
-
* leaving the tab, text typed into the search box — leave it alone, so a
|
|
308
|
-
* round trip through another tab keeps it.
|
|
309
|
-
*/
|
|
310
|
-
const clearStatus = () => {
|
|
311
|
-
if (modelsState.status) dispatch({ type: "MODELS_STATUS_CLEAR" });
|
|
312
|
-
};
|
|
313
|
-
|
|
314
296
|
useScreenKeys("models", {
|
|
315
297
|
// Enter and `a` are the same action here. There is nothing to "open" on
|
|
316
298
|
// this screen — a preset row has one thing it can do — so binding Enter
|
|
317
299
|
// to anything else would leave the most obvious key doing nothing.
|
|
318
300
|
apply: () => {
|
|
319
|
-
clearStatus();
|
|
320
301
|
void handleApply();
|
|
321
302
|
},
|
|
322
303
|
off: () => {
|
|
323
|
-
clearStatus();
|
|
324
304
|
void handleClear();
|
|
325
305
|
},
|
|
326
306
|
reload: () => {
|
|
327
|
-
clearStatus();
|
|
328
307
|
void fetchData();
|
|
329
308
|
},
|
|
330
309
|
search: () => {
|
|
331
|
-
clearStatus();
|
|
332
310
|
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
333
311
|
},
|
|
334
312
|
// Only while a committed filter is set; otherwise Esc is the global back.
|
|
335
313
|
"clear-filter": () => {
|
|
336
314
|
if (!modelsState.searchQuery) return false;
|
|
337
|
-
clearStatus();
|
|
338
315
|
dispatch({ type: "MODELS_SET_SEARCH", query: "" });
|
|
339
316
|
dispatch({ type: "MODELS_SELECT", index: 0 });
|
|
340
317
|
return true;
|
|
341
318
|
},
|
|
342
319
|
move: (key) => {
|
|
343
|
-
clearStatus();
|
|
344
320
|
move(moveDirection(key));
|
|
345
321
|
},
|
|
346
322
|
});
|
|
347
323
|
|
|
348
324
|
// ── Chrome ────────────────────────────────────────────────────────────────
|
|
349
325
|
|
|
350
|
-
// Transient feedback takes the full-width notice row under the header, never
|
|
351
|
-
// the header itself: a long message beside the title shrinks the title away
|
|
352
|
-
// and the two read as one garbled sentence.
|
|
353
|
-
const noticeContent = statusMsg ? (
|
|
354
|
-
<text
|
|
355
|
-
fg={
|
|
356
|
-
statusMsg.tone === "success"
|
|
357
|
-
? theme.colors.success
|
|
358
|
-
: theme.colors.danger
|
|
359
|
-
}
|
|
360
|
-
>
|
|
361
|
-
{statusMsg.text}
|
|
362
|
-
</text>
|
|
363
|
-
) : undefined;
|
|
364
|
-
|
|
365
326
|
// The file's `preset` string is deliberately not shown beside the state: it records what
|
|
366
327
|
// was last APPLIED, so a config edited afterwards would wear a built-in's name here while
|
|
367
328
|
// the list correctly calls it `Custom`.
|
|
@@ -398,7 +359,6 @@ export function ModelsScreen() {
|
|
|
398
359
|
title="magus Models"
|
|
399
360
|
currentScreen="models"
|
|
400
361
|
statusLine={statusContent}
|
|
401
|
-
notice={noticeContent}
|
|
402
362
|
search={
|
|
403
363
|
modelsState.searchQuery || isSearchActive
|
|
404
364
|
? {
|
|
@@ -469,8 +429,8 @@ export function ModelsScreen() {
|
|
|
469
429
|
configPath: snapshot?.path ?? null,
|
|
470
430
|
width: listWidth,
|
|
471
431
|
// Rows the list did not use. One held back because the
|
|
472
|
-
// search bar
|
|
473
|
-
//
|
|
432
|
+
// search bar takes a row from the panel without taking
|
|
433
|
+
// one from `listPanelHeight` — and an
|
|
474
434
|
// over-tall panel overprints rather than clipping.
|
|
475
435
|
available:
|
|
476
436
|
dimensions.listPanelHeight -
|