claudeup 3.7.1 → 3.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 +1 -1
- package/src/data/settings-catalog.js +612 -0
- package/src/data/settings-catalog.ts +689 -0
- package/src/prerunner/index.js +2 -1
- package/src/prerunner/index.ts +2 -0
- package/src/services/plugin-manager.js +2 -0
- package/src/services/plugin-manager.ts +3 -0
- package/src/services/profiles.js +161 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/settings-manager.js +108 -0
- package/src/services/settings-manager.ts +140 -0
- package/src/types/index.ts +34 -0
- package/src/ui/App.js +17 -18
- package/src/ui/App.tsx +21 -23
- package/src/ui/components/TabBar.js +8 -8
- package/src/ui/components/TabBar.tsx +14 -19
- package/src/ui/components/layout/ScreenLayout.js +8 -14
- package/src/ui/components/layout/ScreenLayout.tsx +51 -58
- package/src/ui/components/modals/ModalContainer.js +43 -11
- package/src/ui/components/modals/ModalContainer.tsx +44 -12
- package/src/ui/components/modals/SelectModal.js +4 -18
- package/src/ui/components/modals/SelectModal.tsx +10 -21
- package/src/ui/screens/CliToolsScreen.js +2 -2
- package/src/ui/screens/CliToolsScreen.tsx +8 -8
- package/src/ui/screens/EnvVarsScreen.js +248 -116
- package/src/ui/screens/EnvVarsScreen.tsx +419 -184
- package/src/ui/screens/McpRegistryScreen.tsx +18 -6
- package/src/ui/screens/McpScreen.js +1 -1
- package/src/ui/screens/McpScreen.tsx +15 -5
- package/src/ui/screens/ModelSelectorScreen.js +3 -5
- package/src/ui/screens/ModelSelectorScreen.tsx +12 -16
- package/src/ui/screens/PluginsScreen.js +181 -65
- package/src/ui/screens/PluginsScreen.tsx +308 -91
- package/src/ui/screens/ProfilesScreen.js +255 -0
- package/src/ui/screens/ProfilesScreen.tsx +487 -0
- package/src/ui/screens/StatusLineScreen.js +2 -2
- package/src/ui/screens/StatusLineScreen.tsx +10 -12
- package/src/ui/screens/index.js +2 -2
- package/src/ui/screens/index.ts +2 -2
- package/src/ui/state/AppContext.js +2 -1
- package/src/ui/state/AppContext.tsx +2 -0
- package/src/ui/state/reducer.js +63 -19
- package/src/ui/state/reducer.ts +68 -19
- package/src/ui/state/types.ts +33 -14
- package/src/utils/clipboard.js +56 -0
- package/src/utils/clipboard.ts +58 -0
|
@@ -239,25 +239,35 @@ export function McpRegistryScreen() {
|
|
|
239
239
|
|
|
240
240
|
return (
|
|
241
241
|
<box flexDirection="column">
|
|
242
|
-
<text fg="magenta"
|
|
242
|
+
<text fg="magenta">
|
|
243
|
+
<strong>{selectedServer.name}</strong>
|
|
244
|
+
</text>
|
|
243
245
|
<box marginTop={1}>
|
|
244
246
|
<text>{selectedServer.short_description}</text>
|
|
245
247
|
</box>
|
|
246
248
|
<box marginTop={1}>
|
|
247
|
-
<text
|
|
249
|
+
<text>
|
|
250
|
+
<strong>Version: </strong>
|
|
251
|
+
</text>
|
|
248
252
|
<text fg="green">{versionDisplay}</text>
|
|
249
253
|
</box>
|
|
250
254
|
<box>
|
|
251
|
-
<text
|
|
255
|
+
<text>
|
|
256
|
+
<strong>Published: </strong>
|
|
257
|
+
</text>
|
|
252
258
|
<text fg="cyan">{dateDisplay}</text>
|
|
253
259
|
</box>
|
|
254
260
|
<box marginTop={1} flexDirection="column">
|
|
255
|
-
<text
|
|
261
|
+
<text>
|
|
262
|
+
<strong>URL:</strong>
|
|
263
|
+
</text>
|
|
256
264
|
<text fg="cyan">{selectedServer.url}</text>
|
|
257
265
|
</box>
|
|
258
266
|
{selectedServer.source_code_url && (
|
|
259
267
|
<box marginTop={1} flexDirection="column">
|
|
260
|
-
<text
|
|
268
|
+
<text>
|
|
269
|
+
<strong>Source:</strong>
|
|
270
|
+
</text>
|
|
261
271
|
<text fg="gray">{selectedServer.source_code_url}</text>
|
|
262
272
|
</box>
|
|
263
273
|
)}
|
|
@@ -282,7 +292,9 @@ export function McpRegistryScreen() {
|
|
|
282
292
|
</text>
|
|
283
293
|
) : (
|
|
284
294
|
<text>
|
|
285
|
-
<span
|
|
295
|
+
<span>
|
|
296
|
+
<strong>{server.name}</strong>
|
|
297
|
+
</span>
|
|
286
298
|
<span fg="green">{version}</span>
|
|
287
299
|
</text>
|
|
288
300
|
);
|
|
@@ -198,7 +198,7 @@ export function McpScreen() {
|
|
|
198
198
|
const server = selectedItem.server;
|
|
199
199
|
const isInstalled = mcp.installedServers[server.name] !== undefined;
|
|
200
200
|
const isEnabled = mcp.installedServers[server.name] === true;
|
|
201
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsxs("box", { marginBottom: 1, children: [_jsx("text", { fg: "cyan", children: _jsxs("strong", { children: ["\u26A1 ", server.name] }) }), server.requiresConfig && _jsx("text", { fg: "yellow", children: " \u2699" })] }), _jsx("text", { fg: "gray", children: server.description }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Status " }), isInstalled && isEnabled ? (_jsx("text", { fg: "green", children: "\u25CF Installed" })) : isInstalled ? (_jsx("text", { fg: "yellow", children: "\u25CF Disabled" })) : (_jsx("text", { fg: "gray", children: "\u25CB Not installed" }))] }), _jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Type " }), _jsx("text", { fg: "white", children: server.type === "http" ? "HTTP" : "Command" })] }), server.type === "http" ? (_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "URL " }), _jsx("text", { fg: "
|
|
201
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsxs("box", { marginBottom: 1, children: [_jsx("text", { fg: "cyan", children: _jsxs("strong", { children: ["\u26A1 ", server.name] }) }), server.requiresConfig && _jsx("text", { fg: "yellow", children: " \u2699" })] }), _jsx("text", { fg: "gray", children: server.description }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Status " }), isInstalled && isEnabled ? (_jsx("text", { fg: "green", children: "\u25CF Installed" })) : isInstalled ? (_jsx("text", { fg: "yellow", children: "\u25CF Disabled" })) : (_jsx("text", { fg: "gray", children: "\u25CB Not installed" }))] }), _jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Type " }), _jsx("text", { fg: "white", children: server.type === "http" ? "HTTP" : "Command" })] }), server.type === "http" ? (_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "URL " }), _jsx("text", { fg: "#5c9aff", children: server.url })] })) : (_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Command " }), _jsx("text", { fg: "cyan", children: server.command })] })), server.requiresConfig && (_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Config " }), _jsxs("text", { fg: "yellow", children: [server.configFields?.length || 0, " fields required"] })] }))] }), _jsx("box", { marginTop: 2, children: isInstalled ? (_jsxs("box", { children: [_jsxs("text", { bg: "red", fg: "white", children: [" ", "Enter", " "] }), _jsx("text", { fg: "gray", children: " Remove server" })] })) : (_jsxs("box", { children: [_jsxs("text", { bg: "green", fg: "black", children: [" ", "Enter", " "] }), _jsx("text", { fg: "gray", children: " Install server" })] })) })] }));
|
|
202
202
|
};
|
|
203
203
|
const renderListItem = (item, _idx, isSelected) => {
|
|
204
204
|
// Category header
|
|
@@ -269,7 +269,9 @@ export function McpScreen() {
|
|
|
269
269
|
return (
|
|
270
270
|
<box flexDirection="column">
|
|
271
271
|
<box marginBottom={1}>
|
|
272
|
-
<text fg="cyan"
|
|
272
|
+
<text fg="cyan">
|
|
273
|
+
<strong>⚡ {server.name}</strong>
|
|
274
|
+
</text>
|
|
273
275
|
{server.requiresConfig && <text fg="yellow"> ⚙</text>}
|
|
274
276
|
</box>
|
|
275
277
|
|
|
@@ -295,7 +297,7 @@ export function McpScreen() {
|
|
|
295
297
|
{server.type === "http" ? (
|
|
296
298
|
<box>
|
|
297
299
|
<text fg="gray">URL </text>
|
|
298
|
-
<text fg="
|
|
300
|
+
<text fg="#5c9aff">{server.url}</text>
|
|
299
301
|
</box>
|
|
300
302
|
) : (
|
|
301
303
|
<box>
|
|
@@ -316,12 +318,18 @@ export function McpScreen() {
|
|
|
316
318
|
<box marginTop={2}>
|
|
317
319
|
{isInstalled ? (
|
|
318
320
|
<box>
|
|
319
|
-
<text bg="red" fg="white">
|
|
321
|
+
<text bg="red" fg="white">
|
|
322
|
+
{" "}
|
|
323
|
+
Enter{" "}
|
|
324
|
+
</text>
|
|
320
325
|
<text fg="gray"> Remove server</text>
|
|
321
326
|
</box>
|
|
322
327
|
) : (
|
|
323
328
|
<box>
|
|
324
|
-
<text bg="green" fg="black">
|
|
329
|
+
<text bg="green" fg="black">
|
|
330
|
+
{" "}
|
|
331
|
+
Enter{" "}
|
|
332
|
+
</text>
|
|
325
333
|
<text fg="gray"> Install server</text>
|
|
326
334
|
</box>
|
|
327
335
|
)}
|
|
@@ -338,7 +346,9 @@ export function McpScreen() {
|
|
|
338
346
|
// Category header
|
|
339
347
|
if (item.isCategory) {
|
|
340
348
|
return (
|
|
341
|
-
<text fg="magenta"
|
|
349
|
+
<text fg="magenta">
|
|
350
|
+
<strong>▸ {item.label}</strong>
|
|
351
|
+
</text>
|
|
342
352
|
);
|
|
343
353
|
}
|
|
344
354
|
|
|
@@ -243,9 +243,7 @@ export function ModelSelectorScreen() {
|
|
|
243
243
|
return;
|
|
244
244
|
}
|
|
245
245
|
// Regular text input (single character keys)
|
|
246
|
-
if (event.name.length === 1 &&
|
|
247
|
-
!event.ctrl &&
|
|
248
|
-
!event.shift) {
|
|
246
|
+
if (event.name.length === 1 && !event.ctrl && !event.shift) {
|
|
249
247
|
dispatch({
|
|
250
248
|
type: "MODEL_SELECTOR_SET_SEARCH",
|
|
251
249
|
query: modelSelector.searchQuery + event.name,
|
|
@@ -258,7 +256,7 @@ export function ModelSelectorScreen() {
|
|
|
258
256
|
if (item.type === "header") {
|
|
259
257
|
// Header style: "Name ----------------------------- Status"
|
|
260
258
|
const status = item.configured ? "✔ Configured" : "";
|
|
261
|
-
return (_jsxs("box", { margin: 0, children: [_jsxs("text", { fg: "gray", children: [item.label, " "] }), _jsx("text", { fg: "#888888", children: "\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF" }), status && _jsxs("text", { fg: "green", children: [" ", status] }), item.provider && _jsxs("text", { fg: "
|
|
259
|
+
return (_jsxs("box", { margin: 0, children: [_jsxs("text", { fg: "gray", children: [item.label, " "] }), _jsx("text", { fg: "#888888", children: "\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF" }), status && _jsxs("text", { fg: "green", children: [" ", status] }), item.provider && _jsxs("text", { fg: "#5c9aff", children: [" ", item.provider] })] }));
|
|
262
260
|
}
|
|
263
261
|
// Model item
|
|
264
262
|
// Matches highlighting
|
|
@@ -271,7 +269,7 @@ export function ModelSelectorScreen() {
|
|
|
271
269
|
: "cyan"
|
|
272
270
|
: isSelected
|
|
273
271
|
? "white"
|
|
274
|
-
: "white", children: seg.highlighted ? _jsx("u", { children: _jsx("strong", { children: seg.text }) }) : seg.text }, i)))
|
|
272
|
+
: "white", children: seg.highlighted ? (_jsx("u", { children: _jsx("strong", { children: seg.text }) })) : (seg.text) }, i)))
|
|
275
273
|
: item.label }));
|
|
276
274
|
if (isSelected) {
|
|
277
275
|
return (_jsxs("box", { overflow: "hidden", children: [_jsx("text", { bg: "magenta", fg: "white", children: " " }), _jsx("text", { bg: "magenta", fg: "white", children: _jsx(Label, {}) }), _jsx("box", { flexGrow: 1, children: _jsx("text", { bg: "magenta", children: " " }) }), item.provider && (_jsxs("text", { bg: "magenta", fg: "white", children: [item.provider, " "] }))] }));
|
|
@@ -282,11 +282,7 @@ export function ModelSelectorScreen() {
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
// Regular text input (single character keys)
|
|
285
|
-
if (
|
|
286
|
-
event.name.length === 1 &&
|
|
287
|
-
!event.ctrl &&
|
|
288
|
-
!event.shift
|
|
289
|
-
) {
|
|
285
|
+
if (event.name.length === 1 && !event.ctrl && !event.shift) {
|
|
290
286
|
dispatch({
|
|
291
287
|
type: "MODEL_SELECTOR_SET_SEARCH",
|
|
292
288
|
query: modelSelector.searchQuery + event.name,
|
|
@@ -304,11 +300,9 @@ export function ModelSelectorScreen() {
|
|
|
304
300
|
return (
|
|
305
301
|
<box margin={0}>
|
|
306
302
|
<text fg="gray">{item.label} </text>
|
|
307
|
-
<text fg="#888888">
|
|
308
|
-
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
|
|
309
|
-
</text>
|
|
303
|
+
<text fg="#888888">⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯</text>
|
|
310
304
|
{status && <text fg="green"> {status}</text>}
|
|
311
|
-
{item.provider && <text fg="
|
|
305
|
+
{item.provider && <text fg="#5c9aff"> {item.provider}</text>}
|
|
312
306
|
</box>
|
|
313
307
|
);
|
|
314
308
|
}
|
|
@@ -334,7 +328,13 @@ export function ModelSelectorScreen() {
|
|
|
334
328
|
: "white"
|
|
335
329
|
}
|
|
336
330
|
>
|
|
337
|
-
{seg.highlighted ?
|
|
331
|
+
{seg.highlighted ? (
|
|
332
|
+
<u>
|
|
333
|
+
<strong>{seg.text}</strong>
|
|
334
|
+
</u>
|
|
335
|
+
) : (
|
|
336
|
+
seg.text
|
|
337
|
+
)}
|
|
338
338
|
</text>
|
|
339
339
|
))
|
|
340
340
|
: item.label}
|
|
@@ -395,15 +395,11 @@ export function ModelSelectorScreen() {
|
|
|
395
395
|
<box flexDirection="row" justifyContent="space-between">
|
|
396
396
|
<box>
|
|
397
397
|
<text fg="#7e57c2">Switch Model </text>
|
|
398
|
-
<text
|
|
399
|
-
fg={modelSelector.taskSize === "large" ? "white" : "gray"}
|
|
400
|
-
>
|
|
398
|
+
<text fg={modelSelector.taskSize === "large" ? "white" : "gray"}>
|
|
401
399
|
{modelSelector.taskSize === "large" ? "◎" : "○"} Large Task
|
|
402
400
|
{" "}
|
|
403
401
|
</text>
|
|
404
|
-
<text
|
|
405
|
-
fg={modelSelector.taskSize === "small" ? "white" : "gray"}
|
|
406
|
-
>
|
|
402
|
+
<text fg={modelSelector.taskSize === "small" ? "white" : "gray"}>
|
|
407
403
|
{modelSelector.taskSize === "small" ? "◎" : "○"} Small Task
|
|
408
404
|
</text>
|
|
409
405
|
</box>
|
|
@@ -9,7 +9,9 @@ import { ScrollableList } from "../components/ScrollableList.js";
|
|
|
9
9
|
import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
|
|
10
10
|
import { getAllMarketplaces } from "../../data/marketplaces.js";
|
|
11
11
|
import { getAvailablePlugins, refreshAllMarketplaces, clearMarketplaceCache, getLocalMarketplacesInfo, } from "../../services/plugin-manager.js";
|
|
12
|
-
import { setMcpEnvVar, getMcpEnvVars, } from "../../services/claude-settings.js";
|
|
12
|
+
import { setMcpEnvVar, getMcpEnvVars, readSettings, saveGlobalInstalledPluginVersion, saveLocalInstalledPluginVersion, } from "../../services/claude-settings.js";
|
|
13
|
+
import { saveProfile } from "../../services/profiles.js";
|
|
14
|
+
import { saveInstalledPluginVersion } from "../../services/plugin-manager.js";
|
|
13
15
|
import { installPlugin as cliInstallPlugin, uninstallPlugin as cliUninstallPlugin, updatePlugin as cliUpdatePlugin, } from "../../services/claude-cli.js";
|
|
14
16
|
import { getPluginEnvRequirements, getPluginSourcePath, } from "../../services/plugin-mcp-config.js";
|
|
15
17
|
import { getPluginSetupFromSource, checkMissingDeps, installPluginDeps, } from "../../services/plugin-setup.js";
|
|
@@ -145,50 +147,78 @@ export function PluginsScreen() {
|
|
|
145
147
|
const selectableItems = useMemo(() => {
|
|
146
148
|
return filteredItems.filter((item) => item.type === "plugin" || item.type === "category");
|
|
147
149
|
}, [filteredItems]);
|
|
148
|
-
// Keyboard handling
|
|
150
|
+
// Keyboard handling — inline search with live filtering
|
|
149
151
|
useKeyboard((event) => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
if (state.modal)
|
|
153
|
+
return;
|
|
154
|
+
const hasQuery = pluginsState.searchQuery.length > 0;
|
|
155
|
+
// Escape: always clear search state fully
|
|
156
|
+
if (event.name === "escape") {
|
|
157
|
+
if (hasQuery || isSearchActive) {
|
|
154
158
|
dispatch({ type: "PLUGINS_SET_SEARCH", query: "" });
|
|
155
|
-
}
|
|
156
|
-
else if (event.name === "enter") {
|
|
157
159
|
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
158
|
-
|
|
160
|
+
dispatch({ type: "PLUGINS_SELECT", index: 0 });
|
|
159
161
|
}
|
|
160
|
-
|
|
162
|
+
// Don't return — let GlobalKeyHandler handle Escape too (for quit)
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Backspace: remove last char from search query
|
|
166
|
+
if (event.name === "backspace" || event.name === "delete") {
|
|
167
|
+
if (hasQuery) {
|
|
168
|
+
const newQuery = pluginsState.searchQuery.slice(0, -1);
|
|
169
|
+
dispatch({ type: "PLUGINS_SET_SEARCH", query: newQuery });
|
|
170
|
+
dispatch({ type: "PLUGINS_SELECT", index: 0 });
|
|
171
|
+
// If query becomes empty, exit search mode
|
|
172
|
+
if (newQuery.length === 0) {
|
|
173
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Navigation — always works (even during search)
|
|
179
|
+
if (event.name === "up" || event.name === "k") {
|
|
180
|
+
// 'k' navigates when query is empty, otherwise appends to search
|
|
181
|
+
if (event.name === "k" && (hasQuery || isSearchActive)) {
|
|
161
182
|
dispatch({
|
|
162
183
|
type: "PLUGINS_SET_SEARCH",
|
|
163
|
-
query: pluginsState.searchQuery.
|
|
184
|
+
query: pluginsState.searchQuery + event.name,
|
|
164
185
|
});
|
|
186
|
+
dispatch({ type: "PLUGINS_SELECT", index: 0 });
|
|
187
|
+
return;
|
|
165
188
|
}
|
|
166
|
-
|
|
189
|
+
const newIndex = Math.max(0, pluginsState.selectedIndex - 1);
|
|
190
|
+
dispatch({ type: "PLUGINS_SELECT", index: newIndex });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (event.name === "down" || event.name === "j") {
|
|
194
|
+
// 'j' navigates when query is empty, otherwise appends to search
|
|
195
|
+
if (event.name === "j" && (hasQuery || isSearchActive)) {
|
|
167
196
|
dispatch({
|
|
168
197
|
type: "PLUGINS_SET_SEARCH",
|
|
169
198
|
query: pluginsState.searchQuery + event.name,
|
|
170
199
|
});
|
|
200
|
+
dispatch({ type: "PLUGINS_SELECT", index: 0 });
|
|
201
|
+
return;
|
|
171
202
|
}
|
|
203
|
+
const newIndex = Math.min(selectableItems.length - 1, pluginsState.selectedIndex + 1);
|
|
204
|
+
dispatch({ type: "PLUGINS_SELECT", index: newIndex });
|
|
172
205
|
return;
|
|
173
206
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
207
|
+
// Enter — exit search mode (keep filter active) + select/install
|
|
208
|
+
if (event.name === "enter") {
|
|
209
|
+
if (isSearchActive) {
|
|
210
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
211
|
+
// Keep the query — filter stays active, shortcuts resume
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
handleSelect();
|
|
179
215
|
return;
|
|
180
216
|
}
|
|
181
|
-
//
|
|
182
|
-
if (event.name === "
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
else if (event.name === "down" || event.name === "j") {
|
|
187
|
-
const newIndex = Math.min(selectableItems.length - 1, pluginsState.selectedIndex + 1);
|
|
188
|
-
dispatch({ type: "PLUGINS_SELECT", index: newIndex });
|
|
189
|
-
}
|
|
190
|
-
// Collapse/expand marketplace
|
|
191
|
-
else if ((event.name === "left" || event.name === "right" || event.name === "<" || event.name === ">") &&
|
|
217
|
+
// Collapse/expand marketplace — always works
|
|
218
|
+
if ((event.name === "left" ||
|
|
219
|
+
event.name === "right" ||
|
|
220
|
+
event.name === "<" ||
|
|
221
|
+
event.name === ">") &&
|
|
192
222
|
selectableItems[pluginsState.selectedIndex]?.marketplace) {
|
|
193
223
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
194
224
|
if (item?.marketplace) {
|
|
@@ -197,44 +227,55 @@ export function PluginsScreen() {
|
|
|
197
227
|
name: item.marketplace.name,
|
|
198
228
|
});
|
|
199
229
|
}
|
|
230
|
+
return;
|
|
200
231
|
}
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
232
|
+
// When search query is non-empty, printable letters go to the query
|
|
233
|
+
// (shortcuts are suspended while filtering, digits skip to let tab nav work)
|
|
234
|
+
if (hasQuery || isSearchActive) {
|
|
235
|
+
if (event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
|
|
236
|
+
dispatch({
|
|
237
|
+
type: "PLUGINS_SET_SEARCH",
|
|
238
|
+
query: pluginsState.searchQuery + event.name,
|
|
239
|
+
});
|
|
240
|
+
dispatch({ type: "PLUGINS_SELECT", index: 0 });
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
204
243
|
}
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
244
|
+
// When search query is empty: action shortcuts work normally
|
|
245
|
+
// Start explicit search mode with /
|
|
246
|
+
if (event.name === "/") {
|
|
247
|
+
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
248
|
+
return;
|
|
208
249
|
}
|
|
209
|
-
//
|
|
210
|
-
|
|
250
|
+
// Action shortcuts (only when query is empty)
|
|
251
|
+
if (event.name === "r")
|
|
252
|
+
handleRefresh();
|
|
253
|
+
else if (event.name === "n")
|
|
254
|
+
handleShowAddMarketplaceInstructions();
|
|
255
|
+
else if (event.name === "t")
|
|
211
256
|
handleShowTeamConfigHelp();
|
|
212
|
-
|
|
213
|
-
// Scope-specific toggle shortcuts (u/p/l)
|
|
214
|
-
else if (event.name === "u") {
|
|
257
|
+
else if (event.name === "u")
|
|
215
258
|
handleScopeToggle("user");
|
|
216
|
-
|
|
217
|
-
else if (event.name === "p") {
|
|
259
|
+
else if (event.name === "p")
|
|
218
260
|
handleScopeToggle("project");
|
|
219
|
-
|
|
220
|
-
else if (event.name === "l") {
|
|
261
|
+
else if (event.name === "l")
|
|
221
262
|
handleScopeToggle("local");
|
|
222
|
-
|
|
223
|
-
// Update plugin (Shift+U)
|
|
224
|
-
else if (event.name === "U") {
|
|
263
|
+
else if (event.name === "U")
|
|
225
264
|
handleUpdate();
|
|
226
|
-
|
|
227
|
-
// Update all
|
|
228
|
-
else if (event.name === "a") {
|
|
265
|
+
else if (event.name === "a")
|
|
229
266
|
handleUpdateAll();
|
|
230
|
-
|
|
231
|
-
// Delete/uninstall
|
|
232
|
-
else if (event.name === "d") {
|
|
267
|
+
else if (event.name === "d")
|
|
233
268
|
handleUninstall();
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
269
|
+
else if (event.name === "s")
|
|
270
|
+
handleSaveAsProfile();
|
|
271
|
+
// Any other printable letter: start inline search (skip digits — used for tab nav)
|
|
272
|
+
else if (event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
|
|
273
|
+
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
274
|
+
dispatch({
|
|
275
|
+
type: "PLUGINS_SET_SEARCH",
|
|
276
|
+
query: event.name,
|
|
277
|
+
});
|
|
278
|
+
dispatch({ type: "PLUGINS_SELECT", index: 0 });
|
|
238
279
|
}
|
|
239
280
|
});
|
|
240
281
|
// Handle actions
|
|
@@ -366,7 +407,8 @@ export function PluginsScreen() {
|
|
|
366
407
|
const hasMissing = (missing.pip?.length || 0) +
|
|
367
408
|
(missing.brew?.length || 0) +
|
|
368
409
|
(missing.npm?.length || 0) +
|
|
369
|
-
(missing.cargo?.length || 0) >
|
|
410
|
+
(missing.cargo?.length || 0) >
|
|
411
|
+
0;
|
|
370
412
|
if (!hasMissing)
|
|
371
413
|
return;
|
|
372
414
|
// Build description of what will be installed
|
|
@@ -399,6 +441,27 @@ export function PluginsScreen() {
|
|
|
399
441
|
console.error("Error installing plugin deps:", error);
|
|
400
442
|
}
|
|
401
443
|
};
|
|
444
|
+
/**
|
|
445
|
+
* Save the installed version to settings after CLI install/update.
|
|
446
|
+
* Claude CLI doesn't update installedPluginVersions in settings.json,
|
|
447
|
+
* so we do it ourselves to keep the TUI version display accurate.
|
|
448
|
+
*/
|
|
449
|
+
const saveVersionAfterInstall = async (pluginId, version, scope) => {
|
|
450
|
+
try {
|
|
451
|
+
if (scope === "user") {
|
|
452
|
+
await saveGlobalInstalledPluginVersion(pluginId, version);
|
|
453
|
+
}
|
|
454
|
+
else if (scope === "local") {
|
|
455
|
+
await saveLocalInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
await saveInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
// Non-fatal: version display may be stale but plugin still works
|
|
463
|
+
}
|
|
464
|
+
};
|
|
402
465
|
const handleSelect = async () => {
|
|
403
466
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
404
467
|
if (!item)
|
|
@@ -508,9 +571,11 @@ export function PluginsScreen() {
|
|
|
508
571
|
}
|
|
509
572
|
else if (action === "update") {
|
|
510
573
|
await cliUpdatePlugin(plugin.id, scope);
|
|
574
|
+
await saveVersionAfterInstall(plugin.id, latestVersion, scope);
|
|
511
575
|
}
|
|
512
576
|
else {
|
|
513
577
|
await cliInstallPlugin(plugin.id, scope);
|
|
578
|
+
await saveVersionAfterInstall(plugin.id, latestVersion, scope);
|
|
514
579
|
// On fresh install, configure env vars and install system deps
|
|
515
580
|
modal.hideModal();
|
|
516
581
|
await collectPluginEnvVars(plugin.name, plugin.marketplace);
|
|
@@ -536,6 +601,7 @@ export function PluginsScreen() {
|
|
|
536
601
|
modal.loading(`Updating ${plugin.name}...`);
|
|
537
602
|
try {
|
|
538
603
|
await cliUpdatePlugin(plugin.id, scope);
|
|
604
|
+
await saveVersionAfterInstall(plugin.id, plugin.version || "0.0.0", scope);
|
|
539
605
|
modal.hideModal();
|
|
540
606
|
fetchData();
|
|
541
607
|
}
|
|
@@ -555,6 +621,7 @@ export function PluginsScreen() {
|
|
|
555
621
|
try {
|
|
556
622
|
for (const plugin of updatable) {
|
|
557
623
|
await cliUpdatePlugin(plugin.id, scope);
|
|
624
|
+
await saveVersionAfterInstall(plugin.id, plugin.version || "0.0.0", scope);
|
|
558
625
|
}
|
|
559
626
|
modal.hideModal();
|
|
560
627
|
fetchData();
|
|
@@ -608,9 +675,11 @@ export function PluginsScreen() {
|
|
|
608
675
|
}
|
|
609
676
|
else if (action === "update") {
|
|
610
677
|
await cliUpdatePlugin(plugin.id, scope);
|
|
678
|
+
await saveVersionAfterInstall(plugin.id, latestVersion, scope);
|
|
611
679
|
}
|
|
612
680
|
else {
|
|
613
681
|
await cliInstallPlugin(plugin.id, scope);
|
|
682
|
+
await saveVersionAfterInstall(plugin.id, latestVersion, scope);
|
|
614
683
|
// On fresh install, configure env vars and install system deps
|
|
615
684
|
modal.hideModal();
|
|
616
685
|
await collectPluginEnvVars(plugin.name, plugin.marketplace);
|
|
@@ -626,6 +695,37 @@ export function PluginsScreen() {
|
|
|
626
695
|
await modal.message("Error", `Failed: ${error}`, "error");
|
|
627
696
|
}
|
|
628
697
|
};
|
|
698
|
+
const handleSaveAsProfile = async () => {
|
|
699
|
+
// Read current enabledPlugins from project settings
|
|
700
|
+
const settings = await readSettings(state.projectPath);
|
|
701
|
+
const enabledPlugins = settings.enabledPlugins ?? {};
|
|
702
|
+
const name = await modal.input("Save Profile", "Profile name:");
|
|
703
|
+
if (name === null || !name.trim())
|
|
704
|
+
return;
|
|
705
|
+
const scopeChoice = await modal.select("Save to scope", "Where should this profile be saved?", [
|
|
706
|
+
{
|
|
707
|
+
label: "User — ~/.claude/profiles.json (available everywhere)",
|
|
708
|
+
value: "user",
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
label: "Project — .claude/profiles.json (shared with team via git)",
|
|
712
|
+
value: "project",
|
|
713
|
+
},
|
|
714
|
+
]);
|
|
715
|
+
if (scopeChoice === null)
|
|
716
|
+
return;
|
|
717
|
+
const scope = scopeChoice;
|
|
718
|
+
modal.loading("Saving profile...");
|
|
719
|
+
try {
|
|
720
|
+
await saveProfile(name.trim(), enabledPlugins, scope, state.projectPath);
|
|
721
|
+
modal.hideModal();
|
|
722
|
+
await modal.message("Saved", `Profile "${name.trim()}" saved.\nPress 6 to manage profiles.`, "success");
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
modal.hideModal();
|
|
726
|
+
await modal.message("Error", `Failed to save profile: ${error}`, "error");
|
|
727
|
+
}
|
|
728
|
+
};
|
|
629
729
|
const handleUninstall = async () => {
|
|
630
730
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
631
731
|
if (!item || item.type !== "plugin" || !item.plugin)
|
|
@@ -719,7 +819,11 @@ export function PluginsScreen() {
|
|
|
719
819
|
const plugin = item.plugin;
|
|
720
820
|
let statusIcon = "○";
|
|
721
821
|
let statusColor = "gray";
|
|
722
|
-
if (plugin.
|
|
822
|
+
if (plugin.isOrphaned) {
|
|
823
|
+
statusIcon = "x";
|
|
824
|
+
statusColor = "red";
|
|
825
|
+
}
|
|
826
|
+
else if (plugin.enabled) {
|
|
723
827
|
statusIcon = "●";
|
|
724
828
|
statusColor = "green";
|
|
725
829
|
}
|
|
@@ -729,7 +833,10 @@ export function PluginsScreen() {
|
|
|
729
833
|
}
|
|
730
834
|
// Build version string
|
|
731
835
|
let versionStr = "";
|
|
732
|
-
if (plugin.
|
|
836
|
+
if (plugin.isOrphaned) {
|
|
837
|
+
versionStr = " deprecated";
|
|
838
|
+
}
|
|
839
|
+
else if (plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
|
|
733
840
|
versionStr = ` v${plugin.installedVersion}`;
|
|
734
841
|
if (plugin.hasUpdate && plugin.version) {
|
|
735
842
|
versionStr += ` → v${plugin.version}`;
|
|
@@ -746,6 +853,11 @@ export function PluginsScreen() {
|
|
|
746
853
|
const displayName = segments
|
|
747
854
|
? segments.map((seg) => seg.text).join("")
|
|
748
855
|
: plugin.name;
|
|
856
|
+
if (plugin.isOrphaned) {
|
|
857
|
+
const ver = plugin.installedVersion && plugin.installedVersion !== "0.0.0"
|
|
858
|
+
? ` v${plugin.installedVersion}` : "";
|
|
859
|
+
return (_jsxs("text", { children: [_jsxs("span", { fg: "red", children: [" ", statusIcon, " "] }), _jsx("span", { fg: "gray", children: displayName }), ver && _jsx("span", { fg: "yellow", children: ver }), _jsx("span", { fg: "red", children: " deprecated" })] }));
|
|
860
|
+
}
|
|
749
861
|
return (_jsxs("text", { children: [_jsxs("span", { fg: statusColor, children: [" ", statusIcon, " "] }), _jsx("span", { children: displayName }), _jsx("span", { fg: plugin.hasUpdate ? "yellow" : "gray", children: versionStr })] }));
|
|
750
862
|
}
|
|
751
863
|
return _jsx("text", { fg: "gray", children: item.label });
|
|
@@ -783,11 +895,15 @@ export function PluginsScreen() {
|
|
|
783
895
|
actionHint = "Remove";
|
|
784
896
|
}
|
|
785
897
|
}
|
|
786
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "cyan", children: _jsxs("strong", { children: [mp.displayName, getBadge()] }) }), _jsx("text", { fg: "gray", children: mp.description || "No description" }), _jsx("text", { fg: isEnabled ? "green" : "gray", children: isEnabled ? "● Added" : "○ Not added" }), _jsxs("text", { fg: "
|
|
898
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "cyan", children: _jsxs("strong", { children: [mp.displayName, getBadge()] }) }), _jsx("text", { fg: "gray", children: mp.description || "No description" }), _jsx("text", { fg: isEnabled ? "green" : "gray", children: isEnabled ? "● Added" : "○ Not added" }), _jsxs("text", { fg: "#5c9aff", children: ["github.com/", mp.source.repo] }), _jsxs("text", { children: ["Plugins: ", selectedItem.pluginCount || 0] }), _jsxs("box", { marginTop: 1, children: [_jsxs("text", { bg: isEnabled ? "cyan" : "green", fg: "black", children: [" ", "Enter", " "] }), _jsxs("text", { fg: "gray", children: [" ", actionHint] })] }), isEnabled && (_jsx("box", { children: _jsx("text", { fg: "gray", children: "\u2190 \u2192 to expand/collapse" }) }))] }));
|
|
787
899
|
}
|
|
788
900
|
if (selectedItem.type === "plugin" && selectedItem.plugin) {
|
|
789
901
|
const plugin = selectedItem.plugin;
|
|
790
902
|
const isInstalled = plugin.enabled || plugin.installedVersion;
|
|
903
|
+
// Orphaned/deprecated plugin
|
|
904
|
+
if (plugin.isOrphaned) {
|
|
905
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { justifyContent: "center", children: _jsx("text", { bg: "yellow", fg: "black", children: _jsxs("strong", { children: [" ", plugin.name, " \u2014 DEPRECATED "] }) }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "yellow", children: "This plugin is no longer in the marketplace." }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "It was removed from the marketplace but still referenced in your settings. Press d to uninstall and clean up." }) }), isInstalled && (_jsx("box", { flexDirection: "column", marginTop: 2, children: _jsxs("box", { children: [_jsx("text", { bg: "red", fg: "white", children: " d " }), _jsx("text", { children: " Uninstall (recommended)" })] }) }))] }));
|
|
906
|
+
}
|
|
791
907
|
// Build component counts
|
|
792
908
|
const components = [];
|
|
793
909
|
if (plugin.agents?.length)
|
|
@@ -804,14 +920,14 @@ export function PluginsScreen() {
|
|
|
804
920
|
// Show version only if valid (not null, not 0.0.0)
|
|
805
921
|
const showVersion = plugin.version && plugin.version !== "0.0.0";
|
|
806
922
|
const showInstalledVersion = plugin.installedVersion && plugin.installedVersion !== "0.0.0";
|
|
807
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { justifyContent: "center", children: _jsx("text", { bg: "magenta", fg: "white", children: _jsxs("strong", { children: [" ", plugin.name, plugin.hasUpdate ? " ⬆" : "", " "] }) }) }), _jsx("box", { marginTop: 1, children: isInstalled ? (_jsx("text", { fg: plugin.enabled ? "green" : "yellow", children: plugin.enabled ? "● Enabled" : "● Disabled" })) : (_jsx("text", { fg: "gray", children: "\u25CB Not installed" })) }), _jsx("box", { marginTop: 1, marginBottom: 1, children: _jsx("text", { fg: "white", children: plugin.description }) }), showVersion && (_jsxs("text", { children: [_jsx("span", { children: "Version " }), _jsxs("span", { fg: "
|
|
808
|
-
plugin.installedVersion !== plugin.version && (_jsxs("span", { children: [" (v", plugin.installedVersion, " installed)"] }))] })), plugin.category && (_jsxs("text", { children: [_jsx("span", { children: "Category " }), _jsx("span", { fg: "magenta", children: plugin.category })] })), plugin.author && (_jsxs("text", { children: [_jsx("span", { children: "Author " }), _jsx("span", { children: plugin.author.name })] })), components.length > 0 && (_jsxs("text", { children: [_jsx("span", { children: "Contains " }), _jsx("span", { fg: "yellow", children: components.join(" · ") })] })), _jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsx("text", { children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx("text", { children: _jsx("strong", { children: "Scopes:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [
|
|
923
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { justifyContent: "center", children: _jsx("text", { bg: "magenta", fg: "white", children: _jsxs("strong", { children: [" ", plugin.name, plugin.hasUpdate ? " ⬆" : "", " "] }) }) }), _jsx("box", { marginTop: 1, children: isInstalled ? (_jsx("text", { fg: plugin.enabled ? "green" : "yellow", children: plugin.enabled ? "● Enabled" : "● Disabled" })) : (_jsx("text", { fg: "gray", children: "\u25CB Not installed" })) }), _jsx("box", { marginTop: 1, marginBottom: 1, children: _jsx("text", { fg: "white", children: plugin.description }) }), showVersion && (_jsxs("text", { children: [_jsx("span", { children: "Version " }), _jsxs("span", { fg: "#5c9aff", children: ["v", plugin.version] }), showInstalledVersion &&
|
|
924
|
+
plugin.installedVersion !== plugin.version && (_jsxs("span", { children: [" (v", plugin.installedVersion, " installed)"] }))] })), plugin.category && (_jsxs("text", { children: [_jsx("span", { children: "Category " }), _jsx("span", { fg: "magenta", children: plugin.category })] })), plugin.author && (_jsxs("text", { children: [_jsx("span", { children: "Author " }), _jsx("span", { children: plugin.author.name })] })), components.length > 0 && (_jsxs("text", { children: [_jsx("span", { children: "Contains " }), _jsx("span", { fg: "yellow", children: components.join(" · ") })] })), _jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsx("text", { children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx("text", { children: _jsx("strong", { children: "Scopes:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsxs("span", { bg: "cyan", fg: "black", children: [" ", "u", " "] }), _jsx("span", { fg: plugin.userScope?.enabled ? "cyan" : "gray", children: plugin.userScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: "cyan", children: "User" }), _jsx("span", { children: " global" }), plugin.userScope?.version && (_jsxs("span", { fg: "cyan", children: [" v", plugin.userScope.version] }))] }), _jsxs("text", { children: [_jsxs("span", { bg: "green", fg: "black", children: [" ", "p", " "] }), _jsx("span", { fg: plugin.projectScope?.enabled ? "green" : "gray", children: plugin.projectScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: "green", children: "Project" }), _jsx("span", { children: " team" }), plugin.projectScope?.version && (_jsxs("span", { fg: "green", children: [" v", plugin.projectScope.version] }))] }), _jsxs("text", { children: [_jsxs("span", { bg: "yellow", fg: "black", children: [" ", "l", " "] }), _jsx("span", { fg: plugin.localScope?.enabled ? "yellow" : "gray", children: plugin.localScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: "yellow", children: "Local" }), _jsx("span", { children: " private" }), plugin.localScope?.version && (_jsxs("span", { fg: "yellow", children: [" v", plugin.localScope.version] }))] })] })] }), isInstalled && (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [plugin.hasUpdate && (_jsxs("box", { children: [_jsxs("text", { bg: "magenta", fg: "white", children: [" ", "U", " "] }), _jsxs("text", { children: [" Update to v", plugin.version] })] })), _jsxs("box", { children: [_jsxs("text", { bg: "red", fg: "white", children: [" ", "d", " "] }), _jsx("text", { children: " Uninstall" })] })] }))] }));
|
|
809
925
|
}
|
|
810
926
|
return null;
|
|
811
927
|
};
|
|
812
|
-
const footerHints = isSearchActive
|
|
813
|
-
? "
|
|
814
|
-
: "u/p/l:scope │ U:update │ a:all │ d:remove │
|
|
928
|
+
const footerHints = isSearchActive || pluginsState.searchQuery
|
|
929
|
+
? "↑↓:nav │ Enter:select │ Esc:clear │ type to filter"
|
|
930
|
+
: "u/p/l:scope │ U:update │ a:all │ d:remove │ s:profile │ type to search";
|
|
815
931
|
// Calculate status for subtitle
|
|
816
932
|
const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
|
|
817
933
|
const plugins = pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
|