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
@@ -17,7 +17,12 @@ import {
17
17
  import {
18
18
  setMcpEnvVar,
19
19
  getMcpEnvVars,
20
+ readSettings,
21
+ saveGlobalInstalledPluginVersion,
22
+ saveLocalInstalledPluginVersion,
20
23
  } from "../../services/claude-settings.js";
24
+ import { saveProfile } from "../../services/profiles.js";
25
+ import { saveInstalledPluginVersion } from "../../services/plugin-manager.js";
21
26
  import {
22
27
  installPlugin as cliInstallPlugin,
23
28
  uninstallPlugin as cliUninstallPlugin,
@@ -204,53 +209,87 @@ export function PluginsScreen() {
204
209
  );
205
210
  }, [filteredItems]);
206
211
 
207
- // Keyboard handling
212
+ // Keyboard handling — inline search with live filtering
208
213
  useKeyboard((event) => {
209
- // Handle search mode
210
- if (isSearchActive) {
211
- if (event.name === "escape") {
212
- dispatch({ type: "SET_SEARCHING", isSearching: false });
214
+ if (state.modal) return;
215
+
216
+ const hasQuery = pluginsState.searchQuery.length > 0;
217
+
218
+ // Escape: always clear search state fully
219
+ if (event.name === "escape") {
220
+ if (hasQuery || isSearchActive) {
213
221
  dispatch({ type: "PLUGINS_SET_SEARCH", query: "" });
214
- } else if (event.name === "enter") {
215
222
  dispatch({ type: "SET_SEARCHING", isSearching: false });
216
- // Keep the search query, just exit search mode
217
- } else if (event.name === "backspace" || event.name === "delete") {
218
- dispatch({
219
- type: "PLUGINS_SET_SEARCH",
220
- query: pluginsState.searchQuery.slice(0, -1),
221
- });
222
- } else if (event.name.length === 1 && !event.ctrl && !event.meta) {
223
- dispatch({
224
- type: "PLUGINS_SET_SEARCH",
225
- query: pluginsState.searchQuery + event.name,
226
- });
223
+ dispatch({ type: "PLUGINS_SELECT", index: 0 });
227
224
  }
225
+ // Don't return — let GlobalKeyHandler handle Escape too (for quit)
228
226
  return;
229
227
  }
230
228
 
231
- if (state.modal) return;
232
-
233
- // Start search with /
234
- if (event.name === "/") {
235
- dispatch({ type: "SET_SEARCHING", isSearching: true });
229
+ // Backspace: remove last char from search query
230
+ if (event.name === "backspace" || event.name === "delete") {
231
+ if (hasQuery) {
232
+ const newQuery = pluginsState.searchQuery.slice(0, -1);
233
+ dispatch({ type: "PLUGINS_SET_SEARCH", query: newQuery });
234
+ dispatch({ type: "PLUGINS_SELECT", index: 0 });
235
+ // If query becomes empty, exit search mode
236
+ if (newQuery.length === 0) {
237
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
238
+ }
239
+ }
236
240
  return;
237
241
  }
238
242
 
239
- // Navigation
243
+ // Navigation — always works (even during search)
240
244
  if (event.name === "up" || event.name === "k") {
245
+ // 'k' navigates when query is empty, otherwise appends to search
246
+ if (event.name === "k" && (hasQuery || isSearchActive)) {
247
+ dispatch({
248
+ type: "PLUGINS_SET_SEARCH",
249
+ query: pluginsState.searchQuery + event.name,
250
+ });
251
+ dispatch({ type: "PLUGINS_SELECT", index: 0 });
252
+ return;
253
+ }
241
254
  const newIndex = Math.max(0, pluginsState.selectedIndex - 1);
242
255
  dispatch({ type: "PLUGINS_SELECT", index: newIndex });
243
- } else if (event.name === "down" || event.name === "j") {
256
+ return;
257
+ }
258
+ if (event.name === "down" || event.name === "j") {
259
+ // 'j' navigates when query is empty, otherwise appends to search
260
+ if (event.name === "j" && (hasQuery || isSearchActive)) {
261
+ dispatch({
262
+ type: "PLUGINS_SET_SEARCH",
263
+ query: pluginsState.searchQuery + event.name,
264
+ });
265
+ dispatch({ type: "PLUGINS_SELECT", index: 0 });
266
+ return;
267
+ }
244
268
  const newIndex = Math.min(
245
269
  selectableItems.length - 1,
246
270
  pluginsState.selectedIndex + 1,
247
271
  );
248
272
  dispatch({ type: "PLUGINS_SELECT", index: newIndex });
273
+ return;
274
+ }
275
+
276
+ // Enter — exit search mode (keep filter active) + select/install
277
+ if (event.name === "enter") {
278
+ if (isSearchActive) {
279
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
280
+ // Keep the query — filter stays active, shortcuts resume
281
+ return;
282
+ }
283
+ handleSelect();
284
+ return;
249
285
  }
250
286
 
251
- // Collapse/expand marketplace
252
- else if (
253
- (event.name === "left" || event.name === "right" || event.name === "<" || event.name === ">") &&
287
+ // Collapse/expand marketplace — always works
288
+ if (
289
+ (event.name === "left" ||
290
+ event.name === "right" ||
291
+ event.name === "<" ||
292
+ event.name === ">") &&
254
293
  selectableItems[pluginsState.selectedIndex]?.marketplace
255
294
  ) {
256
295
  const item = selectableItems[pluginsState.selectedIndex];
@@ -260,50 +299,49 @@ export function PluginsScreen() {
260
299
  name: item.marketplace.name,
261
300
  });
262
301
  }
302
+ return;
263
303
  }
264
304
 
265
- // Refresh
266
- else if (event.name === "r") {
267
- handleRefresh();
268
- }
269
-
270
- // New marketplace (show instructions)
271
- else if (event.name === "n") {
272
- handleShowAddMarketplaceInstructions();
273
- }
274
-
275
- // Team config help
276
- else if (event.name === "t") {
277
- handleShowTeamConfigHelp();
278
- }
279
-
280
- // Scope-specific toggle shortcuts (u/p/l)
281
- else if (event.name === "u") {
282
- handleScopeToggle("user");
283
- } else if (event.name === "p") {
284
- handleScopeToggle("project");
285
- } else if (event.name === "l") {
286
- handleScopeToggle("local");
287
- }
288
-
289
- // Update plugin (Shift+U)
290
- else if (event.name === "U") {
291
- handleUpdate();
305
+ // When search query is non-empty, printable letters go to the query
306
+ // (shortcuts are suspended while filtering, digits skip to let tab nav work)
307
+ if (hasQuery || isSearchActive) {
308
+ if (event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
309
+ dispatch({
310
+ type: "PLUGINS_SET_SEARCH",
311
+ query: pluginsState.searchQuery + event.name,
312
+ });
313
+ dispatch({ type: "PLUGINS_SELECT", index: 0 });
314
+ }
315
+ return;
292
316
  }
293
317
 
294
- // Update all
295
- else if (event.name === "a") {
296
- handleUpdateAll();
297
- }
318
+ // When search query is empty: action shortcuts work normally
298
319
 
299
- // Delete/uninstall
300
- else if (event.name === "d") {
301
- handleUninstall();
320
+ // Start explicit search mode with /
321
+ if (event.name === "/") {
322
+ dispatch({ type: "SET_SEARCHING", isSearching: true });
323
+ return;
302
324
  }
303
325
 
304
- // Enter for selection
305
- else if (event.name === "enter") {
306
- handleSelect();
326
+ // Action shortcuts (only when query is empty)
327
+ if (event.name === "r") handleRefresh();
328
+ else if (event.name === "n") handleShowAddMarketplaceInstructions();
329
+ else if (event.name === "t") handleShowTeamConfigHelp();
330
+ else if (event.name === "u") handleScopeToggle("user");
331
+ else if (event.name === "p") handleScopeToggle("project");
332
+ else if (event.name === "l") handleScopeToggle("local");
333
+ else if (event.name === "U") handleUpdate();
334
+ else if (event.name === "a") handleUpdateAll();
335
+ else if (event.name === "d") handleUninstall();
336
+ else if (event.name === "s") handleSaveAsProfile();
337
+ // Any other printable letter: start inline search (skip digits — used for tab nav)
338
+ else if (event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
339
+ dispatch({ type: "SET_SEARCHING", isSearching: true });
340
+ dispatch({
341
+ type: "PLUGINS_SET_SEARCH",
342
+ query: event.name,
343
+ });
344
+ dispatch({ type: "PLUGINS_SELECT", index: 0 });
307
345
  }
308
346
  });
309
347
 
@@ -486,9 +524,10 @@ export function PluginsScreen() {
486
524
  const missing = await checkMissingDeps(setup);
487
525
  const hasMissing =
488
526
  (missing.pip?.length || 0) +
489
- (missing.brew?.length || 0) +
490
- (missing.npm?.length || 0) +
491
- (missing.cargo?.length || 0) > 0;
527
+ (missing.brew?.length || 0) +
528
+ (missing.npm?.length || 0) +
529
+ (missing.cargo?.length || 0) >
530
+ 0;
492
531
 
493
532
  if (!hasMissing) return;
494
533
 
@@ -497,7 +536,8 @@ export function PluginsScreen() {
497
536
  if (missing.pip?.length) parts.push(`pip: ${missing.pip.join(", ")}`);
498
537
  if (missing.brew?.length) parts.push(`brew: ${missing.brew.join(", ")}`);
499
538
  if (missing.npm?.length) parts.push(`npm: ${missing.npm.join(", ")}`);
500
- if (missing.cargo?.length) parts.push(`cargo: ${missing.cargo.join(", ")}`);
539
+ if (missing.cargo?.length)
540
+ parts.push(`cargo: ${missing.cargo.join(", ")}`);
501
541
 
502
542
  const wantInstall = await modal.confirm(
503
543
  "Install Dependencies?",
@@ -531,6 +571,33 @@ export function PluginsScreen() {
531
571
  }
532
572
  };
533
573
 
574
+ /**
575
+ * Save the installed version to settings after CLI install/update.
576
+ * Claude CLI doesn't update installedPluginVersions in settings.json,
577
+ * so we do it ourselves to keep the TUI version display accurate.
578
+ */
579
+ const saveVersionAfterInstall = async (
580
+ pluginId: string,
581
+ version: string,
582
+ scope: PluginScope,
583
+ ): Promise<void> => {
584
+ try {
585
+ if (scope === "user") {
586
+ await saveGlobalInstalledPluginVersion(pluginId, version);
587
+ } else if (scope === "local") {
588
+ await saveLocalInstalledPluginVersion(
589
+ pluginId,
590
+ version,
591
+ state.projectPath,
592
+ );
593
+ } else {
594
+ await saveInstalledPluginVersion(pluginId, version, state.projectPath);
595
+ }
596
+ } catch {
597
+ // Non-fatal: version display may be stale but plugin still works
598
+ }
599
+ };
600
+
534
601
  const handleSelect = async () => {
535
602
  const item = selectableItems[pluginsState.selectedIndex];
536
603
  if (!item) return;
@@ -663,8 +730,10 @@ export function PluginsScreen() {
663
730
  await cliUninstallPlugin(plugin.id, scope, state.projectPath);
664
731
  } else if (action === "update") {
665
732
  await cliUpdatePlugin(plugin.id, scope);
733
+ await saveVersionAfterInstall(plugin.id, latestVersion, scope);
666
734
  } else {
667
735
  await cliInstallPlugin(plugin.id, scope);
736
+ await saveVersionAfterInstall(plugin.id, latestVersion, scope);
668
737
 
669
738
  // On fresh install, configure env vars and install system deps
670
739
  modal.hideModal();
@@ -687,11 +756,17 @@ export function PluginsScreen() {
687
756
  if (!item || item.type !== "plugin" || !item.plugin?.hasUpdate) return;
688
757
 
689
758
  const plugin = item.plugin;
690
- const scope: PluginScope = pluginsState.scope === "global" ? "user" : "project";
759
+ const scope: PluginScope =
760
+ pluginsState.scope === "global" ? "user" : "project";
691
761
 
692
762
  modal.loading(`Updating ${plugin.name}...`);
693
763
  try {
694
764
  await cliUpdatePlugin(plugin.id, scope);
765
+ await saveVersionAfterInstall(
766
+ plugin.id,
767
+ plugin.version || "0.0.0",
768
+ scope,
769
+ );
695
770
  modal.hideModal();
696
771
  fetchData();
697
772
  } catch (error) {
@@ -706,12 +781,18 @@ export function PluginsScreen() {
706
781
  const updatable = pluginsState.plugins.data.filter((p) => p.hasUpdate);
707
782
  if (updatable.length === 0) return;
708
783
 
709
- const scope: PluginScope = pluginsState.scope === "global" ? "user" : "project";
784
+ const scope: PluginScope =
785
+ pluginsState.scope === "global" ? "user" : "project";
710
786
  modal.loading(`Updating ${updatable.length} plugin(s)...`);
711
787
 
712
788
  try {
713
789
  for (const plugin of updatable) {
714
790
  await cliUpdatePlugin(plugin.id, scope);
791
+ await saveVersionAfterInstall(
792
+ plugin.id,
793
+ plugin.version || "0.0.0",
794
+ scope,
795
+ );
715
796
  }
716
797
  modal.hideModal();
717
798
  fetchData();
@@ -771,8 +852,10 @@ export function PluginsScreen() {
771
852
  await cliUninstallPlugin(plugin.id, scope, state.projectPath);
772
853
  } else if (action === "update") {
773
854
  await cliUpdatePlugin(plugin.id, scope);
855
+ await saveVersionAfterInstall(plugin.id, latestVersion, scope);
774
856
  } else {
775
857
  await cliInstallPlugin(plugin.id, scope);
858
+ await saveVersionAfterInstall(plugin.id, latestVersion, scope);
776
859
 
777
860
  // On fresh install, configure env vars and install system deps
778
861
  modal.hideModal();
@@ -789,6 +872,47 @@ export function PluginsScreen() {
789
872
  }
790
873
  };
791
874
 
875
+ const handleSaveAsProfile = async () => {
876
+ // Read current enabledPlugins from project settings
877
+ const settings = await readSettings(state.projectPath);
878
+ const enabledPlugins = settings.enabledPlugins ?? {};
879
+
880
+ const name = await modal.input("Save Profile", "Profile name:");
881
+ if (name === null || !name.trim()) return;
882
+
883
+ const scopeChoice = await modal.select(
884
+ "Save to scope",
885
+ "Where should this profile be saved?",
886
+ [
887
+ {
888
+ label: "User — ~/.claude/profiles.json (available everywhere)",
889
+ value: "user",
890
+ },
891
+ {
892
+ label: "Project — .claude/profiles.json (shared with team via git)",
893
+ value: "project",
894
+ },
895
+ ],
896
+ );
897
+ if (scopeChoice === null) return;
898
+
899
+ const scope = scopeChoice as "user" | "project";
900
+
901
+ modal.loading("Saving profile...");
902
+ try {
903
+ await saveProfile(name.trim(), enabledPlugins, scope, state.projectPath);
904
+ modal.hideModal();
905
+ await modal.message(
906
+ "Saved",
907
+ `Profile "${name.trim()}" saved.\nPress 6 to manage profiles.`,
908
+ "success",
909
+ );
910
+ } catch (error) {
911
+ modal.hideModal();
912
+ await modal.message("Error", `Failed to save profile: ${error}`, "error");
913
+ }
914
+ };
915
+
792
916
  const handleUninstall = async () => {
793
917
  const item = selectableItems[pluginsState.selectedIndex];
794
918
  if (!item || item.type !== "plugin" || !item.plugin) return;
@@ -836,7 +960,11 @@ export function PluginsScreen() {
836
960
  modal.loading(`Uninstalling ${plugin.name}...`);
837
961
 
838
962
  try {
839
- await cliUninstallPlugin(plugin.id, scopeValue as PluginScope, state.projectPath);
963
+ await cliUninstallPlugin(
964
+ plugin.id,
965
+ scopeValue as PluginScope,
966
+ state.projectPath,
967
+ );
840
968
  modal.hideModal();
841
969
  fetchData();
842
970
  } catch (error) {
@@ -852,7 +980,9 @@ export function PluginsScreen() {
852
980
  ) {
853
981
  return (
854
982
  <box flexDirection="column" paddingLeft={1} paddingRight={1}>
855
- <text fg="#7e57c2"><strong>claudeup Plugins</strong></text>
983
+ <text fg="#7e57c2">
984
+ <strong>claudeup Plugins</strong>
985
+ </text>
856
986
  <text fg="gray">Loading...</text>
857
987
  </box>
858
988
  );
@@ -865,7 +995,9 @@ export function PluginsScreen() {
865
995
  ) {
866
996
  return (
867
997
  <box flexDirection="column" paddingLeft={1} paddingRight={1}>
868
- <text fg="#7e57c2"><strong>claudeup Plugins</strong></text>
998
+ <text fg="#7e57c2">
999
+ <strong>claudeup Plugins</strong>
1000
+ </text>
869
1001
  <text fg="red">Error loading data</text>
870
1002
  </box>
871
1003
  );
@@ -908,7 +1040,13 @@ export function PluginsScreen() {
908
1040
  ? ` (${item.pluginCount})`
909
1041
  : "";
910
1042
  return (
911
- <text bg="magenta" fg="white"><strong> {arrow} {mp.displayName}{count} </strong></text>
1043
+ <text bg="magenta" fg="white">
1044
+ <strong>
1045
+ {" "}
1046
+ {arrow} {mp.displayName}
1047
+ {count}{" "}
1048
+ </strong>
1049
+ </text>
912
1050
  );
913
1051
  }
914
1052
 
@@ -928,7 +1066,10 @@ export function PluginsScreen() {
928
1066
  let statusIcon = "○";
929
1067
  let statusColor = "gray";
930
1068
 
931
- if (plugin.enabled) {
1069
+ if (plugin.isOrphaned) {
1070
+ statusIcon = "x";
1071
+ statusColor = "red";
1072
+ } else if (plugin.enabled) {
932
1073
  statusIcon = "●";
933
1074
  statusColor = "green";
934
1075
  } else if (plugin.installedVersion) {
@@ -938,7 +1079,9 @@ export function PluginsScreen() {
938
1079
 
939
1080
  // Build version string
940
1081
  let versionStr = "";
941
- if (plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
1082
+ if (plugin.isOrphaned) {
1083
+ versionStr = " deprecated";
1084
+ } else if (plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
942
1085
  versionStr = ` v${plugin.installedVersion}`;
943
1086
  if (plugin.hasUpdate && plugin.version) {
944
1087
  versionStr += ` → v${plugin.version}`;
@@ -962,9 +1105,26 @@ export function PluginsScreen() {
962
1105
  const displayName = segments
963
1106
  ? segments.map((seg) => seg.text).join("")
964
1107
  : plugin.name;
1108
+
1109
+ if (plugin.isOrphaned) {
1110
+ const ver = plugin.installedVersion && plugin.installedVersion !== "0.0.0"
1111
+ ? ` v${plugin.installedVersion}` : "";
1112
+ return (
1113
+ <text>
1114
+ <span fg="red">{" "}{statusIcon} </span>
1115
+ <span fg="gray">{displayName}</span>
1116
+ {ver && <span fg="yellow">{ver}</span>}
1117
+ <span fg="red"> deprecated</span>
1118
+ </text>
1119
+ );
1120
+ }
1121
+
965
1122
  return (
966
1123
  <text>
967
- <span fg={statusColor}>{" "}{statusIcon} </span>
1124
+ <span fg={statusColor}>
1125
+ {" "}
1126
+ {statusIcon}{" "}
1127
+ </span>
968
1128
  <span>{displayName}</span>
969
1129
  <span fg={plugin.hasUpdate ? "yellow" : "gray"}>{versionStr}</span>
970
1130
  </text>
@@ -1008,15 +1168,23 @@ export function PluginsScreen() {
1008
1168
 
1009
1169
  return (
1010
1170
  <box flexDirection="column">
1011
- <text fg="cyan"><strong>{mp.displayName}{getBadge()}</strong></text>
1171
+ <text fg="cyan">
1172
+ <strong>
1173
+ {mp.displayName}
1174
+ {getBadge()}
1175
+ </strong>
1176
+ </text>
1012
1177
  <text fg="gray">{mp.description || "No description"}</text>
1013
1178
  <text fg={isEnabled ? "green" : "gray"}>
1014
1179
  {isEnabled ? "● Added" : "○ Not added"}
1015
1180
  </text>
1016
- <text fg="blue">github.com/{mp.source.repo}</text>
1181
+ <text fg="#5c9aff">github.com/{mp.source.repo}</text>
1017
1182
  <text>Plugins: {selectedItem.pluginCount || 0}</text>
1018
1183
  <box marginTop={1}>
1019
- <text bg={isEnabled ? "cyan" : "green"} fg="black"> Enter </text>
1184
+ <text bg={isEnabled ? "cyan" : "green"} fg="black">
1185
+ {" "}
1186
+ Enter{" "}
1187
+ </text>
1020
1188
  <text fg="gray"> {actionHint}</text>
1021
1189
  </box>
1022
1190
  {isEnabled && (
@@ -1032,6 +1200,31 @@ export function PluginsScreen() {
1032
1200
  const plugin = selectedItem.plugin;
1033
1201
  const isInstalled = plugin.enabled || plugin.installedVersion;
1034
1202
 
1203
+ // Orphaned/deprecated plugin
1204
+ if (plugin.isOrphaned) {
1205
+ return (
1206
+ <box flexDirection="column">
1207
+ <box justifyContent="center">
1208
+ <text bg="yellow" fg="black"><strong> {plugin.name} — DEPRECATED </strong></text>
1209
+ </box>
1210
+ <box marginTop={1}>
1211
+ <text fg="yellow">This plugin is no longer in the marketplace.</text>
1212
+ </box>
1213
+ <box marginTop={1}>
1214
+ <text fg="gray">It was removed from the marketplace but still referenced in your settings. Press d to uninstall and clean up.</text>
1215
+ </box>
1216
+ {isInstalled && (
1217
+ <box flexDirection="column" marginTop={2}>
1218
+ <box>
1219
+ <text bg="red" fg="white"> d </text>
1220
+ <text> Uninstall (recommended)</text>
1221
+ </box>
1222
+ </box>
1223
+ )}
1224
+ </box>
1225
+ );
1226
+ }
1227
+
1035
1228
  // Build component counts
1036
1229
  const components: string[] = [];
1037
1230
  if (plugin.agents?.length)
@@ -1055,7 +1248,13 @@ export function PluginsScreen() {
1055
1248
  <box flexDirection="column">
1056
1249
  {/* Plugin name header - centered */}
1057
1250
  <box justifyContent="center">
1058
- <text bg="magenta" fg="white"><strong> {plugin.name}{plugin.hasUpdate ? " ⬆" : ""} </strong></text>
1251
+ <text bg="magenta" fg="white">
1252
+ <strong>
1253
+ {" "}
1254
+ {plugin.name}
1255
+ {plugin.hasUpdate ? " ⬆" : ""}{" "}
1256
+ </strong>
1257
+ </text>
1059
1258
  </box>
1060
1259
 
1061
1260
  {/* Status line */}
@@ -1078,7 +1277,7 @@ export function PluginsScreen() {
1078
1277
  {showVersion && (
1079
1278
  <text>
1080
1279
  <span>Version </span>
1081
- <span fg="blue">v{plugin.version}</span>
1280
+ <span fg="#5c9aff">v{plugin.version}</span>
1082
1281
  {showInstalledVersion &&
1083
1282
  plugin.installedVersion !== plugin.version && (
1084
1283
  <span> (v{plugin.installedVersion} installed)</span>
@@ -1107,10 +1306,15 @@ export function PluginsScreen() {
1107
1306
  {/* Scope Status with shortcuts - each scope has its own color */}
1108
1307
  <box flexDirection="column" marginTop={1}>
1109
1308
  <text>────────────────────────</text>
1110
- <text><strong>Scopes:</strong></text>
1309
+ <text>
1310
+ <strong>Scopes:</strong>
1311
+ </text>
1111
1312
  <box marginTop={1} flexDirection="column">
1112
1313
  <text>
1113
- <span bg="cyan" fg="black"> u </span>
1314
+ <span bg="cyan" fg="black">
1315
+ {" "}
1316
+ u{" "}
1317
+ </span>
1114
1318
  <span fg={plugin.userScope?.enabled ? "cyan" : "gray"}>
1115
1319
  {plugin.userScope?.enabled ? " ● " : " ○ "}
1116
1320
  </span>
@@ -1121,7 +1325,10 @@ export function PluginsScreen() {
1121
1325
  )}
1122
1326
  </text>
1123
1327
  <text>
1124
- <span bg="green" fg="black"> p </span>
1328
+ <span bg="green" fg="black">
1329
+ {" "}
1330
+ p{" "}
1331
+ </span>
1125
1332
  <span fg={plugin.projectScope?.enabled ? "green" : "gray"}>
1126
1333
  {plugin.projectScope?.enabled ? " ● " : " ○ "}
1127
1334
  </span>
@@ -1132,7 +1339,10 @@ export function PluginsScreen() {
1132
1339
  )}
1133
1340
  </text>
1134
1341
  <text>
1135
- <span bg="yellow" fg="black"> l </span>
1342
+ <span bg="yellow" fg="black">
1343
+ {" "}
1344
+ l{" "}
1345
+ </span>
1136
1346
  <span fg={plugin.localScope?.enabled ? "yellow" : "gray"}>
1137
1347
  {plugin.localScope?.enabled ? " ● " : " ○ "}
1138
1348
  </span>
@@ -1150,12 +1360,18 @@ export function PluginsScreen() {
1150
1360
  <box flexDirection="column" marginTop={1}>
1151
1361
  {plugin.hasUpdate && (
1152
1362
  <box>
1153
- <text bg="magenta" fg="white"> U </text>
1363
+ <text bg="magenta" fg="white">
1364
+ {" "}
1365
+ U{" "}
1366
+ </text>
1154
1367
  <text> Update to v{plugin.version}</text>
1155
1368
  </box>
1156
1369
  )}
1157
1370
  <box>
1158
- <text bg="red" fg="white"> d </text>
1371
+ <text bg="red" fg="white">
1372
+ {" "}
1373
+ d{" "}
1374
+ </text>
1159
1375
  <text> Uninstall</text>
1160
1376
  </box>
1161
1377
  </box>
@@ -1167,9 +1383,10 @@ export function PluginsScreen() {
1167
1383
  return null;
1168
1384
  };
1169
1385
 
1170
- const footerHints = isSearchActive
1171
- ? "Type to search │ Enter Confirm │ Esc Cancel"
1172
- : "u/p/l:scopeU:updatea:alld:remove n:add │ t:team │ /:search";
1386
+ const footerHints =
1387
+ isSearchActive || pluginsState.searchQuery
1388
+ ? "↑↓:navEnter:selectEsc:cleartype to filter"
1389
+ : "u/p/l:scope │ U:update │ a:all │ d:remove │ s:profile │ type to search";
1173
1390
 
1174
1391
  // Calculate status for subtitle
1175
1392
  const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";