@skillsgate/tui 0.1.1
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/bin/skillsgate-tui +28 -0
- package/bunfig.toml +3 -0
- package/package.json +24 -0
- package/src/app.tsx +18 -0
- package/src/components/agent-filter.tsx +162 -0
- package/src/components/confirm-dialog.tsx +56 -0
- package/src/components/help-overlay.tsx +101 -0
- package/src/components/layout.tsx +272 -0
- package/src/components/search-input.tsx +48 -0
- package/src/components/skill-list-item.tsx +45 -0
- package/src/components/skill-list.tsx +245 -0
- package/src/components/status-bar.tsx +34 -0
- package/src/data/api-client.ts +151 -0
- package/src/data/use-agents.ts +41 -0
- package/src/data/use-auth.ts +136 -0
- package/src/data/use-favorites.ts +147 -0
- package/src/data/use-installed-skills.ts +128 -0
- package/src/data/use-search.ts +118 -0
- package/src/data/use-skill-actions.ts +333 -0
- package/src/db/context.tsx +38 -0
- package/src/db/index.ts +19 -0
- package/src/db/migrations.ts +72 -0
- package/src/db/servers.ts +154 -0
- package/src/db/settings.ts +43 -0
- package/src/db/skills.ts +138 -0
- package/src/db/ssh.ts +319 -0
- package/src/index.tsx +37 -0
- package/src/store/context.tsx +26 -0
- package/src/store/reducers.ts +126 -0
- package/src/store/types.ts +124 -0
- package/src/utils/colors.ts +42 -0
- package/src/views/add-server.tsx +240 -0
- package/src/views/discover.tsx +419 -0
- package/src/views/favorites.tsx +358 -0
- package/src/views/home.tsx +218 -0
- package/src/views/login.tsx +202 -0
- package/src/views/server-skills.tsx +269 -0
- package/src/views/servers.tsx +449 -0
- package/src/views/settings.tsx +185 -0
- package/src/views/skill-detail.tsx +497 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { useState, useMemo, useEffect } from "react"
|
|
2
|
+
import { useKeyboard } from "@opentui/react"
|
|
3
|
+
import { useStore, useDispatch } from "../store/context.js"
|
|
4
|
+
import { useFavorites } from "../data/use-favorites.js"
|
|
5
|
+
import { useSkillActions } from "../data/use-skill-actions.js"
|
|
6
|
+
import { ConfirmDialog } from "../components/confirm-dialog.js"
|
|
7
|
+
import { colors } from "../utils/colors.js"
|
|
8
|
+
import type { CatalogSkill } from "../data/api-client.js"
|
|
9
|
+
import type { EnrichedSkill } from "../store/types.js"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Favorites view: two-column layout.
|
|
13
|
+
* LEFT - Favorites list (40%)
|
|
14
|
+
* RIGHT - Selected favorite detail (flexGrow)
|
|
15
|
+
*
|
|
16
|
+
* Requires authentication. Shows a prompt to login if not authenticated.
|
|
17
|
+
*/
|
|
18
|
+
export function FavoritesView() {
|
|
19
|
+
const state = useStore()
|
|
20
|
+
const dispatch = useDispatch()
|
|
21
|
+
const { favorites, loading, error, toggle } = useFavorites()
|
|
22
|
+
const { installSkill } = useSkillActions()
|
|
23
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
24
|
+
const [installTarget, setInstallTarget] = useState<CatalogSkill | null>(null)
|
|
25
|
+
const [previewSkill, setPreviewSkill] = useState<CatalogSkill | null>(null)
|
|
26
|
+
|
|
27
|
+
// Build a set of installed skill names for the "installed" badge
|
|
28
|
+
const installedNames = useMemo(() => {
|
|
29
|
+
return new Set(state.installedSkills.map((s) => s.name.toLowerCase()))
|
|
30
|
+
}, [state.installedSkills])
|
|
31
|
+
|
|
32
|
+
// Update preview when selection changes
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (favorites[selectedIndex]) {
|
|
35
|
+
setPreviewSkill(favorites[selectedIndex])
|
|
36
|
+
} else {
|
|
37
|
+
setPreviewSkill(null)
|
|
38
|
+
}
|
|
39
|
+
}, [selectedIndex, favorites])
|
|
40
|
+
|
|
41
|
+
// Keyboard navigation for the favorites list
|
|
42
|
+
useKeyboard((key) => {
|
|
43
|
+
if (state.activeView !== "favorites") return
|
|
44
|
+
if (state.showHelp) return
|
|
45
|
+
if (!state.auth) return // No navigation when not logged in
|
|
46
|
+
if (installTarget) return // Block navigation during confirm dialog
|
|
47
|
+
|
|
48
|
+
// j/k or arrow keys
|
|
49
|
+
if (key.name === "up" || (key.name === "k" && !key.ctrl)) {
|
|
50
|
+
setSelectedIndex((i) => Math.max(0, i - 1))
|
|
51
|
+
}
|
|
52
|
+
if (key.name === "down" || (key.name === "j" && !key.ctrl)) {
|
|
53
|
+
setSelectedIndex((i) => Math.min(favorites.length - 1, i + 1))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// g = first, G = last
|
|
57
|
+
if (key.name === "g" && !key.shift) {
|
|
58
|
+
setSelectedIndex(0)
|
|
59
|
+
}
|
|
60
|
+
if (key.name === "g" && key.shift) {
|
|
61
|
+
setSelectedIndex(Math.max(0, favorites.length - 1))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// v to view full detail
|
|
65
|
+
if (key.name === "v" && favorites[selectedIndex]) {
|
|
66
|
+
const skill = favorites[selectedIndex]
|
|
67
|
+
dispatch({
|
|
68
|
+
type: "SELECT_SKILL",
|
|
69
|
+
skill: catalogSkillToEnriched(skill, installedNames),
|
|
70
|
+
})
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// x to unfavorite
|
|
75
|
+
if (key.name === "x" && favorites[selectedIndex]) {
|
|
76
|
+
const skill = favorites[selectedIndex]
|
|
77
|
+
toggle(skill.id)
|
|
78
|
+
dispatch({
|
|
79
|
+
type: "SHOW_NOTIFICATION",
|
|
80
|
+
notification: { type: "info", message: `Removed "${skill.name}" from favorites` },
|
|
81
|
+
})
|
|
82
|
+
// Adjust selection if we removed the last item
|
|
83
|
+
if (selectedIndex >= favorites.length - 1 && selectedIndex > 0) {
|
|
84
|
+
setSelectedIndex(selectedIndex - 1)
|
|
85
|
+
}
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// i to install
|
|
90
|
+
if (key.name === "i" && favorites[selectedIndex]) {
|
|
91
|
+
setInstallTarget(favorites[selectedIndex])
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Confirm dialog for install
|
|
97
|
+
if (installTarget) {
|
|
98
|
+
return (
|
|
99
|
+
<ConfirmDialog
|
|
100
|
+
message={`Install "${installTarget.name}"?`}
|
|
101
|
+
onConfirm={async () => {
|
|
102
|
+
const skill = catalogSkillToEnriched(installTarget, installedNames)
|
|
103
|
+
setInstallTarget(null)
|
|
104
|
+
await installSkill(skill)
|
|
105
|
+
}}
|
|
106
|
+
onCancel={() => setInstallTarget(null)}
|
|
107
|
+
/>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Not authenticated
|
|
112
|
+
if (!state.auth) {
|
|
113
|
+
return (
|
|
114
|
+
<box style={{ flexDirection: "column", padding: 2 }}>
|
|
115
|
+
<text fg={colors.text}>
|
|
116
|
+
Sign in to view your favorites
|
|
117
|
+
</text>
|
|
118
|
+
<text>{" "}</text>
|
|
119
|
+
<text fg={colors.textDim}>
|
|
120
|
+
Press <span fg={colors.primary}>l</span> to login
|
|
121
|
+
</text>
|
|
122
|
+
</box>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Loading state
|
|
127
|
+
if (loading && favorites.length === 0) {
|
|
128
|
+
return (
|
|
129
|
+
<box style={{ padding: 1 }}>
|
|
130
|
+
<text fg={colors.textDim}>Loading favorites...</text>
|
|
131
|
+
</box>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Error state
|
|
136
|
+
if (error && favorites.length === 0) {
|
|
137
|
+
return (
|
|
138
|
+
<box style={{ padding: 1 }}>
|
|
139
|
+
<text fg={colors.error}>Error: {error}</text>
|
|
140
|
+
</box>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<box style={{ flexDirection: "column", width: "100%", flexGrow: 1 }}>
|
|
146
|
+
{/* Status line */}
|
|
147
|
+
<box
|
|
148
|
+
style={{
|
|
149
|
+
height: 1,
|
|
150
|
+
width: "100%",
|
|
151
|
+
paddingLeft: 1,
|
|
152
|
+
backgroundColor: colors.bgAlt,
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
<text fg={colors.textDim}>
|
|
156
|
+
{favorites.length} favorite{favorites.length !== 1 ? "s" : ""}
|
|
157
|
+
{loading ? " (refreshing...)" : ""}
|
|
158
|
+
</text>
|
|
159
|
+
</box>
|
|
160
|
+
|
|
161
|
+
{/* Two-column content: list | detail */}
|
|
162
|
+
<box style={{ flexDirection: "row", flexGrow: 1, width: "100%" }}>
|
|
163
|
+
{/* LEFT: Favorites list */}
|
|
164
|
+
<box
|
|
165
|
+
style={{
|
|
166
|
+
width: "40%",
|
|
167
|
+
borderRight: true,
|
|
168
|
+
borderColor: colors.border,
|
|
169
|
+
flexDirection: "column",
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
{/* List header */}
|
|
173
|
+
<box style={{ height: 1, paddingLeft: 1, backgroundColor: colors.bgAlt }}>
|
|
174
|
+
<text fg={colors.textDim}>FAVORITES</text>
|
|
175
|
+
</box>
|
|
176
|
+
|
|
177
|
+
{favorites.length === 0 ? (
|
|
178
|
+
<box style={{ padding: 1 }}>
|
|
179
|
+
<text fg={colors.textDim}>
|
|
180
|
+
No favorites yet. Browse the Discover tab to find and favorite skills.
|
|
181
|
+
</text>
|
|
182
|
+
</box>
|
|
183
|
+
) : (
|
|
184
|
+
<scrollbox
|
|
185
|
+
focused={state.activeView === "favorites" && !state.showHelp}
|
|
186
|
+
style={{
|
|
187
|
+
width: "100%",
|
|
188
|
+
flexGrow: 1,
|
|
189
|
+
rootOptions: { backgroundColor: colors.bg },
|
|
190
|
+
viewportOptions: { backgroundColor: colors.bg },
|
|
191
|
+
contentOptions: { backgroundColor: colors.bg },
|
|
192
|
+
scrollbarOptions: {
|
|
193
|
+
trackOptions: {
|
|
194
|
+
foregroundColor: colors.primary,
|
|
195
|
+
backgroundColor: colors.border,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
{favorites.map((skill, i) => {
|
|
201
|
+
const isInstalled = installedNames.has(skill.name?.toLowerCase() ?? "")
|
|
202
|
+
return (
|
|
203
|
+
<box
|
|
204
|
+
key={skill.id ?? `${skill.slug}-${i}`}
|
|
205
|
+
style={{
|
|
206
|
+
width: "100%",
|
|
207
|
+
paddingLeft: 1,
|
|
208
|
+
paddingRight: 1,
|
|
209
|
+
flexDirection: "row",
|
|
210
|
+
backgroundColor: i === selectedIndex ? colors.bgAlt : "transparent",
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
<text fg={i === selectedIndex ? colors.primary : colors.text}>
|
|
214
|
+
{skill.name}
|
|
215
|
+
</text>
|
|
216
|
+
{isInstalled ? (
|
|
217
|
+
<text fg={colors.success}> *</text>
|
|
218
|
+
) : null}
|
|
219
|
+
</box>
|
|
220
|
+
)
|
|
221
|
+
})}
|
|
222
|
+
</scrollbox>
|
|
223
|
+
)}
|
|
224
|
+
</box>
|
|
225
|
+
|
|
226
|
+
{/* RIGHT: Detail panel */}
|
|
227
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
228
|
+
{previewSkill ? (
|
|
229
|
+
<FavoriteDetailPanel
|
|
230
|
+
skill={previewSkill}
|
|
231
|
+
isInstalled={installedNames.has(previewSkill.name?.toLowerCase() ?? "")}
|
|
232
|
+
/>
|
|
233
|
+
) : (
|
|
234
|
+
<box style={{ padding: 1 }}>
|
|
235
|
+
<text fg={colors.textDim}>Select a favorite to view details</text>
|
|
236
|
+
</box>
|
|
237
|
+
)}
|
|
238
|
+
</box>
|
|
239
|
+
</box>
|
|
240
|
+
</box>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---------- Inline Detail Panel ----------
|
|
245
|
+
|
|
246
|
+
interface FavoriteDetailPanelProps {
|
|
247
|
+
skill: CatalogSkill
|
|
248
|
+
isInstalled: boolean
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function FavoriteDetailPanel({ skill, isInstalled }: FavoriteDetailPanelProps) {
|
|
252
|
+
const description = skill.summary || skill.description || ""
|
|
253
|
+
const categories = skill.categories?.join(", ") ?? ""
|
|
254
|
+
const sourceLabel = skill.githubUrl ? "github" : "skillsgate"
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<scrollbox
|
|
258
|
+
focused={false}
|
|
259
|
+
style={{
|
|
260
|
+
width: "100%",
|
|
261
|
+
flexGrow: 1,
|
|
262
|
+
rootOptions: { backgroundColor: colors.bg },
|
|
263
|
+
viewportOptions: { backgroundColor: colors.bg },
|
|
264
|
+
contentOptions: { backgroundColor: colors.bg },
|
|
265
|
+
scrollbarOptions: {
|
|
266
|
+
trackOptions: {
|
|
267
|
+
foregroundColor: colors.primary,
|
|
268
|
+
backgroundColor: colors.border,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
}}
|
|
272
|
+
>
|
|
273
|
+
<box style={{ paddingLeft: 1, paddingRight: 1, flexDirection: "column" }}>
|
|
274
|
+
{/* Name */}
|
|
275
|
+
<text fg={colors.primary}>
|
|
276
|
+
<strong>{skill.name}</strong>
|
|
277
|
+
</text>
|
|
278
|
+
|
|
279
|
+
{/* Status */}
|
|
280
|
+
{isInstalled ? (
|
|
281
|
+
<text fg={colors.success}>Installed</text>
|
|
282
|
+
) : (
|
|
283
|
+
<text fg={colors.textDim}>Not installed</text>
|
|
284
|
+
)}
|
|
285
|
+
<text>{" "}</text>
|
|
286
|
+
|
|
287
|
+
{/* Description */}
|
|
288
|
+
<text fg={colors.text}>{description}</text>
|
|
289
|
+
<text>{" "}</text>
|
|
290
|
+
|
|
291
|
+
{/* Source */}
|
|
292
|
+
<box style={{ flexDirection: "row", height: 1 }}>
|
|
293
|
+
<text fg={colors.textDim}>Source: </text>
|
|
294
|
+
<text fg={colors.secondary}>{sourceLabel}</text>
|
|
295
|
+
</box>
|
|
296
|
+
|
|
297
|
+
{/* Categories */}
|
|
298
|
+
{categories ? (
|
|
299
|
+
<box style={{ flexDirection: "row", height: 1 }}>
|
|
300
|
+
<text fg={colors.textDim}>Categories: </text>
|
|
301
|
+
<text fg={colors.secondary}>{categories}</text>
|
|
302
|
+
</box>
|
|
303
|
+
) : null}
|
|
304
|
+
|
|
305
|
+
{/* GitHub URL */}
|
|
306
|
+
{skill.githubUrl ? (
|
|
307
|
+
<box style={{ flexDirection: "row", height: 1 }}>
|
|
308
|
+
<text fg={colors.textDim}>GitHub: </text>
|
|
309
|
+
<text fg={colors.primary}>{skill.githubUrl}</text>
|
|
310
|
+
</box>
|
|
311
|
+
) : null}
|
|
312
|
+
|
|
313
|
+
{/* Install command */}
|
|
314
|
+
{skill.installCommand ? (
|
|
315
|
+
<>
|
|
316
|
+
<text>{" "}</text>
|
|
317
|
+
<text fg={colors.textDim}>Install:</text>
|
|
318
|
+
<text fg={colors.success}> {skill.installCommand}</text>
|
|
319
|
+
</>
|
|
320
|
+
) : null}
|
|
321
|
+
|
|
322
|
+
<text>{" "}</text>
|
|
323
|
+
<text fg={colors.textDim}>v=full detail x=unfavorite i=install</text>
|
|
324
|
+
</box>
|
|
325
|
+
</scrollbox>
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---------- Helpers ----------
|
|
330
|
+
|
|
331
|
+
function catalogSkillToEnriched(
|
|
332
|
+
skill: CatalogSkill,
|
|
333
|
+
installedNames: Set<string>
|
|
334
|
+
): EnrichedSkill {
|
|
335
|
+
return {
|
|
336
|
+
name: skill.name,
|
|
337
|
+
description: skill.summary || skill.description || "",
|
|
338
|
+
filePath: "",
|
|
339
|
+
agents: [],
|
|
340
|
+
metadata: {
|
|
341
|
+
categories: skill.categories,
|
|
342
|
+
capabilities: skill.capabilities,
|
|
343
|
+
keywords: skill.keywords,
|
|
344
|
+
githubUrl: skill.githubUrl,
|
|
345
|
+
installCommand: skill.installCommand,
|
|
346
|
+
},
|
|
347
|
+
lock: skill.githubUrl
|
|
348
|
+
? {
|
|
349
|
+
source: skill.githubUrl,
|
|
350
|
+
sourceType: "github" as const,
|
|
351
|
+
originalUrl: skill.githubUrl,
|
|
352
|
+
skillFolderHash: "",
|
|
353
|
+
installedAt: "",
|
|
354
|
+
updatedAt: "",
|
|
355
|
+
}
|
|
356
|
+
: undefined,
|
|
357
|
+
}
|
|
358
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { useMemo, useState, useEffect } from "react"
|
|
2
|
+
import fs from "node:fs"
|
|
3
|
+
import { useStore } from "../store/context.js"
|
|
4
|
+
import { AgentFilter } from "../components/agent-filter.js"
|
|
5
|
+
import { SkillList } from "../components/skill-list.js"
|
|
6
|
+
import { colors, agentBadges as badgeMap } from "../utils/colors.js"
|
|
7
|
+
import type { EnrichedSkill } from "../store/types.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reads the full SKILL.md content for inline display.
|
|
11
|
+
*/
|
|
12
|
+
function readSkillContent(filePath: string): string {
|
|
13
|
+
try {
|
|
14
|
+
return fs.readFileSync(filePath, "utf-8")
|
|
15
|
+
} catch {
|
|
16
|
+
return ""
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Strips frontmatter (--- delimited block at the top) from markdown content.
|
|
22
|
+
*/
|
|
23
|
+
function stripFrontmatter(content: string): string {
|
|
24
|
+
const lines = content.split("\n")
|
|
25
|
+
if (lines[0]?.trim() !== "---") return content
|
|
26
|
+
|
|
27
|
+
let endIndex = -1
|
|
28
|
+
for (let i = 1; i < lines.length; i++) {
|
|
29
|
+
if (lines[i].trim() === "---") {
|
|
30
|
+
endIndex = i
|
|
31
|
+
break
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (endIndex === -1) return content
|
|
36
|
+
return lines.slice(endIndex + 1).join("\n").trimStart()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Three-panel home view:
|
|
41
|
+
* LEFT - Agent filter sidebar (fixed 22 chars)
|
|
42
|
+
* MIDDLE - Compact skill name list (30%)
|
|
43
|
+
* RIGHT - Skill detail panel (flexGrow)
|
|
44
|
+
*/
|
|
45
|
+
export function HomeView() {
|
|
46
|
+
const state = useStore()
|
|
47
|
+
|
|
48
|
+
// Apply agent filter and text filter
|
|
49
|
+
const filteredSkills = useMemo(() => {
|
|
50
|
+
let skills = state.installedSkills
|
|
51
|
+
|
|
52
|
+
// Agent filter
|
|
53
|
+
if (state.selectedAgentFilter !== "all") {
|
|
54
|
+
skills = skills.filter((s) =>
|
|
55
|
+
s.agents.includes(state.selectedAgentFilter as any)
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Text filter
|
|
60
|
+
if (state.installedFilter) {
|
|
61
|
+
const q = state.installedFilter.toLowerCase()
|
|
62
|
+
skills = skills.filter(
|
|
63
|
+
(s) =>
|
|
64
|
+
s.name.toLowerCase().includes(q) ||
|
|
65
|
+
s.description.toLowerCase().includes(q)
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return skills
|
|
70
|
+
}, [state.installedSkills, state.selectedAgentFilter, state.installedFilter])
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<box style={{ flexDirection: "row", width: "100%", flexGrow: 1 }}>
|
|
74
|
+
{/* LEFT: Agent filter sidebar */}
|
|
75
|
+
<AgentFilter />
|
|
76
|
+
|
|
77
|
+
{/* MIDDLE: Skill list */}
|
|
78
|
+
<box
|
|
79
|
+
style={{
|
|
80
|
+
width: "30%",
|
|
81
|
+
borderRight: true,
|
|
82
|
+
borderColor: state.focusedPane === "list" ? colors.primary : colors.border,
|
|
83
|
+
flexDirection: "column",
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<SkillList skills={filteredSkills} />
|
|
87
|
+
</box>
|
|
88
|
+
|
|
89
|
+
{/* RIGHT: Detail panel */}
|
|
90
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
91
|
+
{state.selectedSkill ? (
|
|
92
|
+
<DetailPanel skill={state.selectedSkill} />
|
|
93
|
+
) : (
|
|
94
|
+
<box style={{ padding: 1 }}>
|
|
95
|
+
<text fg={colors.textDim}>
|
|
96
|
+
{filteredSkills.length > 0
|
|
97
|
+
? "Select a skill to view details"
|
|
98
|
+
: "No skills to display"}
|
|
99
|
+
</text>
|
|
100
|
+
</box>
|
|
101
|
+
)}
|
|
102
|
+
</box>
|
|
103
|
+
</box>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------- Inline Detail Panel ----------
|
|
108
|
+
|
|
109
|
+
interface DetailPanelProps {
|
|
110
|
+
skill: EnrichedSkill
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function DetailPanel({ skill }: DetailPanelProps) {
|
|
114
|
+
const state = useStore()
|
|
115
|
+
const [content, setContent] = useState("")
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (skill.filePath) {
|
|
119
|
+
const raw = readSkillContent(skill.filePath)
|
|
120
|
+
setContent(stripFrontmatter(raw))
|
|
121
|
+
} else {
|
|
122
|
+
setContent("")
|
|
123
|
+
}
|
|
124
|
+
}, [skill.filePath, skill.name])
|
|
125
|
+
|
|
126
|
+
const sourceType = skill.lock?.sourceType ?? "unknown"
|
|
127
|
+
const sourceUrl = skill.lock?.originalUrl ?? ""
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<scrollbox
|
|
131
|
+
focused={false}
|
|
132
|
+
style={{
|
|
133
|
+
width: "100%",
|
|
134
|
+
flexGrow: 1,
|
|
135
|
+
rootOptions: { backgroundColor: colors.bg },
|
|
136
|
+
viewportOptions: { backgroundColor: colors.bg },
|
|
137
|
+
contentOptions: { backgroundColor: colors.bg },
|
|
138
|
+
scrollbarOptions: {
|
|
139
|
+
trackOptions: {
|
|
140
|
+
foregroundColor: colors.primary,
|
|
141
|
+
backgroundColor: colors.border,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
<box style={{ paddingLeft: 1, paddingRight: 1, paddingTop: 0, flexDirection: "column" }}>
|
|
147
|
+
{/* Skill name */}
|
|
148
|
+
<text fg={colors.primary}>
|
|
149
|
+
<strong>{skill.name}</strong>
|
|
150
|
+
</text>
|
|
151
|
+
|
|
152
|
+
{/* Description */}
|
|
153
|
+
<text fg={colors.text}>{skill.description}</text>
|
|
154
|
+
<text>{" "}</text>
|
|
155
|
+
|
|
156
|
+
{/* Metadata row: source + agents */}
|
|
157
|
+
<box style={{ flexDirection: "row", height: 1 }}>
|
|
158
|
+
<text fg={colors.textDim}>Source: </text>
|
|
159
|
+
<text fg={colors.secondary}>{sourceType}</text>
|
|
160
|
+
{sourceUrl ? (
|
|
161
|
+
<>
|
|
162
|
+
<text fg={colors.textDim}> URL: </text>
|
|
163
|
+
<text fg={colors.primary}>{sourceUrl}</text>
|
|
164
|
+
</>
|
|
165
|
+
) : null}
|
|
166
|
+
</box>
|
|
167
|
+
|
|
168
|
+
{/* Agent badges */}
|
|
169
|
+
{skill.agents.length > 0 ? (
|
|
170
|
+
<box style={{ flexDirection: "row", height: 1 }}>
|
|
171
|
+
<text fg={colors.textDim}>Agents: </text>
|
|
172
|
+
{skill.agents.map((a, i) => {
|
|
173
|
+
const badge = badgeMap[a]
|
|
174
|
+
return (
|
|
175
|
+
<text key={a} fg={badge?.color ?? colors.agent}>
|
|
176
|
+
{i > 0 ? " " : ""}{badge?.label ?? a.slice(0, 2).toUpperCase()}
|
|
177
|
+
</text>
|
|
178
|
+
)
|
|
179
|
+
})}
|
|
180
|
+
</box>
|
|
181
|
+
) : null}
|
|
182
|
+
|
|
183
|
+
<text>{" "}</text>
|
|
184
|
+
|
|
185
|
+
{/* Shortcut hints */}
|
|
186
|
+
<text fg={colors.textDim}>v=view detail d=remove u=update Tab=switch pane</text>
|
|
187
|
+
<text fg={colors.border}>---</text>
|
|
188
|
+
|
|
189
|
+
{/* SKILL.md content */}
|
|
190
|
+
{content ? (
|
|
191
|
+
content.split("\n").map((line, i) => {
|
|
192
|
+
if (line.startsWith("### ")) {
|
|
193
|
+
return <text key={i} fg={colors.primary}>{line}</text>
|
|
194
|
+
}
|
|
195
|
+
if (line.startsWith("## ")) {
|
|
196
|
+
return <text key={i} fg={colors.primary}><strong>{line}</strong></text>
|
|
197
|
+
}
|
|
198
|
+
if (line.startsWith("# ")) {
|
|
199
|
+
return <text key={i} fg={colors.primary}><strong>{line}</strong></text>
|
|
200
|
+
}
|
|
201
|
+
if (line.startsWith("```")) {
|
|
202
|
+
return <text key={i} fg={colors.textDim}>{line}</text>
|
|
203
|
+
}
|
|
204
|
+
if (line.trimStart().startsWith("- ") || line.trimStart().startsWith("* ")) {
|
|
205
|
+
return <text key={i} fg={colors.text}>{line}</text>
|
|
206
|
+
}
|
|
207
|
+
if (!line.trim()) {
|
|
208
|
+
return <text key={i}>{" "}</text>
|
|
209
|
+
}
|
|
210
|
+
return <text key={i} fg={colors.text}>{line}</text>
|
|
211
|
+
})
|
|
212
|
+
) : (
|
|
213
|
+
<text fg={colors.textDim}>(No skill content available)</text>
|
|
214
|
+
)}
|
|
215
|
+
</box>
|
|
216
|
+
</scrollbox>
|
|
217
|
+
)
|
|
218
|
+
}
|