@skillsgate/tui 0.1.12 → 0.1.13
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/package.json +1 -8
- package/src/components/help-overlay.tsx +2 -4
- package/src/components/layout.tsx +8 -34
- package/src/components/status-bar.tsx +4 -7
- package/src/data/api-client.ts +78 -109
- package/src/data/use-favorites.ts +18 -4
- package/src/data/use-search.ts +14 -31
- package/src/data/use-skill-actions.ts +68 -16
- package/src/views/discover.tsx +33 -126
- package/src/views/favorites.tsx +10 -354
- package/src/views/settings.tsx +0 -6
- package/src/views/skill-detail.tsx +15 -14
- package/tmp.json +0 -0
package/src/views/favorites.tsx
CHANGED
|
@@ -1,363 +1,19 @@
|
|
|
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
1
|
import { colors } from "../utils/colors.js"
|
|
8
|
-
import type { CatalogSkill } from "../data/api-client.js"
|
|
9
|
-
import type { EnrichedSkill } from "../store/types.js"
|
|
10
2
|
|
|
11
3
|
/**
|
|
12
|
-
* Favorites view:
|
|
13
|
-
*
|
|
14
|
-
* RIGHT - Selected favorite detail (flexGrow)
|
|
15
|
-
*
|
|
16
|
-
* Requires authentication. Shows a prompt to login if not authenticated.
|
|
4
|
+
* Favorites view: Coming soon placeholder.
|
|
5
|
+
* Favorites require authentication which is not yet available in the public TUI.
|
|
17
6
|
*/
|
|
18
7
|
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
8
|
return (
|
|
145
|
-
<box style={{ flexDirection: "column",
|
|
146
|
-
{
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
border: true,
|
|
168
|
-
borderColor: colors.border,
|
|
169
|
-
flexDirection: "column",
|
|
170
|
-
} as any}
|
|
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>
|
|
9
|
+
<box style={{ flexDirection: "column", padding: 2 }}>
|
|
10
|
+
<text fg={colors.primary}>
|
|
11
|
+
<strong>Favorites</strong>
|
|
12
|
+
</text>
|
|
13
|
+
<text>{" "}</text>
|
|
14
|
+
<text fg={colors.text}>
|
|
15
|
+
Coming soon. Favorites will be available once accounts are launched.
|
|
16
|
+
</text>
|
|
240
17
|
</box>
|
|
241
18
|
)
|
|
242
19
|
}
|
|
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
|
-
canonicalPath: "",
|
|
340
|
-
agents: [],
|
|
341
|
-
scope: "custom",
|
|
342
|
-
projectName: null,
|
|
343
|
-
hasSupportingFiles: false,
|
|
344
|
-
supportingFiles: [],
|
|
345
|
-
metadata: {
|
|
346
|
-
categories: skill.categories,
|
|
347
|
-
capabilities: skill.capabilities,
|
|
348
|
-
keywords: skill.keywords,
|
|
349
|
-
githubUrl: skill.githubUrl,
|
|
350
|
-
installCommand: skill.installCommand,
|
|
351
|
-
},
|
|
352
|
-
lock: skill.githubUrl
|
|
353
|
-
? {
|
|
354
|
-
source: skill.githubUrl,
|
|
355
|
-
sourceType: "github" as const,
|
|
356
|
-
originalUrl: skill.githubUrl,
|
|
357
|
-
skillFolderHash: "",
|
|
358
|
-
installedAt: "",
|
|
359
|
-
updatedAt: "",
|
|
360
|
-
}
|
|
361
|
-
: undefined,
|
|
362
|
-
}
|
|
363
|
-
}
|
package/src/views/settings.tsx
CHANGED
|
@@ -34,12 +34,6 @@ const SETTING_DEFS: SettingDef[] = [
|
|
|
34
34
|
options: ["dark", "light", "system"],
|
|
35
35
|
defaultValue: "dark",
|
|
36
36
|
},
|
|
37
|
-
{
|
|
38
|
-
key: "search.preferSemantic",
|
|
39
|
-
label: "Prefer semantic search",
|
|
40
|
-
type: "toggle",
|
|
41
|
-
defaultValue: true,
|
|
42
|
-
},
|
|
43
37
|
{
|
|
44
38
|
key: "telemetry.enabled",
|
|
45
39
|
label: "Anonymous telemetry",
|
|
@@ -6,6 +6,7 @@ import { useKeyboard } from "@opentui/react"
|
|
|
6
6
|
import { useStore, useDispatch } from "../store/context.js"
|
|
7
7
|
import { useSkillActions } from "../data/use-skill-actions.js"
|
|
8
8
|
import { ConfirmDialog } from "../components/confirm-dialog.js"
|
|
9
|
+
import { fetchSkillContent } from "../data/api-client.js"
|
|
9
10
|
import { colors, agentBadges as badgeMap } from "../utils/colors.js"
|
|
10
11
|
import { agents } from "../../../cli/src/core/agents.js"
|
|
11
12
|
|
|
@@ -75,19 +76,15 @@ export function SkillDetailView() {
|
|
|
75
76
|
return
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
// Catalog skill: fetch content from
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
79
|
+
// Catalog skill: fetch SKILL.md content from GitHub
|
|
80
|
+
const source = skill.metadata?.source as string | undefined
|
|
81
|
+
const skillId = skill.metadata?.skillId as string | undefined
|
|
82
|
+
if (source && skillId) {
|
|
82
83
|
setContentLoading(true)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
.then(res => res.ok ? res.json() : null)
|
|
88
|
-
.then((data: any) => {
|
|
89
|
-
if (data?.content) {
|
|
90
|
-
setContent(stripFrontmatter(data.content))
|
|
84
|
+
fetchSkillContent(source, skillId)
|
|
85
|
+
.then((md) => {
|
|
86
|
+
if (md) {
|
|
87
|
+
setContent(stripFrontmatter(md))
|
|
91
88
|
} else {
|
|
92
89
|
setContent(skill.description || "(No content available)")
|
|
93
90
|
}
|
|
@@ -424,8 +421,12 @@ export function SkillDetailView() {
|
|
|
424
421
|
<text>{" "}</text>
|
|
425
422
|
|
|
426
423
|
{/* Description */}
|
|
427
|
-
|
|
428
|
-
|
|
424
|
+
{skill.description ? (
|
|
425
|
+
<>
|
|
426
|
+
<text fg={colors.text}>{skill.description}</text>
|
|
427
|
+
<text>{" "}</text>
|
|
428
|
+
</>
|
|
429
|
+
) : null}
|
|
429
430
|
|
|
430
431
|
{/* Source */}
|
|
431
432
|
<text fg={colors.textDim}>Source</text>
|
package/tmp.json
ADDED
|
File without changes
|