claudeup 3.7.2 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/package.json +1 -1
  2. package/src/data/settings-catalog.js +612 -0
  3. package/src/data/settings-catalog.ts +689 -0
  4. package/src/data/skill-repos.js +86 -0
  5. package/src/data/skill-repos.ts +97 -0
  6. package/src/services/plugin-manager.js +2 -0
  7. package/src/services/plugin-manager.ts +3 -0
  8. package/src/services/profiles.js +161 -0
  9. package/src/services/profiles.ts +225 -0
  10. package/src/services/settings-manager.js +108 -0
  11. package/src/services/settings-manager.ts +140 -0
  12. package/src/services/skills-manager.js +239 -0
  13. package/src/services/skills-manager.ts +328 -0
  14. package/src/services/skillsmp-client.js +67 -0
  15. package/src/services/skillsmp-client.ts +89 -0
  16. package/src/types/index.ts +101 -1
  17. package/src/ui/App.js +23 -18
  18. package/src/ui/App.tsx +27 -23
  19. package/src/ui/components/TabBar.js +9 -8
  20. package/src/ui/components/TabBar.tsx +15 -19
  21. package/src/ui/components/layout/ScreenLayout.js +8 -14
  22. package/src/ui/components/layout/ScreenLayout.tsx +51 -58
  23. package/src/ui/components/modals/ModalContainer.js +43 -11
  24. package/src/ui/components/modals/ModalContainer.tsx +44 -12
  25. package/src/ui/components/modals/SelectModal.js +4 -18
  26. package/src/ui/components/modals/SelectModal.tsx +10 -21
  27. package/src/ui/screens/CliToolsScreen.js +2 -2
  28. package/src/ui/screens/CliToolsScreen.tsx +8 -8
  29. package/src/ui/screens/EnvVarsScreen.js +248 -116
  30. package/src/ui/screens/EnvVarsScreen.tsx +419 -184
  31. package/src/ui/screens/McpRegistryScreen.tsx +18 -6
  32. package/src/ui/screens/McpScreen.js +1 -1
  33. package/src/ui/screens/McpScreen.tsx +15 -5
  34. package/src/ui/screens/ModelSelectorScreen.js +3 -5
  35. package/src/ui/screens/ModelSelectorScreen.tsx +12 -16
  36. package/src/ui/screens/PluginsScreen.js +154 -66
  37. package/src/ui/screens/PluginsScreen.tsx +280 -97
  38. package/src/ui/screens/ProfilesScreen.js +255 -0
  39. package/src/ui/screens/ProfilesScreen.tsx +487 -0
  40. package/src/ui/screens/SkillsScreen.js +325 -0
  41. package/src/ui/screens/SkillsScreen.tsx +574 -0
  42. package/src/ui/screens/StatusLineScreen.js +2 -2
  43. package/src/ui/screens/StatusLineScreen.tsx +10 -12
  44. package/src/ui/screens/index.js +3 -2
  45. package/src/ui/screens/index.ts +3 -2
  46. package/src/ui/state/AppContext.js +2 -1
  47. package/src/ui/state/AppContext.tsx +2 -0
  48. package/src/ui/state/reducer.js +151 -19
  49. package/src/ui/state/reducer.ts +167 -19
  50. package/src/ui/state/types.ts +58 -14
  51. package/src/utils/clipboard.js +56 -0
  52. 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"><strong>{selectedServer.name}</strong></text>
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><strong>Version: </strong></text>
249
+ <text>
250
+ <strong>Version: </strong>
251
+ </text>
248
252
  <text fg="green">{versionDisplay}</text>
249
253
  </box>
250
254
  <box>
251
- <text><strong>Published: </strong></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><strong>URL:</strong></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><strong>Source:</strong></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><strong>{server.name}</strong></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: "blue", 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: [_jsx("text", { bg: "red", fg: "white", children: " Enter " }), _jsx("text", { fg: "gray", children: " Remove server" })] })) : (_jsxs("box", { children: [_jsx("text", { bg: "green", fg: "black", children: " Enter " }), _jsx("text", { fg: "gray", children: " Install server" })] })) })] }));
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"><strong>⚡ {server.name}</strong></text>
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="blue">{server.url}</text>
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"> Enter </text>
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"> Enter </text>
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"><strong>▸ {item.label}</strong></text>
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: "blue", children: [" ", item.provider] })] }));
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="blue"> {item.provider}</text>}
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 ? <u><strong>{seg.text}</strong></u> : seg.text}
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,8 +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, saveGlobalInstalledPluginVersion, saveLocalInstalledPluginVersion, } from "../../services/claude-settings.js";
13
- import { saveInstalledPluginVersion, } from "../../services/plugin-manager.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";
14
15
  import { installPlugin as cliInstallPlugin, uninstallPlugin as cliUninstallPlugin, updatePlugin as cliUpdatePlugin, } from "../../services/claude-cli.js";
