claudeup 4.17.0 → 4.18.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 (76) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/alias-parser.test.ts +317 -0
  3. package/src/__tests__/alias-shell-writer.test.ts +661 -0
  4. package/src/__tests__/alias-store.test.ts +86 -0
  5. package/src/__tests__/gitignore-fixer.test.ts +64 -1
  6. package/src/__tests__/gitignore-prerun.test.ts +2 -2
  7. package/src/__tests__/gitignore-service.test.ts +42 -0
  8. package/src/__tests__/marketplaces.test.ts +40 -0
  9. package/src/__tests__/plugin-manager-fallback.test.ts +120 -0
  10. package/src/__tests__/useGitignoreModal.test.ts +2 -2
  11. package/src/data/alias-flags.js +196 -0
  12. package/src/data/alias-flags.ts +291 -0
  13. package/src/data/gitignore-reasons.js +97 -0
  14. package/src/data/gitignore-reasons.ts +103 -0
  15. package/src/data/marketplaces.js +5 -3
  16. package/src/data/marketplaces.ts +5 -4
  17. package/src/services/alias-settings.js +51 -0
  18. package/src/services/alias-settings.ts +63 -0
  19. package/src/services/alias-shell-writer.js +764 -0
  20. package/src/services/alias-shell-writer.ts +873 -0
  21. package/src/services/alias-store.js +77 -0
  22. package/src/services/alias-store.ts +112 -0
  23. package/src/services/gitignore-fixer.js +70 -10
  24. package/src/services/gitignore-fixer.ts +76 -9
  25. package/src/services/gitignore-prerun.js +3 -3
  26. package/src/services/gitignore-prerun.ts +3 -3
  27. package/src/services/gitignore-service.js +20 -2
  28. package/src/services/gitignore-service.ts +23 -1
  29. package/src/services/marketplace-fetcher.js +96 -0
  30. package/src/services/marketplace-fetcher.ts +137 -0
  31. package/src/services/plugin-manager.js +6 -59
  32. package/src/services/plugin-manager.ts +16 -91
  33. package/src/services/skillsmp-client.js +29 -9
  34. package/src/services/skillsmp-client.ts +38 -8
  35. package/src/types/gitignore.ts +1 -1
  36. package/src/ui/App.js +10 -4
  37. package/src/ui/App.tsx +9 -3
  38. package/src/ui/components/TabBar.js +2 -1
  39. package/src/ui/components/TabBar.tsx +2 -1
  40. package/src/ui/components/layout/FooterHints.js +29 -0
  41. package/src/ui/components/layout/FooterHints.tsx +52 -0
  42. package/src/ui/components/layout/ScreenLayout.js +2 -1
  43. package/src/ui/components/layout/ScreenLayout.tsx +12 -3
  44. package/src/ui/components/layout/index.js +1 -0
  45. package/src/ui/components/layout/index.ts +5 -0
  46. package/src/ui/components/modals/SelectModal.js +8 -1
  47. package/src/ui/components/modals/SelectModal.tsx +12 -1
  48. package/src/ui/hooks/useGitignoreModal.js +7 -8
  49. package/src/ui/hooks/useGitignoreModal.ts +8 -9
  50. package/src/ui/renderers/gitignoreRenderers.js +36 -23
  51. package/src/ui/renderers/gitignoreRenderers.tsx +50 -41
  52. package/src/ui/screens/AliasScreen.js +1008 -0
  53. package/src/ui/screens/AliasScreen.tsx +1402 -0
  54. package/src/ui/screens/CliToolsScreen.js +6 -1
  55. package/src/ui/screens/CliToolsScreen.tsx +6 -1
  56. package/src/ui/screens/EnvVarsScreen.js +6 -1
  57. package/src/ui/screens/EnvVarsScreen.tsx +6 -1
  58. package/src/ui/screens/GitignoreScreen.js +189 -88
  59. package/src/ui/screens/GitignoreScreen.tsx +312 -132
  60. package/src/ui/screens/McpRegistryScreen.js +13 -2
  61. package/src/ui/screens/McpRegistryScreen.tsx +13 -2
  62. package/src/ui/screens/McpScreen.js +6 -1
  63. package/src/ui/screens/McpScreen.tsx +6 -1
  64. package/src/ui/screens/ModelSelectorScreen.js +8 -2
  65. package/src/ui/screens/ModelSelectorScreen.tsx +8 -2
  66. package/src/ui/screens/PluginsScreen.js +13 -2
  67. package/src/ui/screens/PluginsScreen.tsx +13 -2
  68. package/src/ui/screens/ProfilesScreen.js +8 -1
  69. package/src/ui/screens/ProfilesScreen.tsx +8 -1
  70. package/src/ui/screens/SkillsScreen.js +21 -4
  71. package/src/ui/screens/SkillsScreen.tsx +39 -5
  72. package/src/ui/screens/StatusLineScreen.js +7 -1
  73. package/src/ui/screens/StatusLineScreen.tsx +7 -1
  74. package/src/ui/screens/index.js +1 -0
  75. package/src/ui/screens/index.ts +1 -0
  76. package/src/ui/state/types.ts +4 -2
