magus-cli 7.3.1 → 7.4.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__/blind/c1-npm-package.test.ts +206 -0
- package/src/__tests__/blind/c10-colour-roles.test.ts +265 -0
- package/src/__tests__/blind/c11-cli-parse-refusal.test.ts +454 -0
- package/src/__tests__/blind/c12-skill-frontmatter.test.ts +323 -0
- package/src/__tests__/blind/c13-wrong-shape.test.ts +603 -0
- package/src/__tests__/blind/c14-cli-row-label.test.ts +259 -0
- package/src/__tests__/blind/c15-probe-ownership.test.ts +533 -0
- package/src/__tests__/blind/c16-nested-wrong-shape.test.ts +297 -0
- package/src/__tests__/blind/c17-enter-label.test.ts +625 -0
- package/src/__tests__/blind/c18-hook-shape.test.ts +354 -0
- package/src/__tests__/blind/c2-settings-parse.test.ts +350 -0
- package/src/__tests__/blind/c4-key-dispatch.test.ts +397 -0
- package/src/__tests__/blind/c5-keymap.test.ts +461 -0
- package/src/__tests__/blind/c6-request-gate.test.ts +133 -0
- package/src/__tests__/blind/c7-tab-tiers.test.ts +320 -0
- package/src/__tests__/blind/c8-text-width.test.ts +137 -0
- package/src/__tests__/blind/c9-cli-tool-version.test.ts +239 -0
- package/src/__tests__/blind/child-run.ts +114 -0
- package/src/__tests__/cli-tools-adapter.test.ts +5 -2
- package/src/__tests__/component-badge.test.ts +20 -36
- package/src/__tests__/footer-hints.test.ts +1 -1
- package/src/__tests__/manifest.test.ts +25 -17
- package/src/__tests__/marketplace-badge.test.ts +4 -4
- package/src/__tests__/models-screen-state.test.ts +36 -13
- package/src/__tests__/scope-squares.test.tsx +28 -114
- package/src/__tests__/shutdown.test.ts +578 -0
- package/src/__tests__/styles-manager.test.ts +4 -4
- package/src/__tests__/styles-screen-state.test.ts +26 -12
- package/src/__tests__/tabbar-layout.test.ts +44 -32
- package/src/__tests__/theme-adaptive-colors.test.ts +6 -59
- package/src/__tests__/tool-probe.test.ts +47 -22
- package/src/__tests__/uppercase-keybindings.test.ts +25 -12
- package/src/cli/ansi.ts +2 -2
- package/src/cli/bootstrap.ts +33 -2
- package/src/cli/doctor.ts +105 -14
- package/src/cli/install.ts +8 -2
- package/src/cli/profile.ts +18 -1
- package/src/cli/router.ts +31 -18
- package/src/cli/update-view.ts +1 -1
- package/src/data/cli-tools.ts +9 -1
- package/src/services/claude-settings.ts +151 -73
- package/src/services/cli-tool-commands.ts +15 -5
- package/src/services/cli-tool-version.ts +101 -0
- package/src/services/hook-registration.ts +23 -6
- package/src/services/manifest.ts +117 -52
- package/src/services/plugin-manager.ts +19 -5
- package/src/services/profile-adopt.ts +7 -2
- package/src/services/profile-startup.ts +87 -7
- package/src/services/profiles.ts +36 -23
- package/src/services/settings-file.ts +158 -4
- package/src/services/settings-manager.ts +19 -5
- package/src/services/skills-manager.ts +46 -0
- package/src/services/tool-probe.ts +272 -45
- package/src/services/toolchain.ts +39 -0
- package/src/services/update-engine.ts +5 -0
- package/src/services/update-plan.ts +9 -0
- package/src/tui.tsx +48 -21
- package/src/ui/App.tsx +58 -197
- package/src/ui/adapters/cliToolsAdapter.ts +141 -12
- package/src/ui/adapters/pluginsAdapter.ts +10 -7
- package/src/ui/adapters/settingsAdapter.ts +0 -21
- package/src/ui/adapters/skillsAdapter.ts +27 -8
- package/src/ui/adapters/stylesAdapter.ts +15 -11
- package/src/ui/components/CategoryHeader.tsx +7 -1
- package/src/ui/components/EmptyFilterState.tsx +2 -2
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScreenErrorBoundary.tsx +105 -0
- package/src/ui/components/TabBar.tsx +93 -21
- package/src/ui/components/layout/FooterHints.tsx +6 -27
- package/src/ui/components/layout/ScreenLayout.tsx +68 -42
- package/src/ui/components/layout/index.ts +1 -5
- package/src/ui/components/modals/ConfirmModal.tsx +2 -19
- package/src/ui/components/modals/HelpOverlay.tsx +186 -0
- package/src/ui/components/modals/MessageModal.tsx +3 -15
- package/src/ui/components/modals/ModalContainer.tsx +99 -60
- package/src/ui/components/primitives/ErrorNotice.tsx +38 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +18 -7
- package/src/ui/components/primitives/MetaText.tsx +1 -1
- package/src/ui/components/primitives/ScopeDetail.tsx +31 -26
- package/src/ui/components/primitives/ScopeSquares.tsx +23 -28
- package/src/ui/components/primitives/SelectableRow.tsx +24 -1
- package/src/ui/components/primitives/index.ts +4 -3
- package/src/ui/hooks/useProfileStartupModal.ts +15 -3
- package/src/ui/input/KeyRouter.tsx +180 -0
- package/src/ui/input/key-dispatch.ts +149 -0
- package/src/ui/keymap.ts +898 -0
- package/src/ui/renderers/cliToolRenderers.tsx +128 -66
- package/src/ui/renderers/gitignoreRenderers.tsx +41 -43
- package/src/ui/renderers/markdownRenderer.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +28 -14
- package/src/ui/renderers/modelRenderers.tsx +12 -13
- package/src/ui/renderers/modelVisuals.tsx +0 -2
- package/src/ui/renderers/pluginRenderers.tsx +47 -54
- package/src/ui/renderers/profileRenderers.tsx +32 -37
- package/src/ui/renderers/settingsRenderers.tsx +84 -55
- package/src/ui/renderers/skillRenderers.tsx +237 -190
- package/src/ui/renderers/styleRenderers.tsx +13 -2
- package/src/ui/runtime/shutdown.ts +268 -0
- package/src/ui/screens/AliasScreen.tsx +118 -100
- package/src/ui/screens/CliToolsScreen.tsx +98 -87
- package/src/ui/screens/EnvVarsScreen.tsx +44 -42
- package/src/ui/screens/GitignoreScreen.tsx +50 -55
- package/src/ui/screens/McpRegistryScreen.tsx +52 -86
- package/src/ui/screens/McpScreen.tsx +47 -33
- package/src/ui/screens/ModelsScreen.tsx +59 -104
- package/src/ui/screens/PluginsScreen.tsx +123 -127
- package/src/ui/screens/ProfilesScreen.tsx +49 -46
- package/src/ui/screens/SkillsScreen.tsx +184 -204
- package/src/ui/screens/StylesScreen.tsx +104 -157
- package/src/ui/state/AppContext.tsx +6 -0
- package/src/ui/state/DimensionsContext.tsx +1 -10
- package/src/ui/state/reducer.ts +5 -8
- package/src/ui/state/types.ts +11 -9
- package/src/ui/text-width.ts +341 -0
- package/src/ui/theme-mode.ts +34 -71
- package/src/ui/theme.ts +50 -28
- package/src/utils/request-gate.ts +37 -0
- package/src/ui/components/ScopeIndicator.tsx +0 -63
- package/src/ui/components/SearchInput.tsx +0 -57
- package/src/ui/hooks/index.ts +0 -2
- package/src/ui/hooks/useKeyboard.ts +0 -24
- package/src/ui/hooks/useKeyboardHandler.ts +0 -63
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useCallback, useState, useRef } from "react";
|
|
2
2
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { useScreenKeys, useSearchInput } from "../input/KeyRouter.js";
|
|
4
|
+
import { moveDirection } from "../keymap.js";
|
|
4
5
|
import { useApp, useModal, useNavigation } from "../state/AppContext.js";
|
|
5
6
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
6
7
|
|
|
@@ -8,6 +9,7 @@ import { addMcpServer, setAllowMcp } from "../../services/claude-settings.js";
|
|
|
8
9
|
import { formatDate, searchMcpServers } from "../../services/mcp-registry.js";
|
|
9
10
|
import type { McpRegistryServer, McpServerConfig } from "../../types/index.js";
|
|
10
11
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
12
|
+
import { rowLead } from "../components/primitives/index.js";
|
|
11
13
|
import { theme } from "../theme.js";
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -82,9 +84,7 @@ export function McpRegistryScreen() {
|
|
|
82
84
|
const [searchQuery, setSearchQuery] = useState(mcpRegistry.searchQuery || "");
|
|
83
85
|
|
|
84
86
|
const isSearchActive =
|
|
85
|
-
state.isSearching &&
|
|
86
|
-
state.currentRoute.screen === "mcp-registry" &&
|
|
87
|
-
!state.modal;
|
|
87
|
+
state.isSearching && state.currentRoute.screen === "mcp-registry";
|
|
88
88
|
|
|
89
89
|
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
90
90
|
|
|
@@ -132,67 +132,48 @@ export function McpRegistryScreen() {
|
|
|
132
132
|
};
|
|
133
133
|
}, [loadServers]);
|
|
134
134
|
|
|
135
|
-
//
|
|
136
|
-
useKeyboard((event) => {
|
|
137
|
-
// Handle search mode
|
|
138
|
-
if (isSearchActive) {
|
|
139
|
-
if (event.name === "escape") {
|
|
140
|
-
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
141
|
-
} else if (event.name === "enter") {
|
|
142
|
-
// Exit search mode and stay on list for navigation
|
|
143
|
-
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
144
|
-
} else if (event.name === "up") {
|
|
145
|
-
// Allow navigation while searching
|
|
146
|
-
const newIndex = Math.max(0, mcpRegistry.selectedIndex - 1);
|
|
147
|
-
dispatch({ type: "MCPREGISTRY_SELECT", index: newIndex });
|
|
148
|
-
} else if (event.name === "down") {
|
|
149
|
-
// Allow navigation while searching
|
|
150
|
-
const newIndex = Math.min(
|
|
151
|
-
Math.max(0, servers.length - 1),
|
|
152
|
-
mcpRegistry.selectedIndex + 1,
|
|
153
|
-
);
|
|
154
|
-
dispatch({ type: "MCPREGISTRY_SELECT", index: newIndex });
|
|
155
|
-
} else if (event.name === "backspace" || event.name === "delete") {
|
|
156
|
-
const newQuery = searchQuery.slice(0, -1);
|
|
157
|
-
setSearchQuery(newQuery);
|
|
158
|
-
dispatch({ type: "MCPREGISTRY_SEARCH", query: newQuery });
|
|
159
|
-
debouncedSearch(newQuery);
|
|
160
|
-
} else if (event.name.length === 1 && !event.ctrl && !event.meta) {
|
|
161
|
-
const newQuery = searchQuery + event.name;
|
|
162
|
-
setSearchQuery(newQuery);
|
|
163
|
-
dispatch({ type: "MCPREGISTRY_SEARCH", query: newQuery });
|
|
164
|
-
debouncedSearch(newQuery);
|
|
165
|
-
}
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
135
|
+
// ── Keys ──────────────────────────────────────────────────────────────────
|
|
168
136
|
|
|
169
|
-
|
|
137
|
+
/** A new query: the list reloads, debounced, and the cursor starts over. */
|
|
138
|
+
const setQuery = (query: string) => {
|
|
139
|
+
setSearchQuery(query);
|
|
140
|
+
dispatch({ type: "MCPREGISTRY_SEARCH", query });
|
|
141
|
+
dispatch({ type: "MCPREGISTRY_SELECT", index: 0 });
|
|
142
|
+
debouncedSearch(query);
|
|
143
|
+
};
|
|
170
144
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
145
|
+
// The search box, while it has focus: digits, j/k, q and ? are typed here.
|
|
146
|
+
useSearchInput({
|
|
147
|
+
focused: isSearchActive,
|
|
148
|
+
query: searchQuery,
|
|
149
|
+
append: (char) => setQuery(searchQuery + char),
|
|
150
|
+
erase: () => setQuery(searchQuery.slice(0, -1)),
|
|
151
|
+
clear: () => setQuery(""),
|
|
152
|
+
blur: () => dispatch({ type: "SET_SEARCHING", isSearching: false }),
|
|
153
|
+
});
|
|
176
154
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
155
|
+
useScreenKeys("mcp-registry", {
|
|
156
|
+
install: () => {
|
|
157
|
+
void handleInstall();
|
|
158
|
+
},
|
|
159
|
+
search: () => dispatch({ type: "SET_SEARCHING", isSearching: true }),
|
|
160
|
+
local: () => navigateToScreen("mcp"),
|
|
161
|
+
reload: () => {
|
|
162
|
+
void loadServers(searchQuery);
|
|
163
|
+
},
|
|
164
|
+
// Only while a query is set; otherwise Esc is the global back to MCP.
|
|
165
|
+
"clear-filter": () => {
|
|
166
|
+
if (!searchQuery) return false;
|
|
167
|
+
setQuery("");
|
|
168
|
+
return true;
|
|
169
|
+
},
|
|
170
|
+
move: (key) => {
|
|
171
|
+
const index = mcpRegistry.selectedIndex + moveDirection(key);
|
|
172
|
+
dispatch({
|
|
173
|
+
type: "MCPREGISTRY_SELECT",
|
|
174
|
+
index: Math.max(0, Math.min(servers.length - 1, index)),
|
|
175
|
+
});
|
|
176
|
+
},
|
|
196
177
|
});
|
|
197
178
|
|
|
198
179
|
const handleInstall = async () => {
|
|
@@ -254,13 +235,13 @@ export function McpRegistryScreen() {
|
|
|
254
235
|
<box marginTop={1}>
|
|
255
236
|
<text fg={theme.colors.text}>{selectedServer.short_description}</text>
|
|
256
237
|
</box>
|
|
257
|
-
<box marginTop={1}>
|
|
238
|
+
<box flexDirection="row" marginTop={1}>
|
|
258
239
|
<text fg={theme.colors.text}>
|
|
259
240
|
<strong>Version: </strong>
|
|
260
241
|
</text>
|
|
261
|
-
<text fg={theme.colors.
|
|
242
|
+
<text fg={theme.colors.info}>{versionDisplay}</text>
|
|
262
243
|
</box>
|
|
263
|
-
<box>
|
|
244
|
+
<box flexDirection="row">
|
|
264
245
|
<text fg={theme.colors.text}>
|
|
265
246
|
<strong>Published: </strong>
|
|
266
247
|
</text>
|
|
@@ -283,7 +264,7 @@ export function McpRegistryScreen() {
|
|
|
283
264
|
</box>
|
|
284
265
|
)}
|
|
285
266
|
<box marginTop={1}>
|
|
286
|
-
<text fg={theme.colors.
|
|
267
|
+
<text fg={theme.colors.muted}>Press Enter to install</text>
|
|
287
268
|
</box>
|
|
288
269
|
</box>
|
|
289
270
|
);
|
|
@@ -296,37 +277,22 @@ export function McpRegistryScreen() {
|
|
|
296
277
|
) => {
|
|
297
278
|
const version = server.version ? ` v${server.version}` : "";
|
|
298
279
|
return isSelected ? (
|
|
299
|
-
<text bg={theme.
|
|
300
|
-
{
|
|
280
|
+
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
281
|
+
{rowLead(true)}
|
|
301
282
|
{server.name}
|
|
302
283
|
{version}{" "}
|
|
303
284
|
</text>
|
|
304
285
|
) : (
|
|
305
286
|
<text fg={theme.colors.text}>
|
|
287
|
+
<span>{rowLead(false)}</span>
|
|
306
288
|
<span>
|
|
307
289
|
<strong>{server.name}</strong>
|
|
308
290
|
</span>
|
|
309
|
-
<span fg={theme.colors.
|
|
291
|
+
<span fg={theme.colors.info}>{version}</span>
|
|
310
292
|
</text>
|
|
311
293
|
);
|
|
312
294
|
};
|
|
313
295
|
|
|
314
|
-
// Footer hints
|
|
315
|
-
const footerHints = isSearchActive
|
|
316
|
-
? [
|
|
317
|
-
{ keys: ["type"], label: "search" },
|
|
318
|
-
{ keys: ["↑", "↓"], label: "nav" },
|
|
319
|
-
{ keys: ["Enter"], label: "done" },
|
|
320
|
-
{ keys: ["Esc"], label: "cancel" },
|
|
321
|
-
]
|
|
322
|
-
: [
|
|
323
|
-
{ keys: ["↑", "↓"], label: "nav" },
|
|
324
|
-
{ keys: ["Enter"], label: "install" },
|
|
325
|
-
{ keys: ["/"], label: "search" },
|
|
326
|
-
{ keys: ["R"], label: "refresh" },
|
|
327
|
-
{ keys: ["l"], label: "local" },
|
|
328
|
-
];
|
|
329
|
-
|
|
330
296
|
// Status for search placeholder
|
|
331
297
|
const searchPlaceholder = `${servers.length} servers │ / to search`;
|
|
332
298
|
|
|
@@ -340,7 +306,7 @@ export function McpRegistryScreen() {
|
|
|
340
306
|
query: searchQuery,
|
|
341
307
|
placeholder: searchPlaceholder,
|
|
342
308
|
}}
|
|
343
|
-
|
|
309
|
+
footer={{ hide: searchQuery ? [] : ["clear-filter"] }}
|
|
344
310
|
listPanel={
|
|
345
311
|
isLoading ? (
|
|
346
312
|
<text fg={theme.colors.muted}>Loading...</text>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useCallback, useMemo } from "react";
|
|
2
2
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { useScreenKeys } from "../input/KeyRouter.js";
|
|
4
|
+
import { moveDirection } from "../keymap.js";
|
|
4
5
|
import { useApp, useModal, useNavigation } from "../state/AppContext.js";
|
|
5
6
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
6
7
|
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
import { fetchCuratedMcpServers } from "../../services/mcp-registry.js";
|
|
19
20
|
import type { McpServer, McpServerConfig } from "../../types/index.js";
|
|
20
21
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
22
|
+
import { ErrorNotice } from "../components/primitives/index.js";
|
|
21
23
|
import {
|
|
22
24
|
type McpListItem,
|
|
23
25
|
renderMcpDetail,
|
|
@@ -92,26 +94,32 @@ export function McpScreen() {
|
|
|
92
94
|
return items;
|
|
93
95
|
}, [mcp.servers, mcp.installedServers]);
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
} else if (event.name === "r") {
|
|
97
|
+
useScreenKeys("mcp", {
|
|
98
|
+
// Guarded: `installMcpServer` throws on a catalogue entry with no URL or
|
|
99
|
+
// command, and a rejection nobody catches ends the app.
|
|
100
|
+
toggle: () => {
|
|
101
|
+
handleSelect().catch((error: unknown) =>
|
|
102
|
+
modal.message(
|
|
103
|
+
"Error",
|
|
104
|
+
error instanceof Error ? error.message : String(error),
|
|
105
|
+
"error",
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
// Navigate FIRST: a navigation closes any search box, so opening the
|
|
110
|
+
// registry's box has to come after it.
|
|
111
|
+
search: () => {
|
|
111
112
|
navigateToScreen("mcp-registry");
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
114
|
+
},
|
|
115
|
+
registry: () => navigateToScreen("mcp-registry"),
|
|
116
|
+
move: (key) => {
|
|
117
|
+
const index = mcp.selectedIndex + moveDirection(key);
|
|
118
|
+
dispatch({
|
|
119
|
+
type: "MCP_SELECT",
|
|
120
|
+
index: Math.max(0, Math.min(allListItems.length - 1, index)),
|
|
121
|
+
});
|
|
122
|
+
},
|
|
115
123
|
});
|
|
116
124
|
|
|
117
125
|
const handleSelect = async () => {
|
|
@@ -227,26 +235,32 @@ export function McpScreen() {
|
|
|
227
235
|
const enabledCount = Object.values(mcp.installedServers).filter(
|
|
228
236
|
(v) => v === true,
|
|
229
237
|
).length;
|
|
230
|
-
|
|
238
|
+
// A failed read is not "0 configured": the counts are only a fact once the
|
|
239
|
+
// configuration has actually been read.
|
|
240
|
+
const loadError = mcp.servers.status === "error" ? mcp.servers.error : null;
|
|
241
|
+
const subtitle = loadError
|
|
242
|
+
? "configuration unreadable"
|
|
243
|
+
: `${enabledCount} enabled │ ${installedCount} configured │ / to search`;
|
|
231
244
|
|
|
232
245
|
return (
|
|
233
246
|
<ScreenLayout
|
|
234
247
|
title="magus MCP Servers"
|
|
235
248
|
subtitle={subtitle}
|
|
236
249
|
currentScreen="mcp"
|
|
237
|
-
footerHints={[
|
|
238
|
-
{ keys: ["↑", "↓"], label: "nav" },
|
|
239
|
-
{ keys: ["Enter"], label: "toggle" },
|
|
240
|
-
{ keys: ["/"], label: "search" },
|
|
241
|
-
{ keys: ["r"], label: "registry" },
|
|
242
|
-
]}
|
|
243
250
|
listPanel={
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
loadError ? (
|
|
252
|
+
<ErrorNotice
|
|
253
|
+
title="Could not load MCP servers."
|
|
254
|
+
detail={loadError.message}
|
|
255
|
+
/>
|
|
256
|
+
) : (
|
|
257
|
+
<ScrollableList
|
|
258
|
+
items={allListItems}
|
|
259
|
+
selectedIndex={mcp.selectedIndex}
|
|
260
|
+
renderItem={renderMcpRow}
|
|
261
|
+
maxHeight={dimensions.listPanelHeight}
|
|
262
|
+
/>
|
|
263
|
+
)
|
|
250
264
|
}
|
|
251
265
|
detailPanel={renderMcpDetail(selectedItem, isLoading)}
|
|
252
266
|
/>
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
16
16
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
17
17
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
18
|
-
import {
|
|
18
|
+
import { useScreenKeys, useSearchInput } from "../input/KeyRouter.js";
|
|
19
|
+
import { moveDirection } from "../keymap.js";
|
|
19
20
|
import {
|
|
20
21
|
fitPresetList,
|
|
21
22
|
renderModelDetail,
|
|
@@ -33,7 +34,7 @@ export function ModelsScreen() {
|
|
|
33
34
|
const dimensions = useDimensions();
|
|
34
35
|
|
|
35
36
|
const isSearchActive =
|
|
36
|
-
state.isSearching && state.currentRoute.screen === "models"
|
|
37
|
+
state.isSearching && state.currentRoute.screen === "models";
|
|
37
38
|
|
|
38
39
|
// ── Data ──────────────────────────────────────────────────────────────────
|
|
39
40
|
|
|
@@ -287,94 +288,61 @@ export function ModelsScreen() {
|
|
|
287
288
|
[modelsState.selectedIndex, allItems, dispatch],
|
|
288
289
|
);
|
|
289
290
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const hasQuery = modelsState.searchQuery.length > 0;
|
|
304
|
-
|
|
305
|
-
if (event.name === "escape") {
|
|
306
|
-
if (hasQuery || isSearchActive) {
|
|
307
|
-
dispatch({ type: "MODELS_SET_SEARCH", query: "" });
|
|
308
|
-
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
309
|
-
dispatch({ type: "MODELS_SELECT", index: 0 });
|
|
310
|
-
}
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (event.name === "backspace" || event.name === "delete") {
|
|
315
|
-
if (hasQuery) {
|
|
316
|
-
dispatch({ type: "MODELS_SEARCH_BACKSPACE" });
|
|
317
|
-
// Driven by what was there BEFORE this keystroke — the one thing the
|
|
318
|
-
// closure knows reliably.
|
|
319
|
-
if (modelsState.searchQuery.length <= 1) {
|
|
320
|
-
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (event.name === "up") {
|
|
327
|
-
if (isSearchActive)
|
|
328
|
-
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
329
|
-
move(-1);
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
if (event.name === "down") {
|
|
333
|
-
if (isSearchActive)
|
|
334
|
-
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
335
|
-
move(1);
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (event.name === "return" || event.name === "enter") {
|
|
340
|
-
if (isSearchActive) {
|
|
341
|
-
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
// Enter and `a` are the same action here. There is nothing to "open" on
|
|
345
|
-
// this screen — a preset row has one thing it can do — so binding Enter
|
|
346
|
-
// to anything else would leave the most obvious key doing nothing.
|
|
347
|
-
handleApply();
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// While typing a filter, letters are text — not commands.
|
|
352
|
-
if (isSearchActive) {
|
|
353
|
-
if (
|
|
354
|
-
event.name &&
|
|
355
|
-
event.name.length === 1 &&
|
|
356
|
-
!event.ctrl &&
|
|
357
|
-
!event.meta &&
|
|
358
|
-
!/[0-9]/.test(event.name)
|
|
359
|
-
) {
|
|
360
|
-
dispatch({ type: "MODELS_SEARCH_APPEND", char: event.name });
|
|
361
|
-
}
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
291
|
+
// The search box, while it has focus: digits, j/k, q and ? are typed here.
|
|
292
|
+
useSearchInput({
|
|
293
|
+
focused: isSearchActive,
|
|
294
|
+
query: modelsState.searchQuery,
|
|
295
|
+
append: (char) => dispatch({ type: "MODELS_SEARCH_APPEND", char }),
|
|
296
|
+
erase: () => dispatch({ type: "MODELS_SEARCH_BACKSPACE" }),
|
|
297
|
+
clear: () => {
|
|
298
|
+
dispatch({ type: "MODELS_SET_SEARCH", query: "" });
|
|
299
|
+
dispatch({ type: "MODELS_SELECT", index: 0 });
|
|
300
|
+
},
|
|
301
|
+
blur: () => dispatch({ type: "SET_SEARCHING", isSearching: false }),
|
|
302
|
+
});
|
|
364
303
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
+
useScreenKeys("models", {
|
|
315
|
+
// Enter and `a` are the same action here. There is nothing to "open" on
|
|
316
|
+
// this screen — a preset row has one thing it can do — so binding Enter
|
|
317
|
+
// to anything else would leave the most obvious key doing nothing.
|
|
318
|
+
apply: () => {
|
|
319
|
+
clearStatus();
|
|
320
|
+
void handleApply();
|
|
321
|
+
},
|
|
322
|
+
off: () => {
|
|
323
|
+
clearStatus();
|
|
324
|
+
void handleClear();
|
|
325
|
+
},
|
|
326
|
+
reload: () => {
|
|
327
|
+
clearStatus();
|
|
328
|
+
void fetchData();
|
|
329
|
+
},
|
|
330
|
+
search: () => {
|
|
331
|
+
clearStatus();
|
|
376
332
|
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
377
|
-
}
|
|
333
|
+
},
|
|
334
|
+
// Only while a committed filter is set; otherwise Esc is the global back.
|
|
335
|
+
"clear-filter": () => {
|
|
336
|
+
if (!modelsState.searchQuery) return false;
|
|
337
|
+
clearStatus();
|
|
338
|
+
dispatch({ type: "MODELS_SET_SEARCH", query: "" });
|
|
339
|
+
dispatch({ type: "MODELS_SELECT", index: 0 });
|
|
340
|
+
return true;
|
|
341
|
+
},
|
|
342
|
+
move: (key) => {
|
|
343
|
+
clearStatus();
|
|
344
|
+
move(moveDirection(key));
|
|
345
|
+
},
|
|
378
346
|
});
|
|
379
347
|
|
|
380
348
|
// ── Chrome ────────────────────────────────────────────────────────────────
|
|
@@ -413,7 +381,7 @@ export function ModelsScreen() {
|
|
|
413
381
|
{routingState ?? "reading…"}
|
|
414
382
|
</span>
|
|
415
383
|
{modelsState.isApplying ? (
|
|
416
|
-
<span fg={theme.colors.
|
|
384
|
+
<span fg={theme.colors.muted}> │ writing…</span>
|
|
417
385
|
) : null}
|
|
418
386
|
</text>
|
|
419
387
|
);
|
|
@@ -440,20 +408,7 @@ export function ModelsScreen() {
|
|
|
440
408
|
}
|
|
441
409
|
: undefined
|
|
442
410
|
}
|
|
443
|
-
|
|
444
|
-
isSearchActive
|
|
445
|
-
? [
|
|
446
|
-
{ keys: ["type"], label: "filter" },
|
|
447
|
-
{ keys: ["Enter"], label: "done" },
|
|
448
|
-
{ keys: ["Esc"], label: "clear" },
|
|
449
|
-
]
|
|
450
|
-
: [
|
|
451
|
-
{ keys: ["Enter", "a"], label: "apply" },
|
|
452
|
-
{ keys: ["c"], label: "off" },
|
|
453
|
-
{ keys: ["r"], label: "reload" },
|
|
454
|
-
{ keys: ["/"], label: "filter" },
|
|
455
|
-
]
|
|
456
|
-
}
|
|
411
|
+
footer={{ hide: modelsState.searchQuery ? [] : ["clear-filter"] }}
|
|
457
412
|
listPanel={(listWidth) => {
|
|
458
413
|
// ONE layout for every row, measured from the widest of them.
|
|
459
414
|
// Rows used to fit themselves, so at 120×30 the selected row
|
|
@@ -464,7 +419,7 @@ export function ModelsScreen() {
|
|
|
464
419
|
<box flexDirection="column">
|
|
465
420
|
{modelsState.data.status === "loading" && !snapshot && (
|
|
466
421
|
<box paddingLeft={2}>
|
|
467
|
-
<text fg={theme.colors.
|
|
422
|
+
<text fg={theme.colors.muted}>Reading the routing…</text>
|
|
468
423
|
</box>
|
|
469
424
|
)}
|
|
470
425
|
|