15
16
  import { getPluginEnvRequirements, getPluginSourcePath, } from "../../services/plugin-mcp-config.js";
16
17
  import { getPluginSetupFromSource, checkMissingDeps, installPluginDeps, } from "../../services/plugin-setup.js";
@@ -146,50 +147,78 @@ export function PluginsScreen() {
146
147
  const selectableItems = useMemo(() => {
147
148
  return filteredItems.filter((item) => item.type === "plugin" || item.type === "category");
148
149
  }, [filteredItems]);
149
- // Keyboard handling
150
+ // Keyboard handling — inline search with live filtering
150
151
  useKeyboard((event) => {
151
- // Handle search mode
152
- if (isSearchActive) {
153
- if (event.name === "escape") {
154
- dispatch({ type: "SET_SEARCHING", isSearching: false });
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) {
155
158
  dispatch({ type: "PLUGINS_SET_SEARCH", query: "" });
156
- }
157
- else if (event.name === "enter") {
158
159
  dispatch({ type: "SET_SEARCHING", isSearching: false });
159
- // Keep the search query, just exit search mode
160
+ dispatch({ type: "PLUGINS_SELECT", index: 0 });
161
+ }
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
+ }
160
175
  }
161
- else if (event.name === "backspace" || event.name === "delete") {
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)) {
162
182
  dispatch({
163
183
  type: "PLUGINS_SET_SEARCH",
164
- query: pluginsState.searchQuery.slice(0, -1),
184
+ query: pluginsState.searchQuery + event.name,
165
185
  });
186
+ dispatch({ type: "PLUGINS_SELECT", index: 0 });
187
+ return;
166
188
  }
167
- else if (event.name.length === 1 && !event.ctrl && !event.meta) {
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)) {
168
196
  dispatch({
169
197
  type: "PLUGINS_SET_SEARCH",
170
198
  query: pluginsState.searchQuery + event.name,
171
199
  });
200
+ dispatch({ type: "PLUGINS_SELECT", index: 0 });
201
+ return;
172
202
  }
203
+ const newIndex = Math.min(selectableItems.length - 1, pluginsState.selectedIndex + 1);
204
+ dispatch({ type: "PLUGINS_SELECT", index: newIndex });
173
205
  return;
174
206
  }
175
- if (state.modal)
176
- return;
177
- // Start search with /
178
- if (event.name === "/") {
179
- dispatch({ type: "SET_SEARCHING", isSearching: true });
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();
180
215
  return;
181
216
  }
182
- // Navigation
183
- if (event.name === "up" || event.name === "k") {
184
- const newIndex = Math.max(0, pluginsState.selectedIndex - 1);
185
- dispatch({ type: "PLUGINS_SELECT", index: newIndex });
186
- }
187
- else if (event.name === "down" || event.name === "j") {
188
- const newIndex = Math.min(selectableItems.length - 1, pluginsState.selectedIndex + 1);
189
- dispatch({ type: "PLUGINS_SELECT", index: newIndex });
190
- }
191
- // Collapse/expand marketplace
192
- 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 === ">") &&
193
222
  selectableItems[pluginsState.selectedIndex]?.marketplace) {
194
223
  const item = selectableItems[pluginsState.selectedIndex];
195
224
  if (item?.marketplace) {
@@ -198,44 +227,55 @@ export function PluginsScreen() {
198
227
  name: item.marketplace.name,
199
228
  });
200
229
  }
230
+ return;
201
231
  }
202
- // Refresh
203
- else if (event.name === "r") {
204
- handleRefresh();
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;
205
243
  }
206
- // New marketplace (show instructions)
207
- else if (event.name === "n") {
208
- handleShowAddMarketplaceInstructions();
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;
209
249
  }
210
- // Team config help
211
- else if (event.name === "t") {
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")
212
256
  handleShowTeamConfigHelp();
213
- }
214
- // Scope-specific toggle shortcuts (u/p/l)
215
- else if (event.name === "u") {
257
+ else if (event.name === "u")
216
258
  handleScopeToggle("user");
217
- }
218
- else if (event.name === "p") {
259
+ else if (event.name === "p")
219
260
  handleScopeToggle("project");
220
- }
221
- else if (event.name === "l") {
261
+ else if (event.name === "l")
222
262
  handleScopeToggle("local");
223
- }
224
- // Update plugin (Shift+U)
225
- else if (event.name === "U") {
263
+ else if (event.name === "U")
226
264
  handleUpdate();
227
- }
228
- // Update all
229
- else if (event.name === "a") {
265
+ else if (event.name === "a")
230
266
  handleUpdateAll();
231
- }
232
- // Delete/uninstall
233
- else if (event.name === "d") {
267
+ else if (event.name === "d")
234
268
  handleUninstall();
235
- }
236
- // Enter for selection
237
- else if (event.name === "enter") {
238
- handleSelect();
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 });
239
279
  }