@@ -217,7 +217,12 @@ export function McpScreen() {
217
217
  title="claudeup MCP Servers"
218
218
  subtitle={subtitle}
219
219
  currentScreen="mcp"
220
- footerHints="↑↓:nav │ Enter:toggle │ /:search │ r:registry"
220
+ footerHints={[
221
+ { keys: ["↑", "↓"], label: "nav" },
222
+ { keys: ["Enter"], label: "toggle" },
223
+ { keys: ["/"], label: "search" },
224
+ { keys: ["r"], label: "registry" },
225
+ ]}
221
226
  listPanel={
222
227
  <ScrollableList
223
228
  items={allListItems}
@@ -5,6 +5,7 @@ import { useDimensions } from "../state/DimensionsContext.js";
5
5
  import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
6
6
  import { useKeyboard } from "../hooks/useKeyboard.js";
7
7
  import { ScrollableList } from "../components/ScrollableList.js";
8
+ import { FooterHints } from "../components/layout/index.js";
8
9
  const RECENT_MODELS = [
9
10
  {
10
11
  id: "recent-1",
@@ -278,13 +279,18 @@ export function ModelSelectorScreen() {
278
279
  return (_jsxs("box", { overflow: "hidden", children: [_jsx(Label, {}), _jsx("box", { flexGrow: 1 }), item.provider && _jsx("text", { fg: "gray", children: item.provider })] }));
279
280
  }
280
281
  };
281
- const footerHints = "↑↓ choose • tab toggle type • enter choose • esc exit";
282
+ const footerHints = [
283
+ { keys: ["↑", "↓"], label: "choose" },
284
+ { keys: ["tab"], label: "toggle type" },
285
+ { keys: ["enter"], label: "choose" },
286
+ { keys: ["esc"], label: "exit" },
287
+ ];
282
288
  // Available height calculation
283
289
  // Header: 1 line (title) + 1 line (search) + 1 line (separator) = 3 lines?
284
290
  // Title line: Switch Model /// (o) Large
285
291
  // Cursor line: > c
286
292
  // Separator: handled by list? or explicit?
287
293
  const listHeight = Math.max(5, dimensions.contentHeight - 5);
288
- return (_jsxs("box", { flexDirection: "column", height: dimensions.contentHeight, children: [_jsx("box", { flexDirection: "row", border: true, borderStyle: "single", borderColor: "#7e57c2", paddingLeft: 1, paddingRight: 1, children: _jsxs("box", { flexDirection: "column", flexGrow: 1, children: [_jsx("box", { flexDirection: "row", justifyContent: "space-between", children: _jsxs("box", { children: [_jsx("text", { fg: "#7e57c2", children: "Switch Model " }), _jsxs("text", { fg: modelSelector.taskSize === "large" ? "white" : "gray", children: [modelSelector.taskSize === "large" ? "◎" : "○", " Large Task", " "] }), _jsxs("text", { fg: modelSelector.taskSize === "small" ? "white" : "gray", children: [modelSelector.taskSize === "small" ? "◎" : "○", " Small Task"] })] }) }), _jsxs("box", { flexDirection: "row", marginTop: 1, children: [_jsx("text", { fg: "green", children: "> " }), _jsx("text", { children: modelSelector.searchQuery }), _jsx("text", { bg: "gray", fg: "black", children: " " })] })] }) }), _jsx("box", { flexGrow: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(ScrollableList, { items: filteredItems, selectedIndex: modelSelector.selectedIndex, renderItem: renderItem, maxHeight: listHeight, getKey: (item) => item.id }) }), _jsx("box", { height: 1, children: _jsx("text", { fg: "#888888", children: footerHints }) })] }));
294
+ return (_jsxs("box", { flexDirection: "column", height: dimensions.contentHeight, children: [_jsx("box", { flexDirection: "row", border: true, borderStyle: "single", borderColor: "#7e57c2", paddingLeft: 1, paddingRight: 1, children: _jsxs("box", { flexDirection: "column", flexGrow: 1, children: [_jsx("box", { flexDirection: "row", justifyContent: "space-between", children: _jsxs("box", { children: [_jsx("text", { fg: "#7e57c2", children: "Switch Model " }), _jsxs("text", { fg: modelSelector.taskSize === "large" ? "white" : "gray", children: [modelSelector.taskSize === "large" ? "◎" : "○", " Large Task", " "] }), _jsxs("text", { fg: modelSelector.taskSize === "small" ? "white" : "gray", children: [modelSelector.taskSize === "small" ? "◎" : "○", " Small Task"] })] }) }), _jsxs("box", { flexDirection: "row", marginTop: 1, children: [_jsx("text", { fg: "green", children: "> " }), _jsx("text", { children: modelSelector.searchQuery }), _jsx("text", { bg: "gray", fg: "black", children: " " })] })] }) }), _jsx("box", { flexGrow: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(ScrollableList, { items: filteredItems, selectedIndex: modelSelector.selectedIndex, renderItem: renderItem, maxHeight: listHeight, getKey: (item) => item.id }) }), _jsx("box", { height: 1, children: _jsx(FooterHints, { hints: footerHints }) })] }));
289
295
  }
290
296
  export default ModelSelectorScreen;
@@ -5,6 +5,7 @@ import { useDimensions } from "../state/DimensionsContext.js";
5
5
  import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
6
6
  import { useKeyboard } from "../hooks/useKeyboard.js";
7
7
  import { ScrollableList } from "../components/ScrollableList.js";
8
+ import { FooterHints } from "../components/layout/index.js";
8
9
 
9
10
  interface ModelItem {
10
11
  id: string;
@@ -372,7 +373,12 @@ export function ModelSelectorScreen() {
372
373
  }
373
374
  };
374
375
 
375
- const footerHints = "↑↓ choose • tab toggle type • enter choose • esc exit";
376
+ const footerHints = [
377
+ { keys: ["↑", "↓"], label: "choose" },
378
+ { keys: ["tab"], label: "toggle type" },
379
+ { keys: ["enter"], label: "choose" },
380
+ { keys: ["esc"], label: "exit" },
381
+ ];
376
382
  // Available height calculation
377
383
  // Header: 1 line (title) + 1 line (search) + 1 line (separator) = 3 lines?
378
384
  // Title line: Switch Model /// (o) Large
@@ -429,7 +435,7 @@ export function ModelSelectorScreen() {
429
435
  </box>
430
436
 
431
437
  <box height={1}>
432
- <text fg="#888888">{footerHints}</text>
438
+ <FooterHints hints={footerHints} />
433
439
  </box>
434
440
  </box>
435
441
  );
@@ -735,8 +735,19 @@ export function PluginsScreen() {
735
735
  }
736
736
  const selectedItem = selectableItems[pluginsState.selectedIndex];
737
737
  const footerHints = isSearchActive
738
- ? "type to filter │ Enter:done │ Esc:clear"
739
- : "u/p/l:toggle │ U:update a:all │ m:mismatches │ s:profile │ /:search";
738
+ ? [
739
+ { keys: ["type"], label: "filter" },
740
+ { keys: ["Enter"], label: "done" },
741
+ { keys: ["Esc"], label: "clear" },
742
+ ]
743
+ : [
744
+ { keys: ["u", "/", "p", "/", "l"], label: "toggle" },
745
+ { keys: ["U"], label: "update" },
746
+ { keys: ["a"], label: "all" },
747
+ { keys: ["m"], label: "mismatches" },
748
+ { keys: ["s"], label: "profile" },
749
+ { keys: ["/"], label: "search" },
750
+ ];
740
751
  const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
741
752
  const plugins = pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
742
753
  const installedCount = plugins.filter((p) => p.enabled).length;
@@ -937,8 +937,19 @@ export function PluginsScreen() {
937
937
  const selectedItem = selectableItems[pluginsState.selectedIndex];
938
938
 
939
939
  const footerHints = isSearchActive
940
- ? "type to filter │ Enter:done │ Esc:clear"
941
- : "u/p/l:toggle │ U:update a:all │ m:mismatches │ s:profile │ /:search";
940
+ ? [
941
+ { keys: ["type"], label: "filter" },
942
+ { keys: ["Enter"], label: "done" },
943
+ { keys: ["Esc"], label: "clear" },
944
+ ]
945
+ : [
946
+ { keys: ["u", "/", "p", "/", "l"], label: "toggle" },
947
+ { keys: ["U"], label: "update" },
948
+ { keys: ["a"], label: "all" },
949
+ { keys: ["m"], label: "mismatches" },
950
+ { keys: ["s"], label: "profile" },
951
+ { keys: ["/"], label: "search" },
952
+ ];
942
953
 
943
954
  const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
944
955
  const plugins: PluginInfo[] =
@@ -286,6 +286,13 @@ export function ProfilesScreen() {
286
286
  const errorMessage = profilesState.profiles.status === "error"
287
287
  ? profilesState.profiles.error.message
288
288
  : undefined;
289
- return (_jsx(ScreenLayout, { title: "claudeup Plugin Profiles", currentScreen: "profiles", statusLine: statusContent, footerHints: "\u2191\u2193:nav \u2502 Enter/a:apply \u2502 r:rename \u2502 d:delete \u2502 c:copy \u2502 i:import", listPanel: _jsx(ScrollableList, { items: allItems, selectedIndex: effectiveIndex, renderItem: renderProfileRow, maxHeight: dimensions.listPanelHeight }), detailPanel: renderProfileDetail(allItems[effectiveIndex], loadingStatus, errorMessage) }));
289
+ return (_jsx(ScreenLayout, { title: "claudeup Plugin Profiles", currentScreen: "profiles", statusLine: statusContent, footerHints: [
290
+ { keys: ["↑", "↓"], label: "nav" },
291
+ { keys: ["Enter", "/", "a"], label: "apply" },
292
+ { keys: ["r"], label: "rename" },
293
+ { keys: ["d"], label: "delete" },
294
+ { keys: ["c"], label: "copy" },
295
+ { keys: ["i"], label: "import" },
296
+ ], listPanel: _jsx(ScrollableList, { items: allItems, selectedIndex: effectiveIndex, renderItem: renderProfileRow, maxHeight: dimensions.listPanelHeight }), detailPanel: renderProfileDetail(allItems[effectiveIndex], loadingStatus, errorMessage) }));
290
297
  }
291
298
  export default ProfilesScreen;
@@ -393,7 +393,14 @@ export function ProfilesScreen() {
393
393
  title="claudeup Plugin Profiles"
394
394
  currentScreen="profiles"
395
395
  statusLine={statusContent}
396
- footerHints="↑↓:nav │ Enter/a:apply │ r:rename │ d:delete │ c:copy │ i:import"
396
+ footerHints={[
397
+ { keys: ["↑", "↓"], label: "nav" },
398
+ { keys: ["Enter", "/", "a"], label: "apply" },
399
+ { keys: ["r"], label: "rename" },
400
+ { keys: ["d"], label: "delete" },
401
+ { keys: ["c"], label: "copy" },
402
+ { keys: ["i"], label: "import" },
403
+ ]}
397
404
  listPanel={
398
405
  <ScrollableList
399
406
  items={allItems}
@@ -42,6 +42,7 @@ export function SkillsScreen() {
42
42
  // ── Remote search (debounced, cached) ─────────────────────────────────────
43
43
  const [searchResults, setSearchResults] = useState([]);
44
44
  const [isSearchLoading, setIsSearchLoading] = useState(false);
45
+ const [searchError, setSearchError] = useState(null);
45
46
  const searchTimerRef = useRef(null);
46
47
  const searchCacheRef = useRef(new Map());
47
48
  useEffect(() => {
@@ -49,15 +50,18 @@ export function SkillsScreen() {
49
50
  if (query.length < 2) {
50
51
  setSearchResults([]);
51
52
  setIsSearchLoading(false);
53
+ setSearchError(null);
52
54
  return;
53
55
  }
54
56
  const cached = searchCacheRef.current.get(query);
55
57
  if (cached) {
56
58
  setSearchResults(cached);
57
59
  setIsSearchLoading(false);
60
+ setSearchError(null);
58
61
  return;
59
62
  }
60
63
  setIsSearchLoading(true);
64
+ setSearchError(null);
61
65
  if (searchTimerRef.current)
62
66
  clearTimeout(searchTimerRef.current);
63
67
  searchTimerRef.current = setTimeout(async () => {
@@ -86,9 +90,13 @@ export function SkillsScreen() {
86
90
  });
87
91
  searchCacheRef.current.set(query, mapped);
88
92
  setSearchResults(mapped);
93
+ setSearchError(null);
89
94
  }
90
- catch {
95
+ catch (error) {
96
+ // A thrown error means the search service is unreachable — distinct
97
+ // from a successful search that returned zero matches.
91
98
  setSearchResults([]);
99
+ setSearchError(error instanceof Error ? error.message : "Search service unavailable");
92
100
  }
93
101
  setIsSearchLoading(false);
94
102
  }, 400);
@@ -517,7 +525,7 @@ export function SkillsScreen() {
517
525
  const skills = skillsState.skills.status === "success" ? skillsState.skills.data : [];
518
526
  const installedCount = skills.filter((s) => s.installed).length;
519
527
  const query = skillsState.searchQuery.trim();
520
- const statusContent = statusMsg ? (_jsx("text", { children: _jsx("span", { fg: statusMsg.tone === "success" ? "green" : "red", children: statusMsg.text }) })) : (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Skills: " }), _jsxs("span", { fg: "cyan", children: [installedCount, " installed"] }), query.length >= 2 && isSearchLoading && (_jsx("span", { fg: "yellow", children: " \u2502 searching..." })), query.length >= 2 && !isSearchLoading && searchResults.length > 0 && (_jsxs("span", { fg: "green", children: [" \u2502 ", searchResults.length, " found"] })), !query && _jsx("span", { fg: "gray", children: " \u2502 89K+ searchable" })] }));
528
+ const statusContent = statusMsg ? (_jsx("text", { children: _jsx("span", { fg: statusMsg.tone === "success" ? "green" : "red", children: statusMsg.text }) })) : (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Skills: " }), _jsxs("span", { fg: "cyan", children: [installedCount, " installed"] }), query.length >= 2 && isSearchLoading && (_jsx("span", { fg: "yellow", children: " \u2502 searching..." })), query.length >= 2 && !isSearchLoading && !searchError && searchResults.length > 0 && (_jsxs("span", { fg: "green", children: [" \u2502 ", searchResults.length, " found"] })), query.length >= 2 && !isSearchLoading && searchError && (_jsx("span", { fg: "red", children: " \u2502 search unavailable" })), !query && _jsx("span", { fg: "gray", children: " \u2502 89K+ searchable" })] }));
521
529
  // ── Render ────────────────────────────────────────────────────────────────
522
530
  return (_jsx(ScreenLayout, { title: "claudeup Skills", currentScreen: "skills", statusLine: statusContent, search: skillsState.searchQuery || isSearchActive
523
531
  ? {
@@ -526,7 +534,16 @@ export function SkillsScreen() {
526
534
  placeholder: "type to search",
527
535
  }
528
536
  : undefined, footerHints: isSearchActive
529
- ? "type to filter │ Enter:done │ Esc:clear"
530
- : "u:user │ p:project │ o:open │ /:search", listPanel: _jsxs("box", { flexDirection: "column", children: [_jsx(ScrollableList, { items: allItems, selectedIndex: skillsState.selectedIndex, renderItem: renderSkillRow, maxHeight: dimensions.listPanelHeight, getKey: (item, index) => `${index}:${item.id}` }), !query && skillsState.skills.status === "loading" && (_jsx("box", { marginTop: 2, paddingLeft: 2, children: _jsx("text", { fg: "yellow", children: "Loading popular skills..." }) })), query.length >= 2 && isSearchLoading && (_jsx("box", { marginTop: 2, paddingLeft: 2, children: _jsxs("text", { fg: "yellow", children: ["Searching for \"", skillsState.searchQuery, "\"..."] }) })), query.length >= 2 && !isSearchLoading && searchResults.length === 0 && (_jsx(EmptyFilterState, { query: skillsState.searchQuery, entityName: "skills" }))] }), detailPanel: renderSkillDetail(selectedItem) }));
537
+ ? [
538
+ { keys: ["type"], label: "filter" },
539
+ { keys: ["Enter"], label: "done" },
540
+ { keys: ["Esc"], label: "clear" },
541
+ ]
542
+ : [
543
+ { keys: ["u"], label: "user" },
544
+ { keys: ["p"], label: "project" },
545
+ { keys: ["o"], label: "open" },
546
+ { keys: ["/"], label: "search" },
547
+ ], listPanel: _jsxs("box", { flexDirection: "column", children: [_jsx(ScrollableList, { items: allItems, selectedIndex: skillsState.selectedIndex, renderItem: renderSkillRow, maxHeight: dimensions.listPanelHeight, getKey: (item, index) => `${index}:${item.id}` }), !query && skillsState.skills.status === "loading" && (_jsx("box", { marginTop: 2, paddingLeft: 2, children: _jsx("text", { fg: "yellow", children: "Loading popular skills..." }) })), query.length >= 2 && isSearchLoading && (_jsx("box", { marginTop: 2, paddingLeft: 2, children: _jsxs("text", { fg: "yellow", children: ["Searching for \"", skillsState.searchQuery, "\"..."] }) })), query.length >= 2 && !isSearchLoading && searchError && (_jsxs("box", { flexDirection: "column", marginTop: 2, paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: "red", children: "Skill search is unavailable right now." }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "The search service could not be reached" }) }), _jsxs("text", { fg: "gray", children: ["(", searchError, "). Check your connection and retry."] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Press Esc to clear the search." }) })] })), query.length >= 2 && !isSearchLoading && !searchError && searchResults.length === 0 && (_jsx(EmptyFilterState, { query: skillsState.searchQuery, entityName: "skills" }))] }), detailPanel: renderSkillDetail(selectedItem) }));
531
548
  }
532
549
  export default SkillsScreen;
@@ -60,6 +60,7 @@ export function SkillsScreen() {
60
60
 
61
61
  const [searchResults, setSearchResults] = useState<SkillInfo[]>([]);
62
62
  const [isSearchLoading, setIsSearchLoading] = useState(false);
63
+ const [searchError, setSearchError] = useState<string | null>(null);
63
64
  const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
64
65
  const searchCacheRef = useRef<Map<string, SkillInfo[]>>(new Map());
65
66
 
@@ -68,6 +69,7 @@ export function SkillsScreen() {
68
69
  if (query.length < 2) {
69
70
  setSearchResults([]);
70
71
  setIsSearchLoading(false);
72
+ setSearchError(null);
71
73
  return;
72
74
  }
73
75
 
@@ -75,10 +77,12 @@ export function SkillsScreen() {
75
77
  if (cached) {
76
78
  setSearchResults(cached);
77
79
  setIsSearchLoading(false);
80
+ setSearchError(null);
78
81
  return;
79
82
  }
80
83
 
81
84
  setIsSearchLoading(true);
85
+ setSearchError(null);
82
86
 
83
87
  if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
84
88
  searchTimerRef.current = setTimeout(async () => {
@@ -107,8 +111,14 @@ export function SkillsScreen() {
107
111
  });
108
112
  searchCacheRef.current.set(query, mapped);
109
113
  setSearchResults(mapped);
110
- } catch {
114
+ setSearchError(null);
115
+ } catch (error) {
116
+ // A thrown error means the search service is unreachable — distinct
117
+ // from a successful search that returned zero matches.
111
118
  setSearchResults([]);
119
+ setSearchError(
120
+ error instanceof Error ? error.message : "Search service unavailable",
121
+ );
112
122
  }
113
123
  setIsSearchLoading(false);
114
124
  }, 400);
@@ -583,9 +593,12 @@ export function SkillsScreen() {
583
593
  {query.length >= 2 && isSearchLoading && (
584
594
  <span fg="yellow"> │ searching...</span>
585
595
  )}
586
- {query.length >= 2 && !isSearchLoading && searchResults.length > 0 && (
596
+ {query.length >= 2 && !isSearchLoading && !searchError && searchResults.length > 0 && (
587
597
  <span fg="green"> │ {searchResults.length} found</span>
588
598
  )}
599
+ {query.length >= 2 && !isSearchLoading && searchError && (
600
+ <span fg="red"> │ search unavailable</span>
601
+ )}
589
602
  {!query && <span fg="gray"> │ 89K+ searchable</span>}
590
603
  </text>
591
604
  );
@@ -608,8 +621,17 @@ export function SkillsScreen() {
608
621
  }
609
622
  footerHints={
610
623
  isSearchActive
611
- ? "type to filter │ Enter:done │ Esc:clear"
612
- : "u:user │ p:project o:open │ /:search"
624
+ ? [
625
+ { keys: ["type"], label: "filter" },
626
+ { keys: ["Enter"], label: "done" },
627
+ { keys: ["Esc"], label: "clear" },
628
+ ]
629
+ : [
630
+ { keys: ["u"], label: "user" },
631
+ { keys: ["p"], label: "project" },
632
+ { keys: ["o"], label: "open" },
633
+ { keys: ["/"], label: "search" },
634
+ ]
613
635
  }
614
636
  listPanel={
615
637
  <box flexDirection="column">
@@ -630,7 +652,19 @@ export function SkillsScreen() {
630
652
  <text fg="yellow">Searching for "{skillsState.searchQuery}"...</text>
631
653
  </box>
632
654
  )}
633
- {query.length >= 2 && !isSearchLoading && searchResults.length === 0 && (
655
+ {query.length >= 2 && !isSearchLoading && searchError && (
656
+ <box flexDirection="column" marginTop={2} paddingLeft={2} paddingRight={2}>
657
+ <text fg="red">Skill search is unavailable right now.</text>
658
+ <box marginTop={1}>
659
+ <text fg="gray">The search service could not be reached</text>
660
+ </box>
661
+ <text fg="gray">({searchError}). Check your connection and retry.</text>
662
+ <box marginTop={1}>
663
+ <text fg="gray">Press Esc to clear the search.</text>
664
+ </box>
665
+ </box>
666
+ )}
667
+ {query.length >= 2 && !isSearchLoading && !searchError && searchResults.length === 0 && (
634
668
  <EmptyFilterState query={skillsState.searchQuery} entityName="skills" />
635
669
  )}
636
670
  </box>
@@ -195,6 +195,12 @@ export function StatusLineScreen() {
195
195
  const statusContent = (_jsxs(_Fragment, { children: [_jsx("text", { fg: "gray", children: "Scope: " }), _jsx("text", { fg: "cyan", children: scopeLabel }), _jsx("text", { fg: "gray", children: " \u2502 Current: " }), _jsx("text", { fg: "green", children: currentValue
196
196
  ? currentValue.slice(0, 35) + (currentValue.length > 35 ? "..." : "")
197
197
  : "(not set)" })] }));
198
- return (_jsx(ScreenLayout, { title: "claudeup Status Line", currentScreen: "statusline", statusLine: statusContent, footerHints: "\u2191\u2193:nav \u2502 Enter:apply \u2502 p:project \u2502 g:global \u2502 r:reset", listPanel: _jsx(ScrollableList, { items: listItems, selectedIndex: statusLine.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), detailPanel: renderPreview() }));
198
+ return (_jsx(ScreenLayout, { title: "claudeup Status Line", currentScreen: "statusline", statusLine: statusContent, footerHints: [
199
+ { keys: ["↑", "↓"], label: "nav" },
200
+ { keys: ["Enter"], label: "apply" },
201
+ { keys: ["p"], label: "project" },
202
+ { keys: ["g"], label: "global" },
203
+ { keys: ["r"], label: "reset" },
204
+ ], listPanel: _jsx(ScrollableList, { items: listItems, selectedIndex: statusLine.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), detailPanel: renderPreview() }));
199
205
  }
200
206
  export default StatusLineScreen;
@@ -393,7 +393,13 @@ export function StatusLineScreen() {
393
393
  title="claudeup Status Line"
394
394
  currentScreen={"statusline" as never}
395
395
  statusLine={statusContent}
396
- footerHints="↑↓:nav │ Enter:apply │ p:project │ g:global │ r:reset"
396
+ footerHints={[
397
+ { keys: ["↑", "↓"], label: "nav" },
398
+ { keys: ["Enter"], label: "apply" },
399
+ { keys: ["p"], label: "project" },
400
+ { keys: ["g"], label: "global" },
401
+ { keys: ["r"], label: "reset" },
402
+ ]}
397
403
  listPanel={
398
404
  <ScrollableList
399
405
  items={listItems}
@@ -7,3 +7,4 @@ export { ModelSelectorScreen } from "./ModelSelectorScreen.js";
7
7
  export { ProfilesScreen } from "./ProfilesScreen.js";
8
8
  export { SkillsScreen } from "./SkillsScreen.js";
9
9
  export { GitignoreScreen } from "./GitignoreScreen.js";
10
+ export { AliasScreen } from "./AliasScreen.js";
@@ -7,3 +7,4 @@ export { ModelSelectorScreen } from "./ModelSelectorScreen.js";
7
7
  export { ProfilesScreen } from "./ProfilesScreen.js";
8
8
  export { SkillsScreen } from "./SkillsScreen.js";
9
9
  export { GitignoreScreen } from "./GitignoreScreen.js";
10
+ export { AliasScreen } from "./AliasScreen.js";
@@ -21,7 +21,8 @@ export type Screen =
21
21
  | "model-selector"
22
22
  | "profiles"
23
23
  | "skills"
24
- | "gitignore";
24
+ | "gitignore"
25
+ | "alias";
25
26
 
26
27
  export type Route =
27
28
  | { screen: "plugins" }
@@ -32,7 +33,8 @@ export type Route =
32
33
  | { screen: "model-selector" }
33
34
  | { screen: "profiles" }
34
35
  | { screen: "skills" }
35
- | { screen: "gitignore" };
36
+ | { screen: "gitignore" }
37
+ | { screen: "alias" };
36
38
 
37
39
  // ============================================================================
38
40
  // Async Data Types