@skillsgate/tui 0.1.9 → 0.1.12
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 +32 -19
- package/package.json +6 -6
- package/src/components/agent-filter.tsx +2 -2
- package/src/components/help-overlay.tsx +2 -0
- package/src/components/layout.tsx +10 -9
- package/src/data/use-installed-skills.ts +178 -3
- package/src/data/use-skill-actions.ts +19 -2
- package/src/db/skills.ts +24 -0
- package/src/db/ssh.ts +45 -1
- package/src/store/types.ts +8 -0
- package/src/types/bun-sqlite.d.ts +12 -0
- package/src/views/add-server.tsx +3 -3
- package/src/views/discover.tsx +10 -5
- package/src/views/favorites.tsx +7 -2
- package/src/views/home.tsx +531 -9
- package/src/views/login.tsx +1 -1
- package/src/views/server-skills.tsx +61 -4
- package/src/views/servers.tsx +2 -2
- package/src/views/skill-detail.tsx +3 -3
package/src/views/favorites.tsx
CHANGED
|
@@ -164,10 +164,10 @@ export function FavoritesView() {
|
|
|
164
164
|
<box
|
|
165
165
|
style={{
|
|
166
166
|
width: "40%",
|
|
167
|
-
|
|
167
|
+
border: true,
|
|
168
168
|
borderColor: colors.border,
|
|
169
169
|
flexDirection: "column",
|
|
170
|
-
}}
|
|
170
|
+
} as any}
|
|
171
171
|
>
|
|
172
172
|
{/* List header */}
|
|
173
173
|
<box style={{ height: 1, paddingLeft: 1, backgroundColor: colors.bgAlt }}>
|
|
@@ -336,7 +336,12 @@ function catalogSkillToEnriched(
|
|
|
336
336
|
name: skill.name,
|
|
337
337
|
description: skill.summary || skill.description || "",
|
|
338
338
|
filePath: "",
|
|
339
|
+
canonicalPath: "",
|
|
339
340
|
agents: [],
|
|
341
|
+
scope: "custom",
|
|
342
|
+
projectName: null,
|
|
343
|
+
hasSupportingFiles: false,
|
|
344
|
+
supportingFiles: [],
|
|
340
345
|
metadata: {
|
|
341
346
|
categories: skill.categories,
|
|
342
347
|
capabilities: skill.capabilities,
|
package/src/views/home.tsx
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { useMemo, useState, useEffect } from "react"
|
|
2
|
+
import fsPromises from "node:fs/promises"
|
|
3
|
+
import os from "node:os"
|
|
4
|
+
import path from "node:path"
|
|
2
5
|
import fs from "node:fs"
|
|
3
|
-
import {
|
|
6
|
+
import { useKeyboard } from "@opentui/react"
|
|
7
|
+
import { useStore, useDispatch } from "../store/context.js"
|
|
8
|
+
import { useDb } from "../db/context.js"
|
|
4
9
|
import { AgentFilter } from "../components/agent-filter.js"
|
|
5
10
|
import { SkillList } from "../components/skill-list.js"
|
|
6
11
|
import { colors, agentBadges as badgeMap } from "../utils/colors.js"
|
|
7
12
|
import type { EnrichedSkill } from "../store/types.js"
|
|
13
|
+
import { agents } from "../../../cli/src/core/agents.js"
|
|
14
|
+
import { addSkillToLock } from "../../../cli/src/core/skill-lock.js"
|
|
15
|
+
import { sanitizeName, installSkillForAgent } from "../../../cli/src/core/installer.js"
|
|
16
|
+
|
|
17
|
+
const home = os.homedir()
|
|
18
|
+
const CANONICAL_SKILLS_DIR = path.join(home, ".agents", "skills")
|
|
8
19
|
|
|
9
20
|
/**
|
|
10
21
|
* Reads the full SKILL.md content for inline display.
|
|
@@ -44,11 +55,24 @@ function stripFrontmatter(content: string): string {
|
|
|
44
55
|
*/
|
|
45
56
|
export function HomeView() {
|
|
46
57
|
const state = useStore()
|
|
58
|
+
const dispatch = useDispatch()
|
|
59
|
+
const { settings } = useDb()
|
|
60
|
+
const [collectionsVersion, setCollectionsVersion] = useState(0)
|
|
61
|
+
const [showCollections, setShowCollections] = useState(false)
|
|
62
|
+
const [showCreateSkill, setShowCreateSkill] = useState(false)
|
|
47
63
|
|
|
48
64
|
// Apply agent filter and text filter
|
|
65
|
+
const collections = settings.get<Record<string, string[]>>("collections.skills", {})
|
|
66
|
+
const selectedCollection = settings.get<string | null>("ui.home.selectedCollection", null)
|
|
67
|
+
|
|
49
68
|
const filteredSkills = useMemo(() => {
|
|
50
69
|
let skills = state.installedSkills
|
|
51
70
|
|
|
71
|
+
if (selectedCollection) {
|
|
72
|
+
const ids = new Set(collections[selectedCollection] || [])
|
|
73
|
+
skills = skills.filter((skill) => ids.has(skill.canonicalPath))
|
|
74
|
+
}
|
|
75
|
+
|
|
52
76
|
// Agent filter
|
|
53
77
|
if (state.selectedAgentFilter !== "all") {
|
|
54
78
|
skills = skills.filter((s) =>
|
|
@@ -62,12 +86,98 @@ export function HomeView() {
|
|
|
62
86
|
skills = skills.filter(
|
|
63
87
|
(s) =>
|
|
64
88
|
s.name.toLowerCase().includes(q) ||
|
|
65
|
-
s.description.toLowerCase().includes(q)
|
|
89
|
+
s.description.toLowerCase().includes(q) ||
|
|
90
|
+
s.filePath.toLowerCase().includes(q) ||
|
|
91
|
+
s.canonicalPath.toLowerCase().includes(q) ||
|
|
92
|
+
(s.projectName?.toLowerCase().includes(q) ?? false) ||
|
|
93
|
+
(s.lock?.originalUrl?.toLowerCase().includes(q) ?? false) ||
|
|
94
|
+
s.supportingFiles.some((file) => file.relativePath.toLowerCase().includes(q))
|
|
66
95
|
)
|
|
67
96
|
}
|
|
68
97
|
|
|
69
98
|
return skills
|
|
70
|
-
}, [state.installedSkills, state.selectedAgentFilter, state.installedFilter])
|
|
99
|
+
}, [state.installedSkills, state.selectedAgentFilter, state.installedFilter, selectedCollection, collections, collectionsVersion])
|
|
100
|
+
|
|
101
|
+
useKeyboard((key) => {
|
|
102
|
+
if (state.activeView !== "home") return
|
|
103
|
+
if (state.showHelp) return
|
|
104
|
+
if (state.focusedPane === "search") return
|
|
105
|
+
if (showCollections || showCreateSkill) return
|
|
106
|
+
|
|
107
|
+
if (key.name === "c") {
|
|
108
|
+
setShowCollections(true)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (key.name === "n") {
|
|
113
|
+
setShowCreateSkill(true)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
async function createLocalSkill(data: {
|
|
119
|
+
name: string
|
|
120
|
+
description: string
|
|
121
|
+
content: string
|
|
122
|
+
targets: string[]
|
|
123
|
+
}) {
|
|
124
|
+
const name = data.name.trim()
|
|
125
|
+
if (!name) return
|
|
126
|
+
const description = data.description.trim() || name
|
|
127
|
+
const safeName = sanitizeName(name)
|
|
128
|
+
const canonicalDir = path.join(CANONICAL_SKILLS_DIR, safeName)
|
|
129
|
+
const filePath = path.join(canonicalDir, "SKILL.md")
|
|
130
|
+
|
|
131
|
+
await fsPromises.mkdir(canonicalDir, { recursive: true })
|
|
132
|
+
const body = (data.content.trim() || `---
|
|
133
|
+
name: ${safeName}
|
|
134
|
+
description: ${description}
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
# ${name}
|
|
138
|
+
|
|
139
|
+
## Instructions
|
|
140
|
+
|
|
141
|
+
Add your skill instructions here.
|
|
142
|
+
`).trimEnd() + "\n"
|
|
143
|
+
await fsPromises.writeFile(filePath, body, "utf-8")
|
|
144
|
+
|
|
145
|
+
const selectedTargets =
|
|
146
|
+
data.targets.length > 0
|
|
147
|
+
? data.targets
|
|
148
|
+
: settings.get<string[]>("install.defaultAgents", [])
|
|
149
|
+
const mirrorAgents = settings.get<string[]>("sync.mirrorAgents", [])
|
|
150
|
+
const targetNames = Array.from(new Set([...selectedTargets, ...mirrorAgents]))
|
|
151
|
+
for (const targetName of targetNames) {
|
|
152
|
+
const agent = agents[targetName]
|
|
153
|
+
if (agent) {
|
|
154
|
+
await installSkillForAgent(
|
|
155
|
+
{
|
|
156
|
+
name,
|
|
157
|
+
path: canonicalDir,
|
|
158
|
+
entryPath: filePath,
|
|
159
|
+
} as any,
|
|
160
|
+
agent,
|
|
161
|
+
"global",
|
|
162
|
+
"symlink",
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const now = new Date().toISOString()
|
|
168
|
+
await addSkillToLock(safeName, {
|
|
169
|
+
source: canonicalDir,
|
|
170
|
+
sourceType: "local",
|
|
171
|
+
originalUrl: canonicalDir,
|
|
172
|
+
skillFolderHash: "",
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
dispatch({ type: "REFRESH_SKILLS" })
|
|
176
|
+
dispatch({
|
|
177
|
+
type: "SHOW_NOTIFICATION",
|
|
178
|
+
notification: { type: "success", message: `Created "${name}"` },
|
|
179
|
+
})
|
|
180
|
+
}
|
|
71
181
|
|
|
72
182
|
return (
|
|
73
183
|
<box style={{ flexDirection: "row", width: "100%", flexGrow: 1 }}>
|
|
@@ -78,10 +188,10 @@ export function HomeView() {
|
|
|
78
188
|
<box
|
|
79
189
|
style={{
|
|
80
190
|
width: "30%",
|
|
81
|
-
|
|
191
|
+
border: true,
|
|
82
192
|
borderColor: state.focusedPane === "list" ? colors.primary : colors.border,
|
|
83
193
|
flexDirection: "column",
|
|
84
|
-
}}
|
|
194
|
+
} as any}
|
|
85
195
|
>
|
|
86
196
|
<SkillList skills={filteredSkills} />
|
|
87
197
|
</box>
|
|
@@ -89,7 +199,11 @@ export function HomeView() {
|
|
|
89
199
|
{/* RIGHT: Detail panel */}
|
|
90
200
|
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
91
201
|
{state.selectedSkill ? (
|
|
92
|
-
<DetailPanel
|
|
202
|
+
<DetailPanel
|
|
203
|
+
skill={state.selectedSkill}
|
|
204
|
+
collections={collections}
|
|
205
|
+
selectedCollection={selectedCollection}
|
|
206
|
+
/>
|
|
93
207
|
) : (
|
|
94
208
|
<box style={{ padding: 1 }}>
|
|
95
209
|
<text fg={colors.textDim}>
|
|
@@ -100,6 +214,35 @@ export function HomeView() {
|
|
|
100
214
|
</box>
|
|
101
215
|
)}
|
|
102
216
|
</box>
|
|
217
|
+
|
|
218
|
+
{showCollections ? (
|
|
219
|
+
<CollectionOverlay
|
|
220
|
+
collections={collections}
|
|
221
|
+
selectedCollection={selectedCollection}
|
|
222
|
+
selectedSkill={state.selectedSkill}
|
|
223
|
+
onClose={() => setShowCollections(false)}
|
|
224
|
+
onApplyFilter={(name) => {
|
|
225
|
+
settings.set("ui.home.selectedCollection", name)
|
|
226
|
+
setCollectionsVersion((value) => value + 1)
|
|
227
|
+
}}
|
|
228
|
+
onSaveCollections={(next) => {
|
|
229
|
+
settings.set("collections.skills", next)
|
|
230
|
+
setCollectionsVersion((value) => value + 1)
|
|
231
|
+
}}
|
|
232
|
+
/>
|
|
233
|
+
) : null}
|
|
234
|
+
|
|
235
|
+
{showCreateSkill ? (
|
|
236
|
+
<CreateSkillOverlay
|
|
237
|
+
agents={state.detectedAgents}
|
|
238
|
+
defaultTargets={settings.get<string[]>("install.defaultAgents", [])}
|
|
239
|
+
onClose={() => setShowCreateSkill(false)}
|
|
240
|
+
onCreate={async (data) => {
|
|
241
|
+
await createLocalSkill(data)
|
|
242
|
+
setShowCreateSkill(false)
|
|
243
|
+
}}
|
|
244
|
+
/>
|
|
245
|
+
) : null}
|
|
103
246
|
</box>
|
|
104
247
|
)
|
|
105
248
|
}
|
|
@@ -108,10 +251,11 @@ export function HomeView() {
|
|
|
108
251
|
|
|
109
252
|
interface DetailPanelProps {
|
|
110
253
|
skill: EnrichedSkill
|
|
254
|
+
collections: Record<string, string[]>
|
|
255
|
+
selectedCollection: string | null
|
|
111
256
|
}
|
|
112
257
|
|
|
113
|
-
function DetailPanel({ skill }: DetailPanelProps) {
|
|
114
|
-
const state = useStore()
|
|
258
|
+
function DetailPanel({ skill, collections, selectedCollection }: DetailPanelProps) {
|
|
115
259
|
const [content, setContent] = useState("")
|
|
116
260
|
|
|
117
261
|
useEffect(() => {
|
|
@@ -180,10 +324,66 @@ function DetailPanel({ skill }: DetailPanelProps) {
|
|
|
180
324
|
</box>
|
|
181
325
|
) : null}
|
|
182
326
|
|
|
327
|
+
<box style={{ flexDirection: "row", minHeight: 1 }}>
|
|
328
|
+
<text fg={colors.textDim}>Scope: </text>
|
|
329
|
+
<text fg={colors.secondary}>{skill.scope}</text>
|
|
330
|
+
{skill.projectName ? (
|
|
331
|
+
<>
|
|
332
|
+
<text fg={colors.textDim}> Project: </text>
|
|
333
|
+
<text fg={colors.secondary}>{skill.projectName}</text>
|
|
334
|
+
</>
|
|
335
|
+
) : null}
|
|
336
|
+
</box>
|
|
337
|
+
|
|
338
|
+
<box style={{ flexDirection: "column" }}>
|
|
339
|
+
<text fg={colors.textDim}>Path: </text>
|
|
340
|
+
<text fg={colors.secondary}>{skill.canonicalPath}</text>
|
|
341
|
+
</box>
|
|
342
|
+
|
|
343
|
+
<box style={{ flexDirection: "column" }}>
|
|
344
|
+
<text fg={colors.textDim}>Collections:</text>
|
|
345
|
+
{Object.keys(collections).length === 0 ? (
|
|
346
|
+
<text fg={colors.textDim}> none</text>
|
|
347
|
+
) : (
|
|
348
|
+
Object.entries(collections).map(([name, items]) => (
|
|
349
|
+
<text
|
|
350
|
+
key={name}
|
|
351
|
+
fg={
|
|
352
|
+
items.includes(skill.canonicalPath)
|
|
353
|
+
? colors.primary
|
|
354
|
+
: selectedCollection === name
|
|
355
|
+
? colors.warning
|
|
356
|
+
: colors.textDim
|
|
357
|
+
}
|
|
358
|
+
>
|
|
359
|
+
{" "}{items.includes(skill.canonicalPath) ? "[x]" : "[ ]"} {name}
|
|
360
|
+
</text>
|
|
361
|
+
))
|
|
362
|
+
)}
|
|
363
|
+
</box>
|
|
364
|
+
|
|
365
|
+
{skill.supportingFiles.length > 0 ? (
|
|
366
|
+
<box style={{ flexDirection: "column" }}>
|
|
367
|
+
<text fg={colors.textDim}>
|
|
368
|
+
Supporting files ({skill.supportingFiles.length}):
|
|
369
|
+
</text>
|
|
370
|
+
{skill.supportingFiles.slice(0, 10).map((file) => (
|
|
371
|
+
<text key={file.relativePath} fg={colors.secondary}>
|
|
372
|
+
{" "}{file.relativePath}
|
|
373
|
+
</text>
|
|
374
|
+
))}
|
|
375
|
+
{skill.supportingFiles.length > 10 ? (
|
|
376
|
+
<text fg={colors.textDim}>
|
|
377
|
+
{" "}+{skill.supportingFiles.length - 10} more
|
|
378
|
+
</text>
|
|
379
|
+
) : null}
|
|
380
|
+
</box>
|
|
381
|
+
) : null}
|
|
382
|
+
|
|
183
383
|
<text>{" "}</text>
|
|
184
384
|
|
|
185
385
|
{/* Shortcut hints */}
|
|
186
|
-
<text fg={colors.textDim}>v=view detail d=remove u=update Tab=switch pane</text>
|
|
386
|
+
<text fg={colors.textDim}>v=view detail d=remove u=update n=create skill c=collections Tab=switch pane</text>
|
|
187
387
|
<text fg={colors.border}>---</text>
|
|
188
388
|
|
|
189
389
|
{/* SKILL.md content */}
|
|
@@ -216,3 +416,325 @@ function DetailPanel({ skill }: DetailPanelProps) {
|
|
|
216
416
|
</scrollbox>
|
|
217
417
|
)
|
|
218
418
|
}
|
|
419
|
+
|
|
420
|
+
function CollectionOverlay({
|
|
421
|
+
collections,
|
|
422
|
+
selectedCollection,
|
|
423
|
+
selectedSkill,
|
|
424
|
+
onClose,
|
|
425
|
+
onApplyFilter,
|
|
426
|
+
onSaveCollections,
|
|
427
|
+
}: {
|
|
428
|
+
collections: Record<string, string[]>
|
|
429
|
+
selectedCollection: string | null
|
|
430
|
+
selectedSkill: EnrichedSkill | null
|
|
431
|
+
onClose: () => void
|
|
432
|
+
onApplyFilter: (name: string | null) => void
|
|
433
|
+
onSaveCollections: (next: Record<string, string[]>) => void
|
|
434
|
+
}) {
|
|
435
|
+
const dispatch = useDispatch()
|
|
436
|
+
const entries = [{ name: "(all)", count: 0 }, ...Object.keys(collections).sort().map((name) => ({
|
|
437
|
+
name,
|
|
438
|
+
count: collections[name]?.length ?? 0,
|
|
439
|
+
}))]
|
|
440
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
441
|
+
const [inputMode, setInputMode] = useState<"create" | "rename" | null>(null)
|
|
442
|
+
const [draftName, setDraftName] = useState("")
|
|
443
|
+
|
|
444
|
+
useKeyboard((key) => {
|
|
445
|
+
if (inputMode) return
|
|
446
|
+
if (key.name === "escape") {
|
|
447
|
+
onClose()
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
if (key.name === "up" || key.name === "k") {
|
|
451
|
+
setSelectedIndex((value) => Math.max(0, value - 1))
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
if (key.name === "down" || key.name === "j") {
|
|
455
|
+
setSelectedIndex((value) => Math.min(entries.length - 1, value + 1))
|
|
456
|
+
return
|
|
457
|
+
}
|
|
458
|
+
if (key.name === "return") {
|
|
459
|
+
const name = entries[selectedIndex]?.name
|
|
460
|
+
onApplyFilter(name === "(all)" ? null : name)
|
|
461
|
+
onClose()
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
if (key.name === "a") {
|
|
465
|
+
setDraftName("")
|
|
466
|
+
setInputMode("create")
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
if (key.name === "r" && selectedIndex > 0) {
|
|
470
|
+
setDraftName(entries[selectedIndex].name)
|
|
471
|
+
setInputMode("rename")
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
if (key.name === "d" && selectedIndex > 0) {
|
|
475
|
+
const next = { ...collections }
|
|
476
|
+
delete next[entries[selectedIndex].name]
|
|
477
|
+
onSaveCollections(next)
|
|
478
|
+
setSelectedIndex((value) => Math.max(0, Math.min(value - 1, entries.length - 2)))
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
if ((key.name === "space" || key.name === "f") && selectedIndex > 0 && selectedSkill) {
|
|
482
|
+
const name = entries[selectedIndex].name
|
|
483
|
+
const next = { ...collections }
|
|
484
|
+
const current = new Set(next[name] || [])
|
|
485
|
+
if (current.has(selectedSkill.canonicalPath)) current.delete(selectedSkill.canonicalPath)
|
|
486
|
+
else current.add(selectedSkill.canonicalPath)
|
|
487
|
+
next[name] = Array.from(current).sort()
|
|
488
|
+
onSaveCollections(next)
|
|
489
|
+
dispatch({
|
|
490
|
+
type: "SHOW_NOTIFICATION",
|
|
491
|
+
notification: { type: "info", message: `Updated ${name}` },
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
return (
|
|
497
|
+
<box
|
|
498
|
+
style={{
|
|
499
|
+
position: "absolute",
|
|
500
|
+
width: "100%",
|
|
501
|
+
height: "100%",
|
|
502
|
+
justifyContent: "center",
|
|
503
|
+
alignItems: "center",
|
|
504
|
+
backgroundColor: colors.bg,
|
|
505
|
+
}}
|
|
506
|
+
>
|
|
507
|
+
<box
|
|
508
|
+
style={{
|
|
509
|
+
width: 64,
|
|
510
|
+
border: true,
|
|
511
|
+
borderColor: colors.primary,
|
|
512
|
+
backgroundColor: "#1a1a2e",
|
|
513
|
+
flexDirection: "column",
|
|
514
|
+
paddingLeft: 1,
|
|
515
|
+
paddingRight: 1,
|
|
516
|
+
paddingTop: 1,
|
|
517
|
+
paddingBottom: 1,
|
|
518
|
+
}}
|
|
519
|
+
title="Collections"
|
|
520
|
+
>
|
|
521
|
+
{inputMode ? (
|
|
522
|
+
<>
|
|
523
|
+
<text fg={colors.text}>
|
|
524
|
+
{inputMode === "create" ? "New collection name" : "Rename collection"}
|
|
525
|
+
</text>
|
|
526
|
+
<box
|
|
527
|
+
style={{
|
|
528
|
+
height: 3,
|
|
529
|
+
width: "100%",
|
|
530
|
+
border: true,
|
|
531
|
+
borderColor: colors.primary,
|
|
532
|
+
paddingLeft: 1,
|
|
533
|
+
paddingRight: 1,
|
|
534
|
+
}}
|
|
535
|
+
>
|
|
536
|
+
<input
|
|
537
|
+
placeholder="collection name"
|
|
538
|
+
focused={true}
|
|
539
|
+
onInput={(value: string) => setDraftName(value)}
|
|
540
|
+
onSubmit={((value: string) => {
|
|
541
|
+
const trimmed = value.trim()
|
|
542
|
+
if (!trimmed) {
|
|
543
|
+
setInputMode(null)
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
const next = { ...collections }
|
|
547
|
+
if (inputMode === "create") {
|
|
548
|
+
next[trimmed] = next[trimmed] || []
|
|
549
|
+
} else {
|
|
550
|
+
const currentName = entries[selectedIndex].name
|
|
551
|
+
next[trimmed] = next[currentName] || []
|
|
552
|
+
delete next[currentName]
|
|
553
|
+
}
|
|
554
|
+
onSaveCollections(next)
|
|
555
|
+
setInputMode(null)
|
|
556
|
+
}) as any}
|
|
557
|
+
/>
|
|
558
|
+
</box>
|
|
559
|
+
<text fg={colors.textDim}>Enter=save Esc close overlay</text>
|
|
560
|
+
</>
|
|
561
|
+
) : (
|
|
562
|
+
<>
|
|
563
|
+
{entries.map((entry, index) => (
|
|
564
|
+
<box
|
|
565
|
+
key={entry.name}
|
|
566
|
+
style={{
|
|
567
|
+
width: "100%",
|
|
568
|
+
flexDirection: "row",
|
|
569
|
+
backgroundColor: index === selectedIndex ? colors.bgAlt : "transparent",
|
|
570
|
+
}}
|
|
571
|
+
>
|
|
572
|
+
<text
|
|
573
|
+
fg={
|
|
574
|
+
entry.name === "(all)"
|
|
575
|
+
? selectedCollection === null && index === selectedIndex
|
|
576
|
+
? colors.primary
|
|
577
|
+
: colors.text
|
|
578
|
+
: selectedCollection === entry.name
|
|
579
|
+
? colors.warning
|
|
580
|
+
: colors.text
|
|
581
|
+
}
|
|
582
|
+
style={{ flexGrow: 1 }}
|
|
583
|
+
>
|
|
584
|
+
{entry.name}
|
|
585
|
+
</text>
|
|
586
|
+
{entry.name !== "(all)" ? (
|
|
587
|
+
<text fg={colors.textDim}>{entry.count}</text>
|
|
588
|
+
) : null}
|
|
589
|
+
</box>
|
|
590
|
+
))}
|
|
591
|
+
<text>{" "}</text>
|
|
592
|
+
<text fg={colors.textDim}>
|
|
593
|
+
Enter=filter a=new r=rename d=delete f=toggle selected skill Esc=close
|
|
594
|
+
</text>
|
|
595
|
+
</>
|
|
596
|
+
)}
|
|
597
|
+
</box>
|
|
598
|
+
</box>
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function CreateSkillOverlay({
|
|
603
|
+
agents,
|
|
604
|
+
defaultTargets,
|
|
605
|
+
onClose,
|
|
606
|
+
onCreate,
|
|
607
|
+
}: {
|
|
608
|
+
agents: Array<{ name: string; displayName: string }>
|
|
609
|
+
defaultTargets: string[]
|
|
610
|
+
onClose: () => void
|
|
611
|
+
onCreate: (data: { name: string; description: string; content: string; targets: string[] }) => Promise<void>
|
|
612
|
+
}) {
|
|
613
|
+
const [name, setName] = useState("")
|
|
614
|
+
const [description, setDescription] = useState("")
|
|
615
|
+
const [content, setContent] = useState("")
|
|
616
|
+
const [targets, setTargets] = useState<string[]>(defaultTargets)
|
|
617
|
+
const [focusedField, setFocusedField] = useState<0 | 1 | 2>(0)
|
|
618
|
+
const [saving, setSaving] = useState(false)
|
|
619
|
+
|
|
620
|
+
useKeyboard((key) => {
|
|
621
|
+
if (key.name === "escape" && !saving) {
|
|
622
|
+
onClose()
|
|
623
|
+
return
|
|
624
|
+
}
|
|
625
|
+
if (key.name === "tab" && !saving) {
|
|
626
|
+
setFocusedField((value) => (value === 0 ? 1 : value === 1 ? 2 : 0))
|
|
627
|
+
return
|
|
628
|
+
}
|
|
629
|
+
if (/^[1-9]$/.test(key.raw ?? "") && !saving) {
|
|
630
|
+
const idx = Number(key.raw) - 1
|
|
631
|
+
const target = agents[idx]
|
|
632
|
+
if (target) {
|
|
633
|
+
setTargets((prev) =>
|
|
634
|
+
prev.includes(target.name)
|
|
635
|
+
? prev.filter((value) => value !== target.name)
|
|
636
|
+
: [...prev, target.name],
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
return
|
|
640
|
+
}
|
|
641
|
+
if (key.name === "s" && key.ctrl && !saving && name.trim()) {
|
|
642
|
+
setSaving(true)
|
|
643
|
+
onCreate({ name: name.trim(), description: description.trim(), content, targets }).finally(() => {
|
|
644
|
+
setSaving(false)
|
|
645
|
+
})
|
|
646
|
+
}
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
return (
|
|
650
|
+
<box
|
|
651
|
+
style={{
|
|
652
|
+
position: "absolute",
|
|
653
|
+
width: "100%",
|
|
654
|
+
height: "100%",
|
|
655
|
+
justifyContent: "center",
|
|
656
|
+
alignItems: "center",
|
|
657
|
+
backgroundColor: colors.bg,
|
|
658
|
+
}}
|
|
659
|
+
>
|
|
660
|
+
<box
|
|
661
|
+
style={{
|
|
662
|
+
width: 72,
|
|
663
|
+
border: true,
|
|
664
|
+
borderColor: colors.primary,
|
|
665
|
+
backgroundColor: "#1a1a2e",
|
|
666
|
+
flexDirection: "column",
|
|
667
|
+
paddingLeft: 1,
|
|
668
|
+
paddingRight: 1,
|
|
669
|
+
paddingTop: 1,
|
|
670
|
+
paddingBottom: 1,
|
|
671
|
+
}}
|
|
672
|
+
title="Create Skill"
|
|
673
|
+
>
|
|
674
|
+
<text fg={colors.text}>Name</text>
|
|
675
|
+
<box
|
|
676
|
+
style={{
|
|
677
|
+
height: 3,
|
|
678
|
+
width: "100%",
|
|
679
|
+
border: true,
|
|
680
|
+
borderColor: focusedField === 0 ? colors.primary : colors.border,
|
|
681
|
+
paddingLeft: 1,
|
|
682
|
+
paddingRight: 1,
|
|
683
|
+
}}
|
|
684
|
+
>
|
|
685
|
+
<input
|
|
686
|
+
placeholder="skill name"
|
|
687
|
+
focused={focusedField === 0 && !saving}
|
|
688
|
+
onInput={(value: string) => setName(value)}
|
|
689
|
+
onSubmit={() => setFocusedField(1)}
|
|
690
|
+
/>
|
|
691
|
+
</box>
|
|
692
|
+
<text fg={colors.text}>Description</text>
|
|
693
|
+
<box
|
|
694
|
+
style={{
|
|
695
|
+
height: 3,
|
|
696
|
+
width: "100%",
|
|
697
|
+
border: true,
|
|
698
|
+
borderColor: focusedField === 1 ? colors.primary : colors.border,
|
|
699
|
+
paddingLeft: 1,
|
|
700
|
+
paddingRight: 1,
|
|
701
|
+
}}
|
|
702
|
+
>
|
|
703
|
+
<input
|
|
704
|
+
placeholder="short description"
|
|
705
|
+
focused={focusedField === 1 && !saving}
|
|
706
|
+
onInput={(value: string) => setDescription(value)}
|
|
707
|
+
onSubmit={() => setFocusedField(2)}
|
|
708
|
+
/>
|
|
709
|
+
</box>
|
|
710
|
+
<text fg={colors.text}>Content</text>
|
|
711
|
+
<box
|
|
712
|
+
style={{
|
|
713
|
+
height: 3,
|
|
714
|
+
width: "100%",
|
|
715
|
+
border: true,
|
|
716
|
+
borderColor: focusedField === 2 ? colors.primary : colors.border,
|
|
717
|
+
paddingLeft: 1,
|
|
718
|
+
paddingRight: 1,
|
|
719
|
+
}}
|
|
720
|
+
>
|
|
721
|
+
<input
|
|
722
|
+
placeholder="optional SKILL.md body or frontmatter"
|
|
723
|
+
focused={focusedField === 2 && !saving}
|
|
724
|
+
onInput={(value: string) => setContent(value)}
|
|
725
|
+
onSubmit={() => {}}
|
|
726
|
+
/>
|
|
727
|
+
</box>
|
|
728
|
+
<text>{" "}</text>
|
|
729
|
+
<text fg={colors.text}>Targets</text>
|
|
730
|
+
{agents.map((agent, index) => (
|
|
731
|
+
<text key={agent.name} fg={targets.includes(agent.name) ? colors.primary : colors.textDim}>
|
|
732
|
+
{index + 1}. {targets.includes(agent.name) ? "[x]" : "[ ]"} {agent.displayName}
|
|
733
|
+
</text>
|
|
734
|
+
))}
|
|
735
|
+
<text>{" "}</text>
|
|
736
|
+
<text fg={colors.textDim}>Tab=switch field 1-9 toggle targets Ctrl+S=create Esc=cancel</text>
|
|
737
|
+
</box>
|
|
738
|
+
</box>
|
|
739
|
+
)
|
|
740
|
+
}
|
package/src/views/login.tsx
CHANGED
|
@@ -174,7 +174,7 @@ export function LoginView() {
|
|
|
174
174
|
<input
|
|
175
175
|
placeholder="XXXX-XXXX"
|
|
176
176
|
focused={state.activeView === "login" && step === "code" && !state.showHelp}
|
|
177
|
-
onSubmit={handleCodeSubmit}
|
|
177
|
+
onSubmit={handleCodeSubmit as any}
|
|
178
178
|
/>
|
|
179
179
|
</box>
|
|
180
180
|
<text>{" "}</text>
|