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.
Files changed (46) 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/prerunner/index.js +2 -1
  5. package/src/prerunner/index.ts +2 -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/types/index.ts +34 -0
  13. package/src/ui/App.js +17 -18
  14. package/src/ui/App.tsx +21 -23
  15. package/src/ui/components/TabBar.js +8 -8
  16. package/src/ui/components/TabBar.tsx +14 -19
  17. package/src/ui/components/layout/ScreenLayout.js +8 -14
  18. package/src/ui/components/layout/ScreenLayout.tsx +51 -58
  19. package/src/ui/components/modals/ModalContainer.js +43 -11
  20. package/src/ui/components/modals/ModalContainer.tsx +44 -12
  21. package/src/ui/components/modals/SelectModal.js +4 -18
  22. package/src/ui/components/modals/SelectModal.tsx +10 -21
  23. package/src/ui/screens/CliToolsScreen.js +2 -2
  24. package/src/ui/screens/CliToolsScreen.tsx +8 -8
  25. package/src/ui/screens/EnvVarsScreen.js +248 -116
  26. package/src/ui/screens/EnvVarsScreen.tsx +419 -184
  27. package/src/ui/screens/McpRegistryScreen.tsx +18 -6
  28. package/src/ui/screens/McpScreen.js +1 -1
  29. package/src/ui/screens/McpScreen.tsx +15 -5
  30. package/src/ui/screens/ModelSelectorScreen.js +3 -5
  31. package/src/ui/screens/ModelSelectorScreen.tsx +12 -16
  32. package/src/ui/screens/PluginsScreen.js +181 -65
  33. package/src/ui/screens/PluginsScreen.tsx +308 -91
  34. package/src/ui/screens/ProfilesScreen.js +255 -0
  35. package/src/ui/screens/ProfilesScreen.tsx +487 -0
  36. package/src/ui/screens/StatusLineScreen.js +2 -2
  37. package/src/ui/screens/StatusLineScreen.tsx +10 -12
  38. package/src/ui/screens/index.js +2 -2
  39. package/src/ui/screens/index.ts +2 -2
  40. package/src/ui/state/AppContext.js +2 -1
  41. package/src/ui/state/AppContext.tsx +2 -0
  42. package/src/ui/state/reducer.js +63 -19
  43. package/src/ui/state/reducer.ts +68 -19
  44. package/src/ui/state/types.ts +33 -14
  45. package/src/utils/clipboard.js +56 -0
  46. 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,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
- // Handle search mode
151
- if (isSearchActive) {
152
- if (event.name === "escape") {
153
- 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) {
154
158
  dispatch({ type: "PLUGINS_SET_SEARCH", query: "" });
155
- }
156
- else if (event.name === "enter") {
157
159
  dispatch({ type: "SET_SEARCHING", isSearching: false });
158
- // Keep the search query, just exit search mode
160
+ dispatch({ type: "PLUGINS_SELECT", index: 0 });
159
161
  }
160
- else if (event.name === "backspace" || event.name === "delete") {
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.slice(0, -1),
184
+ query: pluginsState.searchQuery + event.name,
164
185
  });
186
+ dispatch({ type: "PLUGINS_SELECT", index: 0 });
187
+ return;
165
188
  }
166
- 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)) {
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
- if (state.modal)
175
- return;
176
- // Start search with /
177
- if (event.name === "/") {
178
- 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();
179
215
  return;
180
216
  }
181
- // Navigation
182
- if (event.name === "up" || event.name === "k") {
183
- const newIndex = Math.max(0, pluginsState.selectedIndex - 1);
184
- dispatch({ type: "PLUGINS_SELECT", index: newIndex });
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
- // Refresh
202
- else if (event.name === "r") {
203
- 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;
204
243
  }
205
- // New marketplace (show instructions)
206
- else if (event.name === "n") {
207
- 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;
208
249
  }
209
- // Team config help
210
- 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")
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
- // Enter for selection
236
- else if (event.name === "enter") {
237
- 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 });
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) > 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.enabled) {
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.installedVersion && plugin.installedVersion !== "0.0.0") {
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: "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" }) }))] }));
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: "blue", children: ["v", plugin.version] }), showInstalledVersion &&
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: [_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" })] })] }))] }));
809
925
  }
810
926
  return null;
811
927
  };
812
- const footerHints = isSearchActive
813
- ? "Type to search │ Enter Confirm │ Esc Cancel"
814
- : "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";
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 : [];