@gustavobrunodev/ai-tools 1.0.1 → 1.1.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.
- package/CHANGELOG.md +0 -0
- package/jest.config.ts +26 -0
- package/package.json +1 -1
- package/project.json +74 -0
- package/src/app.tsx +56 -0
- package/src/atoms/deprecatedSkills.ts +11 -0
- package/src/atoms/environmentCheck.ts +51 -0
- package/src/atoms/installedSkills.ts +36 -0
- package/src/atoms/wizard.ts +6 -0
- package/src/cli/audit.ts +21 -0
- package/src/cli/cache.ts +28 -0
- package/src/cli/install.ts +93 -0
- package/src/cli/list.ts +41 -0
- package/src/cli/remove.ts +70 -0
- package/src/cli/update.ts +107 -0
- package/src/components/AnimatedTransition.tsx +42 -0
- package/src/components/AuditLogViewer.tsx +85 -0
- package/src/components/CategoryHeader.tsx +39 -0
- package/src/components/ConfirmPrompt.tsx +34 -0
- package/src/components/FooterBar.tsx +36 -0
- package/src/components/Header.tsx +97 -0
- package/src/components/InstallResults.tsx +110 -0
- package/src/components/KeyboardShortcutsOverlay.tsx +112 -0
- package/src/components/MultiSelectPrompt.tsx +219 -0
- package/src/components/SearchInput.tsx +36 -0
- package/src/components/SelectPrompt.tsx +108 -0
- package/src/components/SkillCard.tsx +74 -0
- package/src/components/SkillDetailPanel.tsx +233 -0
- package/src/components/StatusBadge.tsx +45 -0
- package/src/components/__tests__/AnimatedTransition.pbt.test.tsx +51 -0
- package/src/components/__tests__/CategoryHeader.pbt.test.tsx +107 -0
- package/src/components/__tests__/CategoryHeader.test.tsx +105 -0
- package/src/components/__tests__/KeyboardShortcutsOverlay.pbt.test.tsx +155 -0
- package/src/components/__tests__/KeyboardShortcutsOverlay.test.tsx +136 -0
- package/src/components/__tests__/SkillDetailPanel.test.tsx +273 -0
- package/src/components/index.ts +12 -0
- package/src/hooks/__tests__/useConfig.test.ts +242 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAgents.ts +28 -0
- package/src/hooks/useConfig.ts +114 -0
- package/src/hooks/useFilter.ts +31 -0
- package/src/hooks/useInstaller.ts +39 -0
- package/src/hooks/useKeyboardNav.ts +39 -0
- package/src/hooks/useKonamiCode.ts +48 -0
- package/src/hooks/useRemover.ts +59 -0
- package/src/hooks/useSkillContent.ts +67 -0
- package/src/hooks/useSkills.ts +38 -0
- package/src/hooks/useWizardStep.ts +19 -0
- package/src/index.ts +129 -0
- package/src/services/__tests__/audit-log.spec.ts +220 -0
- package/src/services/__tests__/badge-format.test.ts +102 -0
- package/src/services/__tests__/category-colors.test.ts +253 -0
- package/src/services/__tests__/config.test.ts +184 -0
- package/src/services/__tests__/installer.security.spec.ts +151 -0
- package/src/services/__tests__/lockfile.security.spec.ts +132 -0
- package/src/services/__tests__/markdown-parser.spec.ts +185 -0
- package/src/services/__tests__/terminal-dimensions.pbt.test.ts +246 -0
- package/src/services/__tests__/terminal-dimensions.test.ts +109 -0
- package/src/services/__tests__/update-cache.pbt.test.ts +214 -0
- package/src/services/__tests__/update-cache.test.ts +215 -0
- package/src/services/agents.ts +42 -0
- package/src/services/audio-player.ts +55 -0
- package/src/services/audit-log.ts +69 -0
- package/src/services/badge-format.ts +4 -0
- package/src/services/categories.ts +176 -0
- package/src/services/category-colors.ts +19 -0
- package/src/services/config.ts +84 -0
- package/src/services/github-contributors.ts +56 -0
- package/src/services/global-path.ts +20 -0
- package/src/services/index.ts +21 -0
- package/src/services/installer.ts +371 -0
- package/src/services/lockfile.ts +177 -0
- package/src/services/markdown-parser.ts +108 -0
- package/src/services/package-info.ts +19 -0
- package/src/services/project-root.ts +18 -0
- package/src/services/registry.ts +382 -0
- package/src/services/skills-provider.ts +169 -0
- package/src/services/terminal-dimensions.ts +18 -0
- package/src/services/update-cache.ts +65 -0
- package/src/services/update-check.ts +26 -0
- package/src/theme/colors.ts +24 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/symbols.ts +22 -0
- package/src/types.ts +38 -0
- package/src/utils/constants.ts +49 -0
- package/src/utils/paths.ts +52 -0
- package/src/views/ActionSelector.tsx +45 -0
- package/src/views/AgentSelector.tsx +105 -0
- package/src/views/CreditsView.tsx +332 -0
- package/src/views/InstallConfig.tsx +162 -0
- package/src/views/InstallWizard.tsx +181 -0
- package/src/views/ListView.tsx +41 -0
- package/src/views/RemoveWizard.tsx +237 -0
- package/src/views/SkillBrowser.tsx +504 -0
- package/src/views/UpdateView.tsx +272 -0
- package/src/views/arcade/ArcadeMenu.tsx +89 -0
- package/src/views/arcade/VibeInvaders.tsx +339 -0
- package/src/views/arcade/index.ts +2 -0
- package/src/views/index.ts +11 -0
- package/tsconfig.json +19 -0
- package/tsconfig.spec.json +25 -0
- package/LICENSE +0 -26
- package/README.md +0 -257
- package/index.js +0 -12
- package/index.js.map +0 -7
- /package/{assets → src/assets}/chiptune.mp3 +0 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { Box, Text, useInput, useStdout } from 'ink'
|
|
2
|
+
import { useAtomValue } from 'jotai'
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { CategoryHeader, Header, SearchInput, SkillCard, SkillDetailPanel } from '../components'
|
|
6
|
+
import { FooterBar } from '../components/FooterBar'
|
|
7
|
+
import { KeyboardShortcutsOverlay, type ShortcutEntry } from '../components/KeyboardShortcutsOverlay'
|
|
8
|
+
import { useFilter, useSkills } from '../hooks'
|
|
9
|
+
import { groupSkillsByCategory } from '../services/categories'
|
|
10
|
+
import { canShowDetailPanel } from '../services/terminal-dimensions'
|
|
11
|
+
import { colors, symbols } from '../theme'
|
|
12
|
+
import type { SkillInfo } from '../types'
|
|
13
|
+
|
|
14
|
+
import { deprecatedSkillsAtom } from '../atoms/deprecatedSkills'
|
|
15
|
+
import { installedSkillsAtom } from '../atoms/installedSkills'
|
|
16
|
+
import { selectedAgentsAtom } from '../atoms/wizard'
|
|
17
|
+
|
|
18
|
+
interface SkillBrowserProps {
|
|
19
|
+
onInstall?: (selectedSkills: SkillInfo[]) => void
|
|
20
|
+
onExit?: () => void
|
|
21
|
+
overrideSkills?: SkillInfo[]
|
|
22
|
+
readOnly?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type VisualItem =
|
|
26
|
+
| { type: 'header'; category: string; categoryId?: string; count: number; installedCount: number }
|
|
27
|
+
| { type: 'skill'; skill: SkillInfo }
|
|
28
|
+
|
|
29
|
+
const MIN_VISIBLE = 5
|
|
30
|
+
const CHROME_LINES = 24
|
|
31
|
+
const PANEL_WIDTH_RATIO = 0.35
|
|
32
|
+
|
|
33
|
+
const getShortcuts = (readOnly: boolean): ShortcutEntry[] => {
|
|
34
|
+
const common = [
|
|
35
|
+
{ key: '/', description: 'Filter skills' },
|
|
36
|
+
{ key: '←/→', description: 'Collapse / expand' },
|
|
37
|
+
{ key: 'tab/→', description: 'Skill details' },
|
|
38
|
+
{ key: 'f', description: 'Expand / compact panel' },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const exitKey = { key: 'esc', description: readOnly ? 'Exit / close panel' : 'Back / close panel' }
|
|
42
|
+
|
|
43
|
+
if (readOnly) return [...common, exitKey]
|
|
44
|
+
|
|
45
|
+
return [
|
|
46
|
+
...common,
|
|
47
|
+
{ key: 'space', description: 'Toggle / expand' },
|
|
48
|
+
{ key: 'enter', description: 'Install selected' },
|
|
49
|
+
{ key: 'ctrl+a', description: 'Select all' },
|
|
50
|
+
exitKey,
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const SkillBrowser = ({ onInstall, onExit, overrideSkills, readOnly = false }: SkillBrowserProps) => {
|
|
55
|
+
const { skills: fetchedSkills, loading: fetching, error } = useSkills()
|
|
56
|
+
const { stdout } = useStdout()
|
|
57
|
+
|
|
58
|
+
const selectedAgents = useAtomValue(selectedAgentsAtom)
|
|
59
|
+
const installedSkills = useAtomValue(installedSkillsAtom)
|
|
60
|
+
const deprecatedMap = useAtomValue(deprecatedSkillsAtom)
|
|
61
|
+
|
|
62
|
+
const skills = overrideSkills || fetchedSkills
|
|
63
|
+
const loading = overrideSkills ? false : fetching
|
|
64
|
+
|
|
65
|
+
const { query, setQuery, filtered } = useFilter(skills, {
|
|
66
|
+
keys: ['name', 'description', 'category'],
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const [selectedSet, setSelectedSet] = useState<Set<string>>(new Set())
|
|
70
|
+
const [focusArea, setFocusArea] = useState<'search' | 'list'>('list')
|
|
71
|
+
const [listIndex, setListIndex] = useState(0)
|
|
72
|
+
const [offset, setOffset] = useState(0)
|
|
73
|
+
const [showSearch, setShowSearch] = useState(false)
|
|
74
|
+
const [showShortcuts, setShowShortcuts] = useState(false)
|
|
75
|
+
const [detailSkill, setDetailSkill] = useState<SkillInfo | null>(null)
|
|
76
|
+
const [drawerExpanded, setDrawerExpanded] = useState(false)
|
|
77
|
+
const [expandedCategory, setExpandedCategory] = useState<string | null>(null)
|
|
78
|
+
|
|
79
|
+
const canShowPanel = canShowDetailPanel()
|
|
80
|
+
const terminalRows = stdout?.rows ?? 40
|
|
81
|
+
const terminalCols = stdout?.columns ?? 120
|
|
82
|
+
const VISIBLE_ITEMS = Math.max(MIN_VISIBLE, terminalRows - CHROME_LINES)
|
|
83
|
+
const panelWidth = Math.max(30, Math.round(terminalCols * PANEL_WIDTH_RATIO))
|
|
84
|
+
const contentAreaHeight = Math.max(10, terminalRows - 17)
|
|
85
|
+
const isSearchExpanded = query.trim().length > 0
|
|
86
|
+
|
|
87
|
+
const groupedMap = useMemo(() => groupSkillsByCategory(filtered), [filtered])
|
|
88
|
+
|
|
89
|
+
useMemo(() => {
|
|
90
|
+
if (query) setShowSearch(true)
|
|
91
|
+
}, [query])
|
|
92
|
+
|
|
93
|
+
const isCategoryExpanded = (categoryName: string) => isSearchExpanded || expandedCategory === categoryName
|
|
94
|
+
|
|
95
|
+
const toggleCategory = (categoryName: string) => {
|
|
96
|
+
setExpandedCategory(isCategoryExpanded(categoryName) ? null : categoryName)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const visualList = useMemo(() => {
|
|
100
|
+
const list: VisualItem[] = []
|
|
101
|
+
|
|
102
|
+
for (const [category, categorySkills] of groupedMap.entries()) {
|
|
103
|
+
const installedCount = categorySkills.filter((s) => {
|
|
104
|
+
const agents = installedSkills[s.name] || []
|
|
105
|
+
if (selectedAgents.length > 0) return agents.some((a) => selectedAgents.includes(a))
|
|
106
|
+
return agents.length > 0
|
|
107
|
+
}).length
|
|
108
|
+
|
|
109
|
+
list.push({
|
|
110
|
+
type: 'header',
|
|
111
|
+
category: category.name,
|
|
112
|
+
categoryId: category.id,
|
|
113
|
+
count: categorySkills.length,
|
|
114
|
+
installedCount,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if (isCategoryExpanded(category.name)) categorySkills.forEach((skill) => list.push({ type: 'skill', skill }))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return list
|
|
121
|
+
}, [groupedMap, expandedCategory, isSearchExpanded, installedSkills, selectedAgents])
|
|
122
|
+
|
|
123
|
+
const handleToggleShortcuts = () => setShowShortcuts((prev) => !prev)
|
|
124
|
+
|
|
125
|
+
const handleEscape = () => {
|
|
126
|
+
if (showSearch) {
|
|
127
|
+
setShowSearch(false)
|
|
128
|
+
setQuery('')
|
|
129
|
+
setFocusArea('list')
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
onExit?.()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const handleSelectAll = () => {
|
|
136
|
+
const allSkillNames = filtered.map((s) => s.name)
|
|
137
|
+
setSelectedSet(selectedSet.size === allSkillNames.length ? new Set() : new Set(allSkillNames))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const handleSearchNavigation = (key: { downArrow?: boolean; return?: boolean }) => {
|
|
141
|
+
if ((key.downArrow || key.return) && visualList.length > 0) {
|
|
142
|
+
setFocusArea('list')
|
|
143
|
+
setListIndex(0)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const handleUpArrow = () => {
|
|
148
|
+
if (listIndex === 0 && showSearch) {
|
|
149
|
+
setFocusArea('search')
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const newIndex = Math.max(0, listIndex - 1)
|
|
154
|
+
setListIndex(newIndex)
|
|
155
|
+
if (newIndex < offset) setOffset(newIndex)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const handleDownArrow = () => {
|
|
159
|
+
const newIndex = Math.min(visualList.length - 1, listIndex + 1)
|
|
160
|
+
setListIndex(newIndex)
|
|
161
|
+
if (newIndex >= offset + VISIBLE_ITEMS) setOffset(newIndex - VISIBLE_ITEMS + 1)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const handleSpaceKey = () => {
|
|
165
|
+
const item = visualList[listIndex]
|
|
166
|
+
|
|
167
|
+
if (item.type === 'header') {
|
|
168
|
+
toggleCategory(item.category)
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (item.type === 'skill' && !readOnly) {
|
|
173
|
+
const isInstalled =
|
|
174
|
+
selectedAgents.length > 0
|
|
175
|
+
? (installedSkills[item.skill.name]?.some((a) => selectedAgents.includes(a)) ?? false)
|
|
176
|
+
: (installedSkills[item.skill.name]?.length ?? 0) > 0
|
|
177
|
+
|
|
178
|
+
if (isInstalled && !overrideSkills) return
|
|
179
|
+
|
|
180
|
+
const newSet = new Set(selectedSet)
|
|
181
|
+
if (newSet.has(item.skill.name)) {
|
|
182
|
+
newSet.delete(item.skill.name)
|
|
183
|
+
} else {
|
|
184
|
+
newSet.add(item.skill.name)
|
|
185
|
+
}
|
|
186
|
+
setSelectedSet(newSet)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const handleEnterKey = () => {
|
|
191
|
+
const item = visualList[listIndex]
|
|
192
|
+
|
|
193
|
+
if (item.type === 'header') {
|
|
194
|
+
toggleCategory(item.category)
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (readOnly) return
|
|
199
|
+
|
|
200
|
+
const selectedSkills = skills.filter((s) => selectedSet.has(s.name))
|
|
201
|
+
if (selectedSkills.length > 0) onInstall?.(selectedSkills)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const handleTabOrRightArrow = (isTab: boolean) => {
|
|
205
|
+
const item = visualList[listIndex]
|
|
206
|
+
|
|
207
|
+
if (item.type === 'skill' && canShowPanel) {
|
|
208
|
+
setDetailSkill(item.skill)
|
|
209
|
+
setDrawerExpanded(false)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!isTab && item.type === 'header' && !isCategoryExpanded(item.category)) setExpandedCategory(item.category)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const handleLeftArrow = () => {
|
|
217
|
+
const item = visualList[listIndex]
|
|
218
|
+
if (item.type === 'header' && isCategoryExpanded(item.category)) setExpandedCategory(null)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const isRegularCharacter = (
|
|
222
|
+
input: string,
|
|
223
|
+
key: {
|
|
224
|
+
ctrl?: boolean
|
|
225
|
+
meta?: boolean
|
|
226
|
+
upArrow?: boolean
|
|
227
|
+
downArrow?: boolean
|
|
228
|
+
leftArrow?: boolean
|
|
229
|
+
rightArrow?: boolean
|
|
230
|
+
},
|
|
231
|
+
) =>
|
|
232
|
+
input.length === 1 && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow
|
|
233
|
+
|
|
234
|
+
useInput(
|
|
235
|
+
(input, key) => {
|
|
236
|
+
if (input === '?') return handleToggleShortcuts()
|
|
237
|
+
if (showShortcuts) return setShowShortcuts(false)
|
|
238
|
+
if (key.escape) return handleEscape()
|
|
239
|
+
|
|
240
|
+
if (!showSearch && input === '/') {
|
|
241
|
+
setShowSearch(true)
|
|
242
|
+
setFocusArea('search')
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (input === 'a' && key.ctrl && !readOnly) return handleSelectAll()
|
|
247
|
+
if (focusArea === 'search') return handleSearchNavigation(key)
|
|
248
|
+
|
|
249
|
+
if (focusArea === 'list') {
|
|
250
|
+
if (key.upArrow) return handleUpArrow()
|
|
251
|
+
if (key.downArrow) return handleDownArrow()
|
|
252
|
+
if (input === ' ') return handleSpaceKey()
|
|
253
|
+
if (key.return) return handleEnterKey()
|
|
254
|
+
if (key.tab) return handleTabOrRightArrow(true)
|
|
255
|
+
if (key.rightArrow) return handleTabOrRightArrow(false)
|
|
256
|
+
if (key.leftArrow) return handleLeftArrow()
|
|
257
|
+
|
|
258
|
+
if (isRegularCharacter(input, key)) {
|
|
259
|
+
setShowSearch(true)
|
|
260
|
+
setFocusArea('search')
|
|
261
|
+
setQuery(input)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
{ isActive: !detailSkill },
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
const visibleWindow = visualList.slice(offset, offset + VISIBLE_ITEMS)
|
|
269
|
+
const hasItemsAbove = offset > 0
|
|
270
|
+
const hasItemsBelow = offset + VISIBLE_ITEMS < visualList.length
|
|
271
|
+
const scrollPercent =
|
|
272
|
+
visualList.length <= VISIBLE_ITEMS ? 100 : Math.round(((offset + VISIBLE_ITEMS) / visualList.length) * 100)
|
|
273
|
+
const showSkillList = !detailSkill || !drawerExpanded
|
|
274
|
+
|
|
275
|
+
if (loading) {
|
|
276
|
+
return (
|
|
277
|
+
<Box flexDirection="column" paddingX={1}>
|
|
278
|
+
<Header />
|
|
279
|
+
<Box flexDirection="column" alignItems="center" justifyContent="center" paddingY={4}>
|
|
280
|
+
<Text color={colors.accent}>Loading skills...</Text>
|
|
281
|
+
</Box>
|
|
282
|
+
</Box>
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (error || (!loading && skills.length === 0)) {
|
|
287
|
+
return (
|
|
288
|
+
<Box flexDirection="column" paddingX={1} minHeight={20}>
|
|
289
|
+
<Header />
|
|
290
|
+
<Box flexDirection="column" alignItems="center" justifyContent="center" flexGrow={1}>
|
|
291
|
+
<Box
|
|
292
|
+
flexDirection="column"
|
|
293
|
+
borderStyle="round"
|
|
294
|
+
borderColor={colors.error}
|
|
295
|
+
paddingX={3}
|
|
296
|
+
paddingY={2}
|
|
297
|
+
alignItems="center"
|
|
298
|
+
>
|
|
299
|
+
<Text color={colors.error} bold>
|
|
300
|
+
{symbols.cross} No Skills Available
|
|
301
|
+
</Text>
|
|
302
|
+
<Box marginTop={1}>
|
|
303
|
+
<Text color={colors.textDim}>Check your internet connection and try again</Text>
|
|
304
|
+
</Box>
|
|
305
|
+
{error && (
|
|
306
|
+
<Box marginTop={1}>
|
|
307
|
+
<Text color={colors.textMuted} dimColor>
|
|
308
|
+
{error}
|
|
309
|
+
</Text>
|
|
310
|
+
</Box>
|
|
311
|
+
)}
|
|
312
|
+
</Box>
|
|
313
|
+
<Box marginTop={2}>
|
|
314
|
+
<Text color={colors.textDim}>
|
|
315
|
+
Press{' '}
|
|
316
|
+
<Text color={colors.accent} bold>
|
|
317
|
+
Esc
|
|
318
|
+
</Text>{' '}
|
|
319
|
+
to exit
|
|
320
|
+
</Text>
|
|
321
|
+
</Box>
|
|
322
|
+
</Box>
|
|
323
|
+
</Box>
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const renderSkillListItem = (item: VisualItem, index: number) => {
|
|
328
|
+
const realIndex = offset + index
|
|
329
|
+
const isFocused = focusArea === 'list' && realIndex === listIndex
|
|
330
|
+
|
|
331
|
+
if (item.type === 'header') {
|
|
332
|
+
return (
|
|
333
|
+
<Box key={`cat-${item.category}`} marginTop={index === 0 ? 0 : 1}>
|
|
334
|
+
<CategoryHeader
|
|
335
|
+
name={item.category}
|
|
336
|
+
categoryId={item.categoryId}
|
|
337
|
+
totalCount={item.count}
|
|
338
|
+
installedCount={item.installedCount}
|
|
339
|
+
isExpanded={isCategoryExpanded(item.category)}
|
|
340
|
+
isFocused={isFocused}
|
|
341
|
+
/>
|
|
342
|
+
</Box>
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const isSelected = selectedSet.has(item.skill.name)
|
|
347
|
+
const isInstalled =
|
|
348
|
+
selectedAgents.length > 0
|
|
349
|
+
? (installedSkills[item.skill.name]?.some((a) => selectedAgents.includes(a)) ?? false)
|
|
350
|
+
: (installedSkills[item.skill.name]?.length ?? 0) > 0
|
|
351
|
+
const isDeprecated = deprecatedMap instanceof Map && deprecatedMap.has(item.skill.name)
|
|
352
|
+
const status = isDeprecated ? 'deprecated' : isInstalled ? 'installed' : null
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<SkillCard
|
|
356
|
+
key={item.skill.name}
|
|
357
|
+
name={item.skill.name}
|
|
358
|
+
description={item.skill.description}
|
|
359
|
+
status={status}
|
|
360
|
+
selected={isSelected}
|
|
361
|
+
focused={isFocused}
|
|
362
|
+
readOnly={readOnly}
|
|
363
|
+
/>
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const renderScrollIndicator = (direction: 'up' | 'down') => (
|
|
368
|
+
<Box justifyContent="center" marginBottom={direction === 'up' ? 1 : 0} marginTop={direction === 'down' ? 1 : 0}>
|
|
369
|
+
<Text color={colors.textDim}>
|
|
370
|
+
{direction === 'up' ? symbols.arrowUp : symbols.arrowDown}{' '}
|
|
371
|
+
{direction === 'up' ? symbols.arrowUp : symbols.arrowDown}{' '}
|
|
372
|
+
{direction === 'up' ? symbols.arrowUp : symbols.arrowDown}
|
|
373
|
+
</Text>
|
|
374
|
+
</Box>
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
const renderFooterHints = () => {
|
|
378
|
+
if (detailSkill) {
|
|
379
|
+
return [
|
|
380
|
+
{ key: '↑/↓', label: 'scroll' },
|
|
381
|
+
{ key: 'f', label: drawerExpanded ? 'compact' : 'expand' },
|
|
382
|
+
{ key: 'Esc', label: 'close', color: colors.warning },
|
|
383
|
+
]
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (readOnly) {
|
|
387
|
+
return [
|
|
388
|
+
{ key: '/', label: 'filter' },
|
|
389
|
+
{ key: 'tab', label: 'detail' },
|
|
390
|
+
{ key: 'esc', label: 'exit', color: colors.warning },
|
|
391
|
+
{ key: '?', label: 'help' },
|
|
392
|
+
]
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return [
|
|
396
|
+
{ key: 'space', label: 'toggle' },
|
|
397
|
+
{ key: 'enter', label: 'install', color: colors.success },
|
|
398
|
+
{ key: '/', label: 'filter' },
|
|
399
|
+
{ key: 'tab', label: 'detail' },
|
|
400
|
+
{ key: 'esc', label: 'exit', color: colors.warning },
|
|
401
|
+
{ key: '?', label: 'help' },
|
|
402
|
+
]
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const renderFooterStatus = () => {
|
|
406
|
+
if (detailSkill) return undefined
|
|
407
|
+
|
|
408
|
+
return (
|
|
409
|
+
<>
|
|
410
|
+
{!readOnly && selectedSet.size > 0 && (
|
|
411
|
+
<Text>
|
|
412
|
+
<Text color={colors.success} bold>
|
|
413
|
+
{symbols.checkboxActive} {selectedSet.size}
|
|
414
|
+
</Text>
|
|
415
|
+
<Text color={colors.textDim}> selected</Text>
|
|
416
|
+
</Text>
|
|
417
|
+
)}
|
|
418
|
+
{visualList.length > VISIBLE_ITEMS && (
|
|
419
|
+
<Text color={colors.textDim}>
|
|
420
|
+
{!readOnly && selectedSet.size > 0 ? ` ${symbols.dot} ` : ''}
|
|
421
|
+
{scrollPercent}%
|
|
422
|
+
</Text>
|
|
423
|
+
)}
|
|
424
|
+
</>
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<Box flexDirection="column" paddingX={1} minHeight={20}>
|
|
430
|
+
<Header />
|
|
431
|
+
|
|
432
|
+
{showShortcuts ? (
|
|
433
|
+
<Box flexDirection="column" flexGrow={1} alignItems="center" justifyContent="center">
|
|
434
|
+
<KeyboardShortcutsOverlay
|
|
435
|
+
visible={showShortcuts}
|
|
436
|
+
onDismiss={() => setShowShortcuts(false)}
|
|
437
|
+
shortcuts={getShortcuts(readOnly)}
|
|
438
|
+
/>
|
|
439
|
+
</Box>
|
|
440
|
+
) : (
|
|
441
|
+
<Box
|
|
442
|
+
flexDirection="row"
|
|
443
|
+
height={detailSkill ? contentAreaHeight : undefined}
|
|
444
|
+
flexGrow={detailSkill ? 0 : 1}
|
|
445
|
+
overflow="hidden"
|
|
446
|
+
>
|
|
447
|
+
{showSkillList && (
|
|
448
|
+
<Box key="skill-list" flexDirection="column" flexGrow={1} flexShrink={1}>
|
|
449
|
+
{showSearch && (
|
|
450
|
+
<Box marginBottom={1}>
|
|
451
|
+
<SearchInput
|
|
452
|
+
query={query}
|
|
453
|
+
onChange={(q) => {
|
|
454
|
+
setQuery(q)
|
|
455
|
+
setListIndex(0)
|
|
456
|
+
setOffset(0)
|
|
457
|
+
}}
|
|
458
|
+
total={skills.length}
|
|
459
|
+
filtered={filtered.length}
|
|
460
|
+
isLoading={loading}
|
|
461
|
+
focus={focusArea === 'search'}
|
|
462
|
+
/>
|
|
463
|
+
</Box>
|
|
464
|
+
)}
|
|
465
|
+
|
|
466
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
467
|
+
{hasItemsAbove && renderScrollIndicator('up')}
|
|
468
|
+
{visibleWindow.map(renderSkillListItem)}
|
|
469
|
+
{hasItemsBelow && renderScrollIndicator('down')}
|
|
470
|
+
{visualList.length === 0 && (
|
|
471
|
+
<Box paddingY={1}>
|
|
472
|
+
<Text color={colors.textMuted}>No skills match "{query}"</Text>
|
|
473
|
+
</Box>
|
|
474
|
+
)}
|
|
475
|
+
</Box>
|
|
476
|
+
</Box>
|
|
477
|
+
)}
|
|
478
|
+
|
|
479
|
+
{detailSkill && (
|
|
480
|
+
<Box
|
|
481
|
+
key="detail-panel"
|
|
482
|
+
flexDirection="column"
|
|
483
|
+
width={drawerExpanded ? undefined : panelWidth}
|
|
484
|
+
flexGrow={drawerExpanded ? 1 : 0}
|
|
485
|
+
flexShrink={0}
|
|
486
|
+
>
|
|
487
|
+
<SkillDetailPanel
|
|
488
|
+
skill={detailSkill}
|
|
489
|
+
expanded={drawerExpanded}
|
|
490
|
+
onClose={() => {
|
|
491
|
+
setDetailSkill(null)
|
|
492
|
+
setDrawerExpanded(false)
|
|
493
|
+
}}
|
|
494
|
+
onToggleExpand={() => setDrawerExpanded((prev) => !prev)}
|
|
495
|
+
/>
|
|
496
|
+
</Box>
|
|
497
|
+
)}
|
|
498
|
+
</Box>
|
|
499
|
+
)}
|
|
500
|
+
|
|
501
|
+
<FooterBar hints={renderFooterHints()} status={renderFooterStatus()} />
|
|
502
|
+
</Box>
|
|
503
|
+
)
|
|
504
|
+
}
|