240
280
  });
241
281
  // Handle actions
@@ -367,7 +407,8 @@ export function PluginsScreen() {
367
407
  const hasMissing = (missing.pip?.length || 0) +
368
408
  (missing.brew?.length || 0) +
369
409
  (missing.npm?.length || 0) +
370
- (missing.cargo?.length || 0) > 0;
410
+ (missing.cargo?.length || 0) >
411
+ 0;
371
412
  if (!hasMissing)
372
413
  return;
373
414
  // Build description of what will be installed
@@ -654,6 +695,37 @@ export function PluginsScreen() {
654
695
  await modal.message("Error", `Failed: ${error}`, "error");
655
696
  }
656
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
+ };
657
729
  const handleUninstall = async () => {
658
730
  const item = selectableItems[pluginsState.selectedIndex];
659
731
  if (!item || item.type !== "plugin" || !item.plugin)
@@ -747,7 +819,11 @@ export function PluginsScreen() {
747
819
  const plugin = item.plugin;
748
820
  let statusIcon = "○";
749
821
  let statusColor = "gray";
750
- if (plugin.enabled) {
822
+ if (plugin.isOrphaned) {
823
+ statusIcon = "x";
824
+ statusColor = "red";
825
+ }
826
+ else if (plugin.enabled) {
751
827
  statusIcon = "●";
752
828
  statusColor = "green";
753
829
  }
@@ -757,7 +833,10 @@ export function PluginsScreen() {
757
833
  }
758
834
  // Build version string
759
835
  let versionStr = "";
760
- if (plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
836
+ if (plugin.isOrphaned) {
837
+ versionStr = " deprecated";
838
+ }
839
+ else if (plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
761
840
  versionStr = ` v${plugin.installedVersion}`;
762
841
  if (plugin.hasUpdate && plugin.version) {
763
842
  versionStr += ` → v${plugin.version}`;
@@ -774,6 +853,11 @@ export function PluginsScreen() {
774
853
  const displayName = segments
775
854
  ? segments.map((seg) => seg.text).join("")
776
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
+ }
777
861
  return (_jsxs("text", { children: [_jsxs("span", { fg: statusColor, children: [" ", statusIcon, " "] }), _jsx("span", { children: displayName }), _jsx("span", { fg: plugin.hasUpdate ? "yellow" : "gray", children: versionStr })] }));
778
862
  }
779
863
  return _jsx("text", { fg: "gray", children: item.label });
@@ -811,11 +895,15 @@ export function PluginsScreen() {
811
895
  actionHint = "Remove";
812
896
  }
813
897
  }
814
- 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: "blue", children: ["github.com/", mp.source.repo] }), _jsxs("text", { children: ["Plugins: ", selectedItem.pluginCount || 0] }), _jsxs("box", { marginTop: 1, children: [_jsx("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" }) }))] }));
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" }) }))] }));
815
899
  }
816
900
  if (selectedItem.type === "plugin" && selectedItem.plugin) {
817
901
  const plugin = selectedItem.plugin;
818
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
+ }
819
907
  // Build component counts
820
908
  const components = [];
821
909
  if (plugin.agents?.length)
@@ -832,14 +920,14 @@ export function PluginsScreen() {
832
920
  // Show version only if valid (not null, not 0.0.0)
833
921
  const showVersion = plugin.version && plugin.version !== "0.0.0";
834
922
  const showInstalledVersion = plugin.installedVersion && plugin.installedVersion !== "0.0.0";
835
- 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: "blue", children: ["v", plugin.version] }), showInstalledVersion &&
836
- 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: [_jsx("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: [_jsx("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: [_jsx("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: [_jsx("text", { bg: "magenta", fg: "white", children: " U " }), _jsxs("text", { children: [" Update to v", plugin.version] })] })), _jsxs("box", { children: [_jsx("text", { bg: "red", fg: "white", children: " d " }), _jsx("text", { children: " Uninstall" })] })] }))] }));
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" })] })] }))] }));
837
925
  }
838
926
  return null;
839
927
  };
840
- const footerHints = isSearchActive
841
- ? "Type to search │ Enter Confirm │ Esc Cancel"
842
- : "u/p/l:scope │ U:update │ a:all │ d:remove │ n:addt:team /:search";
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:profiletype to search";
843
931
  // Calculate status for subtitle
844
932
  const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
845
933
  const plugins = pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];