claudeup 3.8.0 → 3.10.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.
@@ -0,0 +1,430 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { useEffect, useCallback, useMemo, useState, useRef } from "react";
3
+ import { useApp, useModal } from "../state/AppContext.js";
4
+ import { useDimensions } from "../state/DimensionsContext.js";
5
+ import { useKeyboard } from "../hooks/useKeyboard.js";
6
+ import { ScreenLayout } from "../components/layout/index.js";
7
+ import { ScrollableList } from "../components/ScrollableList.js";
8
+ import { EmptyFilterState } from "../components/EmptyFilterState.js";
9
+ import { fetchAvailableSkills, fetchSkillFrontmatter, installSkill, uninstallSkill, } from "../../services/skills-manager.js";
10
+ import { searchSkills } from "../../services/skillsmp-client.js";
11
+ import { DEFAULT_SKILL_REPOS, RECOMMENDED_SKILLS } from "../../data/skill-repos.js";
12
+ function formatStars(stars) {
13
+ if (!stars)
14
+ return "";
15
+ if (stars >= 1000000)
16
+ return `★ ${(stars / 1000000).toFixed(1)}M`;
17
+ if (stars >= 10000)
18
+ return `★ ${Math.round(stars / 1000)}K`;
19
+ if (stars >= 1000)
20
+ return `★ ${(stars / 1000).toFixed(1)}K`;
21
+ return `★ ${stars}`;
22
+ }
23
+ export function SkillsScreen() {
24
+ const { state, dispatch } = useApp();
25
+ const { skills: skillsState } = state;
26
+ const modal = useModal();
27
+ const dimensions = useDimensions();
28
+ const isSearchActive = state.isSearching &&
29
+ state.currentRoute.screen === "skills" &&
30
+ !state.modal;
31
+ // Fetch data
32
+ const fetchData = useCallback(async () => {
33
+ dispatch({ type: "SKILLS_DATA_LOADING" });
34
+ try {
35
+ const skills = await fetchAvailableSkills(DEFAULT_SKILL_REPOS, state.projectPath);
36
+ dispatch({ type: "SKILLS_DATA_SUCCESS", skills });
37
+ }
38
+ catch (error) {
39
+ dispatch({
40
+ type: "SKILLS_DATA_ERROR",
41
+ error: error instanceof Error ? error : new Error(String(error)),
42
+ });
43
+ }
44
+ }, [dispatch, state.projectPath]);
45
+ useEffect(() => {
46
+ fetchData();
47
+ }, [fetchData, state.dataRefreshVersion]);
48
+ // Remote search: query Firebase API when user types (debounced, cached)
49
+ const [searchResults, setSearchResults] = useState([]);
50
+ const [isSearchLoading, setIsSearchLoading] = useState(false);
51
+ const searchTimerRef = useRef(null);
52
+ const searchCacheRef = useRef(new Map());
53
+ useEffect(() => {
54
+ const query = skillsState.searchQuery.trim();
55
+ if (query.length < 2) {
56
+ setSearchResults([]);
57
+ setIsSearchLoading(false);
58
+ return;
59
+ }
60
+ // Check cache first
61
+ const cached = searchCacheRef.current.get(query);
62
+ if (cached) {
63
+ setSearchResults(cached);
64
+ setIsSearchLoading(false);
65
+ return;
66
+ }
67
+ setIsSearchLoading(true);
68
+ if (searchTimerRef.current)
69
+ clearTimeout(searchTimerRef.current);
70
+ searchTimerRef.current = setTimeout(async () => {
71
+ try {
72
+ const results = await searchSkills(query, { limit: 30 });
73
+ const mapped = results.map((r) => {
74
+ const source = {
75
+ label: r.repo || "unknown",
76
+ repo: r.repo || "unknown",
77
+ skillsPath: "",
78
+ };
79
+ return {
80
+ id: `remote:${r.repo}/${r.skillPath}`,
81
+ name: r.name,
82
+ description: r.description || "",
83
+ source,
84
+ repoPath: r.skillPath ? `${r.skillPath}/SKILL.md` : "SKILL.md",
85
+ gitBlobSha: "",
86
+ frontmatter: null,
87
+ installed: false,
88
+ installedScope: null,
89
+ hasUpdate: false,
90
+ stars: r.stars,
91
+ };
92
+ });
93
+ searchCacheRef.current.set(query, mapped);
94
+ setSearchResults(mapped);
95
+ }
96
+ catch {
97
+ setSearchResults([]);
98
+ }
99
+ setIsSearchLoading(false);
100
+ }, 400);
101
+ return () => {
102
+ if (searchTimerRef.current)
103
+ clearTimeout(searchTimerRef.current);
104
+ };
105
+ }, [skillsState.searchQuery]);
106
+ // Static recommended skills — always available, no API needed
107
+ const staticRecommended = useMemo(() => {
108
+ return RECOMMENDED_SKILLS.map((r) => ({
109
+ id: `rec:${r.repo}/${r.skillPath}`,
110
+ name: r.name,
111
+ description: r.description,
112
+ source: { label: r.repo, repo: r.repo, skillsPath: "" },
113
+ repoPath: `${r.skillPath}/SKILL.md`,
114
+ gitBlobSha: "",
115
+ frontmatter: null,
116
+ installed: false,
117
+ installedScope: null,
118
+ hasUpdate: false,
119
+ isRecommended: true,
120
+ stars: undefined,
121
+ }));
122
+ }, []);
123
+ // Merge static recommended with fetched data (to get install status + stars)
124
+ const mergedRecommended = useMemo(() => {
125
+ if (skillsState.skills.status !== "success")
126
+ return staticRecommended;
127
+ const fetched = skillsState.skills.data.filter((s) => s.isRecommended);
128
+ // Merge: keep fetched data (has stars + install status), fall back to static
129
+ return staticRecommended.map((staticSkill) => {
130
+ const match = fetched.find((f) => f.source.repo === staticSkill.source.repo && f.name === staticSkill.name);
131
+ return match || staticSkill;
132
+ });
133
+ }, [staticRecommended, skillsState.skills]);
134
+ // Build list: recommended always shown, then search results or popular
135
+ const allItems = useMemo(() => {
136
+ const query = skillsState.searchQuery.toLowerCase();
137
+ const items = [];
138
+ // ── RECOMMENDED: always shown, filtered when searching ──
139
+ const filteredRec = query
140
+ ? mergedRecommended.filter((s) => s.name.toLowerCase().includes(query) ||
141
+ (s.description || "").toLowerCase().includes(query))
142
+ : mergedRecommended;
143
+ items.push({
144
+ id: "cat:recommended",
145
+ type: "category",
146
+ label: "Recommended",
147
+ categoryKey: "recommended",
148
+ });
149
+ for (const skill of filteredRec) {
150
+ items.push({
151
+ id: `skill:${skill.id}`,
152
+ type: "skill",
153
+ label: skill.name,
154
+ skill,
155
+ });
156
+ }
157
+ // ── SEARCH MODE ──
158
+ if (query.length >= 2) {
159
+ // Loading and no-results handled in listPanel, not as list items
160
+ if (!isSearchLoading && searchResults.length > 0) {
161
+ // Dedup against recommended
162
+ const recNames = new Set(mergedRecommended.map((s) => s.name));
163
+ const deduped = searchResults.filter((s) => !recNames.has(s.name));
164
+ if (deduped.length > 0) {
165
+ items.push({
166
+ id: "cat:search",
167
+ type: "category",
168
+ label: `Search (${deduped.length})`,
169
+ categoryKey: "popular",
170
+ });
171
+ for (const skill of deduped) {
172
+ items.push({
173
+ id: `skill:${skill.id}`,
174
+ type: "skill",
175
+ label: skill.name,
176
+ skill,
177
+ });
178
+ }
179
+ }
180
+ }
181
+ // No-results message handled in listPanel below, not as a list item
182
+ return items;
183
+ }
184
+ // ── POPULAR (default, no search query) ──
185
+ // Loading state handled in listPanel, not as category header
186
+ if (skillsState.skills.status === "success") {
187
+ const popularSkills = skillsState.skills.data
188
+ .filter((s) => !s.isRecommended)
189
+ .sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
190
+ if (popularSkills.length > 0) {
191
+ items.push({
192
+ id: "cat:popular",
193
+ type: "category",
194
+ label: "Popular",
195
+ categoryKey: "popular",
196
+ });
197
+ for (const skill of popularSkills) {
198
+ items.push({
199
+ id: `skill:${skill.id}`,
200
+ type: "skill",
201
+ label: skill.name,
202
+ skill,
203
+ });
204
+ }
205
+ }
206
+ }
207
+ return items;
208
+ }, [skillsState.skills, skillsState.searchQuery, searchResults, isSearchLoading, mergedRecommended]);
209
+ const selectableItems = useMemo(() => allItems.filter((item) => item.type === "skill" || item.type === "category"), [allItems]);
210
+ const selectedItem = selectableItems[skillsState.selectedIndex];
211
+ const selectedSkill = selectedItem?.type === "skill" ? selectedItem.skill : undefined;
212
+ // Lazy-load frontmatter for selected skill
213
+ useEffect(() => {
214
+ if (!selectedSkill || selectedSkill.frontmatter)
215
+ return;
216
+ fetchSkillFrontmatter(selectedSkill).then((fm) => {
217
+ dispatch({
218
+ type: "SKILLS_UPDATE_ITEM",
219
+ name: selectedSkill.name,
220
+ updates: { frontmatter: fm },
221
+ });
222
+ }).catch(() => { });
223
+ }, [selectedSkill?.id, dispatch]);
224
+ // Install handler
225
+ const handleInstall = useCallback(async (scope) => {
226
+ if (!selectedSkill)
227
+ return;
228
+ modal.loading(`Installing ${selectedSkill.name}...`);
229
+ try {
230
+ await installSkill(selectedSkill, scope, state.projectPath);
231
+ modal.hideModal();
232
+ dispatch({
233
+ type: "SKILLS_UPDATE_ITEM",
234
+ name: selectedSkill.name,
235
+ updates: {
236
+ installed: true,
237
+ installedScope: scope,
238
+ },
239
+ });
240
+ await modal.message("Installed", `${selectedSkill.name} installed to ${scope === "user" ? "~/.claude/skills/" : ".claude/skills/"}`, "success");
241
+ }
242
+ catch (error) {
243
+ modal.hideModal();
244
+ await modal.message("Error", `Failed to install: ${error}`, "error");
245
+ }
246
+ }, [selectedSkill, state.projectPath, dispatch, modal]);
247
+ // Uninstall handler
248
+ const handleUninstall = useCallback(async () => {
249
+ if (!selectedSkill || !selectedSkill.installed)
250
+ return;
251
+ const scope = selectedSkill.installedScope;
252
+ if (!scope)
253
+ return;
254
+ const confirmed = await modal.confirm(`Uninstall "${selectedSkill.name}"?`, `This will remove it from the ${scope} scope.`);
255
+ if (!confirmed)
256
+ return;
257
+ modal.loading(`Uninstalling ${selectedSkill.name}...`);
258
+ try {
259
+ await uninstallSkill(selectedSkill.name, scope, state.projectPath);
260
+ modal.hideModal();
261
+ dispatch({
262
+ type: "SKILLS_UPDATE_ITEM",
263
+ name: selectedSkill.name,
264
+ updates: {
265
+ installed: false,
266
+ installedScope: null,
267
+ },
268
+ });
269
+ await modal.message("Uninstalled", `${selectedSkill.name} removed.`, "success");
270
+ }
271
+ catch (error) {
272
+ modal.hideModal();
273
+ await modal.message("Error", `Failed to uninstall: ${error}`, "error");
274
+ }
275
+ }, [selectedSkill, state.projectPath, dispatch, modal]);
276
+ // Keyboard handling — same pattern as PluginsScreen
277
+ useKeyboard((event) => {
278
+ if (state.modal)
279
+ return;
280
+ const hasQuery = skillsState.searchQuery.length > 0;
281
+ // Escape: clear search
282
+ if (event.name === "escape") {
283
+ if (hasQuery || isSearchActive) {
284
+ dispatch({ type: "SKILLS_SET_SEARCH", query: "" });
285
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
286
+ dispatch({ type: "SKILLS_SELECT", index: 0 });
287
+ }
288
+ return;
289
+ }
290
+ // Backspace: remove last char
291
+ if (event.name === "backspace" || event.name === "delete") {
292
+ if (hasQuery) {
293
+ const newQuery = skillsState.searchQuery.slice(0, -1);
294
+ dispatch({ type: "SKILLS_SET_SEARCH", query: newQuery });
295
+ dispatch({ type: "SKILLS_SELECT", index: 0 });
296
+ if (!newQuery)
297
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
298
+ }
299
+ return;
300
+ }
301
+ // Navigation — always works; exits search mode on navigate
302
+ if (event.name === "up" || event.name === "k") {
303
+ if (isSearchActive)
304
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
305
+ const newIndex = Math.max(0, skillsState.selectedIndex - 1);
306
+ dispatch({ type: "SKILLS_SELECT", index: newIndex });
307
+ return;
308
+ }
309
+ if (event.name === "down" || event.name === "j") {
310
+ if (isSearchActive)
311
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
312
+ const newIndex = Math.min(Math.max(0, selectableItems.length - 1), skillsState.selectedIndex + 1);
313
+ dispatch({ type: "SKILLS_SELECT", index: newIndex });
314
+ return;
315
+ }
316
+ // Enter — install (always works)
317
+ if (event.name === "return" || event.name === "enter") {
318
+ if (isSearchActive) {
319
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
320
+ return;
321
+ }
322
+ if (selectedSkill && !selectedSkill.installed) {
323
+ handleInstall("project");
324
+ }
325
+ return;
326
+ }
327
+ // When actively typing in search, letters go to the query
328
+ if (isSearchActive) {
329
+ if (event.name === "k" || event.name === "j") {
330
+ const delta = event.name === "k" ? -1 : 1;
331
+ const newIndex = Math.max(0, Math.min(selectableItems.length - 1, skillsState.selectedIndex + delta));
332
+ dispatch({ type: "SKILLS_SELECT", index: newIndex });
333
+ return;
334
+ }
335
+ if (event.name && event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
336
+ dispatch({ type: "SKILLS_SET_SEARCH", query: skillsState.searchQuery + event.name });
337
+ dispatch({ type: "SKILLS_SELECT", index: 0 });
338
+ }
339
+ return;
340
+ }
341
+ // Action shortcuts (work when not actively typing, even with filter visible)
342
+ if (event.name === "u" && selectedSkill) {
343
+ if (selectedSkill.installed && selectedSkill.installedScope === "user")
344
+ handleUninstall();
345
+ else
346
+ handleInstall("user");
347
+ }
348
+ else if (event.name === "p" && selectedSkill) {
349
+ if (selectedSkill.installed && selectedSkill.installedScope === "project")
350
+ handleUninstall();
351
+ else
352
+ handleInstall("project");
353
+ }
354
+ else if (event.name === "d" && selectedSkill?.installed) {
355
+ handleUninstall();
356
+ }
357
+ else if (event.name === "r") {
358
+ fetchData();
359
+ }
360
+ // "/" to enter search mode
361
+ else if (event.name === "/") {
362
+ dispatch({ type: "SET_SEARCHING", isSearching: true });
363
+ }
364
+ });
365
+ const renderListItem = (item, _idx, isSelected) => {
366
+ if (item.type === "category") {
367
+ const isRec = item.categoryKey === "recommended";
368
+ const bgColor = isRec ? "green" : "cyan";
369
+ const star = isRec ? "★ " : "";
370
+ if (isSelected) {
371
+ return (_jsx("text", { bg: "magenta", fg: "white", children: _jsxs("strong", { children: [" ", star, item.label, " "] }) }));
372
+ }
373
+ return (_jsx("text", { bg: bgColor, fg: "white", children: _jsxs("strong", { children: [" ", star, item.label, " "] }) }));
374
+ }
375
+ if (item.type === "skill" && item.skill) {
376
+ const skill = item.skill;
377
+ const indicator = skill.installed ? "●" : "○";
378
+ const indicatorColor = skill.installed ? "cyan" : "gray";
379
+ const scopeTag = skill.installedScope === "user" ? "u" : skill.installedScope === "project" ? "p" : "";
380
+ const starsStr = formatStars(skill.stars);
381
+ if (isSelected) {
382
+ return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", indicator, " ", skill.name, skill.hasUpdate ? " ⬆" : "", scopeTag ? ` [${scopeTag}]` : "", starsStr ? ` ${starsStr}` : "", " "] }));
383
+ }
384
+ return (_jsxs("text", { children: [_jsxs("span", { fg: indicatorColor, children: [" ", indicator, " "] }), _jsx("span", { fg: "white", children: skill.name }), skill.hasUpdate && _jsx("span", { fg: "yellow", children: " \u2B06" }), scopeTag && (_jsxs("span", { fg: scopeTag === "u" ? "cyan" : "green", children: [" [", scopeTag, "]"] })), starsStr && (_jsxs("span", { fg: "yellow", children: [" ", starsStr] }))] }));
385
+ }
386
+ return _jsx("text", { fg: "gray", children: item.label });
387
+ };
388
+ const renderDetail = () => {
389
+ if (skillsState.skills.status === "loading") {
390
+ return _jsx("text", { fg: "gray", children: "Loading skills..." });
391
+ }
392
+ if (skillsState.skills.status === "error") {
393
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "red", children: "Failed to load skills" }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: skillsState.skills.error.message }) })] }));
394
+ }
395
+ if (!selectedItem) {
396
+ return _jsx("text", { fg: "gray", children: "Select a skill to see details" });
397
+ }
398
+ if (selectedItem.type === "category") {
399
+ const isRec = selectedItem.categoryKey === "recommended";
400
+ const isNoResults = selectedItem.categoryKey === "no-results";
401
+ if (isNoResults) {
402
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "yellow", children: _jsx("strong", { children: "No skills found" }) }), _jsx("box", { marginTop: 1, children: _jsxs("text", { fg: "gray", children: ["Nothing matched \"", skillsState.searchQuery, "\"."] }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Try a different search term, or if you think this is a mistake, create an issue at:" }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#5c9aff", children: "github.com/MadAppGang/magus/issues" }) }), _jsx("box", { marginTop: 2, children: _jsx("text", { fg: "gray", children: "Press Esc to clear the search." }) })] }));
403
+ }
404
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: isRec ? "green" : "cyan", children: _jsxs("strong", { children: [isRec ? "★ " : "", selectedItem.label] }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: isRec ? "Curated skills recommended for most projects" : "Popular skills sorted by stars" }) })] }));
405
+ }
406
+ if (!selectedSkill)
407
+ return null;
408
+ const fm = selectedSkill.frontmatter;
409
+ const description = fm?.description || selectedSkill.description || "Loading...";
410
+ const scopeColor = selectedSkill.installedScope === "user" ? "cyan" : "green";
411
+ const starsStr = formatStars(selectedSkill.stars);
412
+ return (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { fg: "cyan", children: [_jsx("strong", { children: selectedSkill.name }), starsStr && _jsxs("span", { fg: "yellow", children: [" ", starsStr] })] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "white", children: description }) }), fm?.category && (_jsx("box", { marginTop: 1, children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Category " }), _jsx("span", { fg: "cyan", children: fm.category })] }) })), fm?.author && (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Author " }), _jsx("span", { children: fm.author })] }) })), fm?.version && (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Version " }), _jsx("span", { children: fm.version })] }) })), fm?.tags && fm.tags.length > 0 && (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Tags " }), _jsx("span", { children: fm.tags.join(", ") })] }) })), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Source " }), _jsx("span", { fg: "#5c9aff", children: selectedSkill.source.repo })] }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: " " }), _jsx("span", { fg: "gray", children: selectedSkill.repoPath })] })] }), selectedSkill.installed && selectedSkill.installedScope && (_jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { children: "─".repeat(24) }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Installed " }), _jsxs("span", { fg: scopeColor, children: [selectedSkill.installedScope === "user"
413
+ ? "~/.claude/skills/"
414
+ : ".claude/skills/", selectedSkill.name, "/"] })] })] })), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { children: "─".repeat(24) }), _jsx("text", { children: _jsx("strong", { children: "Install scope:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { bg: "cyan", fg: "black", children: " u " }), _jsx("span", { fg: selectedSkill.installedScope === "user" ? "cyan" : "gray", children: selectedSkill.installedScope === "user" ? " ● " : " ○ " }), _jsx("span", { fg: "cyan", children: "User" }), _jsx("span", { fg: "gray", children: " ~/.claude/skills/" })] }), _jsxs("text", { children: [_jsx("span", { bg: "green", fg: "black", children: " p " }), _jsx("span", { fg: selectedSkill.installedScope === "project" ? "green" : "gray", children: selectedSkill.installedScope === "project" ? " ● " : " ○ " }), _jsx("span", { fg: "green", children: "Project" }), _jsx("span", { fg: "gray", children: " .claude/skills/" })] })] })] }), selectedSkill.hasUpdate && (_jsx("box", { marginTop: 1, children: _jsx("text", { bg: "yellow", fg: "black", children: " UPDATE AVAILABLE " }) })), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [!selectedSkill.installed && (_jsx("text", { fg: "gray", children: "Press u/p to install in scope" })), selectedSkill.installed && (_jsx("text", { fg: "gray", children: "Press d to uninstall" }))] })] }));
415
+ };
416
+ const skills = skillsState.skills.status === "success" ? skillsState.skills.data : [];
417
+ const installedCount = skills.filter((s) => s.installed).length;
418
+ const query = skillsState.searchQuery.trim();
419
+ const statusContent = (_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" }))] }));
420
+ return (_jsx(ScreenLayout, { title: "claudeup Skills", currentScreen: "skills", statusLine: statusContent, search: skillsState.searchQuery || isSearchActive
421
+ ? {
422
+ isActive: isSearchActive,
423
+ query: skillsState.searchQuery,
424
+ placeholder: "type to search",
425
+ }
426
+ : undefined, footerHints: isSearchActive
427
+ ? "type to filter │ Enter:done │ Esc:clear"
428
+ : "u:user │ p:project │ d:uninstall │ /:search", listPanel: _jsxs("box", { flexDirection: "column", children: [_jsx(ScrollableList, { items: selectableItems, selectedIndex: skillsState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), !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: renderDetail() }));
429
+ }
430
+ export default SkillsScreen;