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.
- 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 +9 -34
- package/src/__tests__/gitignore-profile.test.ts +0 -4
- 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 -42
- 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/gitignore-service.ts +0 -19
- 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-absorb.ts +9 -0
- package/src/services/profile-activate.ts +1 -1
- package/src/services/profile-startup.ts +30 -27
- package/src/services/profile-state.ts +81 -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 +186 -123
- package/src/ui/adapters/cliToolsAdapter.ts +203 -1
- package/src/ui/adapters/failureReport.ts +137 -0
- package/src/ui/adapters/pluginsAdapter.ts +299 -28
- 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 -13
- 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 +31 -31
- package/src/ui/hooks/useProfileStartupModal.ts +127 -122
- package/src/ui/hooks/useStaleLoadModal.ts +33 -53
- 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 -44
- package/src/ui/screens/CliToolsScreen.tsx +65 -51
- package/src/ui/screens/EnvVarsScreen.tsx +21 -6
- package/src/ui/screens/GitignoreScreen.tsx +49 -75
- package/src/ui/screens/McpRegistryScreen.tsx +72 -40
- package/src/ui/screens/McpScreen.tsx +55 -24
- package/src/ui/screens/ModelsScreen.tsx +46 -86
- package/src/ui/screens/PluginsScreen.tsx +316 -245
- package/src/ui/screens/ProfilesScreen.tsx +88 -73
- package/src/ui/screens/SkillsScreen.tsx +83 -80
- package/src/ui/screens/StylesScreen.tsx +211 -275
- package/src/ui/state/AppContext.tsx +72 -7
- package/src/ui/state/DimensionsContext.tsx +3 -22
- package/src/ui/state/reducer.ts +25 -14
- package/src/ui/state/types.ts +61 -31
- package/src/utils/user-path.ts +60 -0
- 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,15 +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
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
useApp,
|
|
8
|
+
useFailurePopup,
|
|
9
|
+
useModal,
|
|
10
|
+
useNavigation,
|
|
11
|
+
} from "../state/AppContext.js";
|
|
6
12
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
7
13
|
|
|
8
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
addMcpServer,
|
|
16
|
+
getEnabledMcpServers,
|
|
17
|
+
getInstalledMcpServers,
|
|
18
|
+
setAllowMcp,
|
|
19
|
+
} from "../../services/claude-settings.js";
|
|
9
20
|
import { formatDate, searchMcpServers } from "../../services/mcp-registry.js";
|
|
10
21
|
import type { McpRegistryServer, McpServerConfig } from "../../types/index.js";
|
|
11
22
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
12
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
type McpRegistryListItem,
|
|
25
|
+
renderMcpRegistryRow,
|
|
26
|
+
renderMcpRegistryStatus,
|
|
27
|
+
} from "../renderers/mcpRenderers.js";
|
|
13
28
|
import { theme } from "../theme.js";
|
|
14
29
|
|
|
15
30
|
/**
|
|
@@ -75,6 +90,7 @@ export function McpRegistryScreen() {
|
|
|
75
90
|
const { state, dispatch } = useApp();
|
|
76
91
|
const { mcpRegistry } = state;
|
|
77
92
|
const modal = useModal();
|
|
93
|
+
const showFailures = useFailurePopup();
|
|
78
94
|
const { navigateToScreen } = useNavigation();
|
|
79
95
|
const dimensions = useDimensions();
|
|
80
96
|
|
|
@@ -88,6 +104,33 @@ export function McpRegistryScreen() {
|
|
|
88
104
|
|
|
89
105
|
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
90
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
|
+
|
|
91
134
|
// Load servers
|
|
92
135
|
const loadServers = useCallback(async (query: string) => {
|
|
93
136
|
setIsLoading(true);
|
|
@@ -190,19 +233,28 @@ export function McpRegistryScreen() {
|
|
|
190
233
|
await setAllowMcp(true, state.projectPath);
|
|
191
234
|
await addMcpServer(server.name, config, state.projectPath);
|
|
192
235
|
modal.hideModal();
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
);
|
|
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();
|
|
198
241
|
} catch (error) {
|
|
199
242
|
modal.hideModal();
|
|
200
|
-
await
|
|
243
|
+
await showFailures(
|
|
244
|
+
stepFailureReport(`Could not add ${server.name}`, server.name, error),
|
|
245
|
+
);
|
|
201
246
|
}
|
|
202
247
|
};
|
|
203
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
|
+
|
|
204
255
|
// Get selected server
|
|
205
|
-
const
|
|
256
|
+
const selectedItem = listItems[mcpRegistry.selectedIndex];
|
|
257
|
+
const selectedServer = selectedItem?.server;
|
|
206
258
|
|
|
207
259
|
const renderDetail = () => {
|
|
208
260
|
if (isLoading) {
|
|
@@ -213,7 +265,7 @@ export function McpRegistryScreen() {
|
|
|
213
265
|
return <text fg={theme.colors.danger}>Error: {error}</text>;
|
|
214
266
|
}
|
|
215
267
|
|
|
216
|
-
if (!selectedServer) {
|
|
268
|
+
if (!selectedServer || !selectedItem) {
|
|
217
269
|
return (
|
|
218
270
|
<text fg={theme.colors.muted}>Select a server to see details</text>
|
|
219
271
|
);
|
|
@@ -235,6 +287,7 @@ export function McpRegistryScreen() {
|
|
|
235
287
|
<box marginTop={1}>
|
|
236
288
|
<text fg={theme.colors.text}>{selectedServer.short_description}</text>
|
|
237
289
|
</box>
|
|
290
|
+
<box marginTop={1}>{renderMcpRegistryStatus(selectedItem)}</box>
|
|
238
291
|
<box flexDirection="row" marginTop={1}>
|
|
239
292
|
<text fg={theme.colors.text}>
|
|
240
293
|
<strong>Version: </strong>
|
|
@@ -263,36 +316,15 @@ export function McpRegistryScreen() {
|
|
|
263
316
|
</text>
|
|
264
317
|
</box>
|
|
265
318
|
)}
|
|
266
|
-
|
|
267
|
-
<
|
|
268
|
-
|
|
319
|
+
{selectedItem.isInstalled ? null : (
|
|
320
|
+
<box marginTop={1}>
|
|
321
|
+
<text fg={theme.colors.muted}>Press Enter to install</text>
|
|
322
|
+
</box>
|
|
323
|
+
)}
|
|
269
324
|
</box>
|
|
270
325
|
);
|
|
271
326
|
};
|
|
272
327
|
|
|
273
|
-
const renderListItem = (
|
|
274
|
-
server: McpRegistryServer,
|
|
275
|
-
_idx: number,
|
|
276
|
-
isSelected: boolean,
|
|
277
|
-
) => {
|
|
278
|
-
const version = server.version ? ` v${server.version}` : "";
|
|
279
|
-
return isSelected ? (
|
|
280
|
-
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
281
|
-
{rowLead(true)}
|
|
282
|
-
{server.name}
|
|
283
|
-
{version}{" "}
|
|
284
|
-
</text>
|
|
285
|
-
) : (
|
|
286
|
-
<text fg={theme.colors.text}>
|
|
287
|
-
<span>{rowLead(false)}</span>
|
|
288
|
-
<span>
|
|
289
|
-
<strong>{server.name}</strong>
|
|
290
|
-
</span>
|
|
291
|
-
<span fg={theme.colors.info}>{version}</span>
|
|
292
|
-
</text>
|
|
293
|
-
);
|
|
294
|
-
};
|
|
295
|
-
|
|
296
328
|
// Status for search placeholder
|
|
297
329
|
const searchPlaceholder = `${servers.length} servers │ / to search`;
|
|
298
330
|
|
|
@@ -316,11 +348,11 @@ export function McpRegistryScreen() {
|
|
|
316
348
|
<text fg={theme.colors.muted}>No servers found</text>
|
|
317
349
|
) : (
|
|
318
350
|
<ScrollableList
|
|
319
|
-
items={
|
|
351
|
+
items={listItems}
|
|
320
352
|
selectedIndex={mcpRegistry.selectedIndex}
|
|
321
|
-
renderItem={
|
|
353
|
+
renderItem={renderMcpRegistryRow}
|
|
322
354
|
maxHeight={dimensions.listPanelHeight}
|
|
323
|
-
getKey={(
|
|
355
|
+
getKey={(item) => item.server.name}
|
|
324
356
|
/>
|
|
325
357
|
)
|
|
326
358
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
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
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
useApp,
|
|
9
|
+
useFailurePopup,
|
|
10
|
+
useModal,
|
|
11
|
+
useNavigation,
|
|
12
|
+
} from "../state/AppContext.js";
|
|
6
13
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
7
14
|
|
|
8
15
|
import {
|
|
@@ -30,6 +37,7 @@ export function McpScreen() {
|
|
|
30
37
|
const { state, dispatch } = useApp();
|
|
31
38
|
const { mcp } = state;
|
|
32
39
|
const modal = useModal();
|
|
40
|
+
const showFailures = useFailurePopup();
|
|
33
41
|
const { navigateToScreen } = useNavigation();
|
|
34
42
|
const dimensions = useDimensions();
|
|
35
43
|
|
|
@@ -69,9 +77,12 @@ export function McpScreen() {
|
|
|
69
77
|
}
|
|
70
78
|
}, [dispatch, state.projectPath]);
|
|
71
79
|
|
|
80
|
+
// Load data on mount or when dataRefreshVersion changes — a profile
|
|
81
|
+
// activating changes what this screen shows.
|
|
82
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: dataRefreshVersion is the external refresh trigger; bumping it must re-run the fetch even though the body never reads it
|
|
72
83
|
useEffect(() => {
|
|
73
84
|
fetchData();
|
|
74
|
-
}, [fetchData]);
|
|
85
|
+
}, [fetchData, state.dataRefreshVersion]);
|
|
75
86
|
|
|
76
87
|
const allListItems = useMemo((): McpListItem[] => {
|
|
77
88
|
if (mcp.servers.status !== "success") return [];
|
|
@@ -87,22 +98,34 @@ export function McpScreen() {
|
|
|
87
98
|
|
|
88
99
|
for (const server of servers) {
|
|
89
100
|
const isInstalled = mcp.installedServers[server.name] !== undefined;
|
|
90
|
-
items.push({
|
|
101
|
+
items.push({
|
|
102
|
+
kind: "server",
|
|
103
|
+
server,
|
|
104
|
+
isInstalled,
|
|
105
|
+
needsRestart: mcp.changedThisSession.includes(server.name),
|
|
106
|
+
});
|
|
91
107
|
}
|
|
92
108
|
}
|
|
93
109
|
|
|
94
110
|
return items;
|
|
95
|
-
}, [mcp.servers, mcp.installedServers]);
|
|
111
|
+
}, [mcp.servers, mcp.installedServers, mcp.changedThisSession]);
|
|
96
112
|
|
|
97
113
|
useScreenKeys("mcp", {
|
|
98
114
|
// Guarded: `installMcpServer` throws on a catalogue entry with no URL or
|
|
99
115
|
// command, and a rejection nobody catches ends the app.
|
|
100
116
|
toggle: () => {
|
|
117
|
+
const item = allListItems[mcp.selectedIndex];
|
|
118
|
+
if (item?.kind !== "server") return;
|
|
119
|
+
const { name } = item.server;
|
|
101
120
|
handleSelect().catch((error: unknown) =>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
showFailures(
|
|
122
|
+
stepFailureReport(
|
|
123
|
+
item.isInstalled
|
|
124
|
+
? `Could not remove ${name}`
|
|
125
|
+
: `Could not add ${name}`,
|
|
126
|
+
name,
|
|
127
|
+
error,
|
|
128
|
+
),
|
|
106
129
|
),
|
|
107
130
|
);
|
|
108
131
|
},
|
|
@@ -138,15 +161,19 @@ export function McpScreen() {
|
|
|
138
161
|
try {
|
|
139
162
|
await removeMcpServer(server.name, state.projectPath);
|
|
140
163
|
modal.hideModal();
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
"success",
|
|
145
|
-
);
|
|
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 });
|
|
146
167
|
fetchData();
|
|
147
168
|
} catch (error) {
|
|
148
169
|
modal.hideModal();
|
|
149
|
-
await
|
|
170
|
+
await showFailures(
|
|
171
|
+
stepFailureReport(
|
|
172
|
+
`Could not remove ${server.name}`,
|
|
173
|
+
server.name,
|
|
174
|
+
error,
|
|
175
|
+
),
|
|
176
|
+
);
|
|
150
177
|
}
|
|
151
178
|
}
|
|
152
179
|
} else {
|
|
@@ -197,10 +224,14 @@ export function McpScreen() {
|
|
|
197
224
|
if (value === null) return;
|
|
198
225
|
|
|
199
226
|
if (field.required && !value) {
|
|
200
|
-
await
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
]),
|
|
204
235
|
);
|
|
205
236
|
return;
|
|
206
237
|
}
|
|
@@ -216,15 +247,15 @@ export function McpScreen() {
|
|
|
216
247
|
try {
|
|
217
248
|
await addMcpServer(server.name, config, state.projectPath);
|
|
218
249
|
modal.hideModal();
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
"success",
|
|
223
|
-
);
|
|
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 });
|
|
224
253
|
fetchData();
|
|
225
254
|
} catch (error) {
|
|
226
255
|
modal.hideModal();
|
|
227
|
-
await
|
|
256
|
+
await showFailures(
|
|
257
|
+
stepFailureReport(`Could not add ${server.name}`, server.name, error),
|
|
258
|
+
);
|
|
228
259
|
}
|
|
229
260
|
};
|
|
230
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 -
|