dsh-skill-picker 0.2.0 → 0.2.2
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/README.md +3 -2
- package/lib/client.js +25875 -43
- package/lib/client.js.map +4 -4
- package/package.json +22 -5
- package/src/client/index.jsx +183 -46
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-picker",
|
|
3
3
|
"description": "DSH Web GUI skill picker: a button beside the composer opens a searchable list of installed skills; picking one inserts the official `/skill-name` gesture into the input box, so the skill loads with your message (WorkBuddy-style skill invocation for DeepSeek Harness).",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"build": "node build.mjs"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"esbuild": "^0.25.0"
|
|
16
|
+
"esbuild": "^0.25.0",
|
|
17
|
+
"fuzzysort": "^4.0.2"
|
|
17
18
|
},
|
|
18
19
|
"dsh": {
|
|
19
20
|
"bundle": {
|
|
@@ -25,16 +26,29 @@
|
|
|
25
26
|
"@deepseek-ai/dsh-client-locale"
|
|
26
27
|
],
|
|
27
28
|
"platform": "web"
|
|
29
|
+
},
|
|
30
|
+
"compatibility": {
|
|
31
|
+
"dshReleases": {
|
|
32
|
+
"0.1.1-rc.1": "unknown",
|
|
33
|
+
"0.1.1-rc.2": "compatible",
|
|
34
|
+
"0.1.2-alpha.2": "compatible",
|
|
35
|
+
"0.1.2-alpha.3": "compatible",
|
|
36
|
+
"0.1.2-alpha.4": "compatible",
|
|
37
|
+
"0.1.2-alpha.5": "compatible"
|
|
38
|
+
}
|
|
28
39
|
}
|
|
29
40
|
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22"
|
|
43
|
+
},
|
|
30
44
|
"peerDependencies": {
|
|
31
45
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
46
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
47
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
32
49
|
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
33
50
|
"@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
|
|
34
51
|
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
35
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
36
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
37
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
38
52
|
"react": "^18.2.0"
|
|
39
53
|
},
|
|
40
54
|
"files": [
|
|
@@ -47,5 +61,8 @@
|
|
|
47
61
|
"repository": {
|
|
48
62
|
"type": "git",
|
|
49
63
|
"url": "https://github.com/a735624258/dsh-skill-picker.git"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"pinyin-pro": "^3.29.3"
|
|
50
67
|
}
|
|
51
68
|
}
|
package/src/client/index.jsx
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
19
|
+
import fuzzysort from 'fuzzysort'
|
|
20
|
+
import { pinyin } from 'pinyin-pro'
|
|
19
21
|
|
|
20
22
|
/** Required services: slot registry, host connection (official skills API), sessions (workspace cwd fallback), input triggers (/ fuzzy source). */
|
|
21
23
|
export const inject = ['slots', 'connection', 'sessions', 'inputTriggers']
|
|
@@ -44,13 +46,64 @@ function saveUsage(usage) {
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Shared usage ordering — the single rule used by BOTH the ⚡ panel and the
|
|
51
|
+
* `/` completion: last picked first, then most frequent, then by name.
|
|
52
|
+
* A fresh copy is returned; the input array is untouched.
|
|
53
|
+
*/
|
|
54
|
+
function rankByUsage(skills, usage) {
|
|
55
|
+
return skills.slice().sort((a, b) => {
|
|
56
|
+
const ua = usage[a.name]
|
|
57
|
+
const ub = usage[b.name]
|
|
58
|
+
const la = ua?.lastUsed ?? 0
|
|
59
|
+
const lb = ub?.lastUsed ?? 0
|
|
60
|
+
if (la !== lb) return lb - la
|
|
61
|
+
const ca = ua?.count ?? 0
|
|
62
|
+
const cb = ub?.count ?? 0
|
|
63
|
+
if (ca !== cb) return cb - ca
|
|
64
|
+
return a.name.localeCompare(b.name)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pinyin search text for a skill name/description: spaced full pinyin
|
|
70
|
+
* (`ji yi`), joined (`jiyi`), and initials (`jy`) for the name, plus joined
|
|
71
|
+
* full pinyin and initials for the description — so either the `/` fuzzy
|
|
72
|
+
* completion or the ⚡ panel matches queries like `ji yi`, `jiyi`, or `jy`
|
|
73
|
+
* against 记忆/知识库/每日打卡-ish Chinese text. Cached per (name, desc)
|
|
74
|
+
* pair; never throws (falls back to '').
|
|
75
|
+
*/
|
|
76
|
+
const pinyinCache = new Map()
|
|
77
|
+
function skillPinyinText(name, description = '') {
|
|
78
|
+
const key = `${name}\u0000${description}`
|
|
79
|
+
const cached = pinyinCache.get(key)
|
|
80
|
+
if (cached !== undefined) return cached
|
|
81
|
+
let text = ''
|
|
82
|
+
try {
|
|
83
|
+
const base = { toneType: 'none', nonZh: 'consecutive' }
|
|
84
|
+
const nameSpaced = pinyin(name, base)
|
|
85
|
+
const nameJoined = pinyin(name, { ...base, separator: '' })
|
|
86
|
+
const nameFirstSpaced = pinyin(name, { ...base, pattern: 'first' })
|
|
87
|
+
const nameFirstJoined = pinyin(name, { ...base, pattern: 'first', separator: '' })
|
|
88
|
+
const descSpaced = pinyin(description, base)
|
|
89
|
+
const descJoined = pinyin(description, { ...base, separator: '' })
|
|
90
|
+
const descFirstSpaced = pinyin(description, { ...base, pattern: 'first' })
|
|
91
|
+
const descFirstJoined = pinyin(description, { ...base, pattern: 'first', separator: '' })
|
|
92
|
+
text = `${nameSpaced} ${nameJoined} ${nameFirstSpaced} ${nameFirstJoined} ${descSpaced} ${descJoined} ${descFirstSpaced} ${descFirstJoined}`
|
|
93
|
+
} catch {
|
|
94
|
+
text = ''
|
|
95
|
+
}
|
|
96
|
+
pinyinCache.set(key, text)
|
|
97
|
+
return text
|
|
98
|
+
}
|
|
99
|
+
|
|
47
100
|
/** Row height matches the resident chrome (access mode, plan, attach, model). */
|
|
48
101
|
const buttonStyle = {
|
|
49
102
|
display: 'inline-flex',
|
|
50
103
|
alignItems: 'center',
|
|
51
104
|
justifyContent: 'center',
|
|
52
|
-
width: '
|
|
53
|
-
height: '
|
|
105
|
+
width: '24px',
|
|
106
|
+
height: '24px',
|
|
54
107
|
margin: '0 2px',
|
|
55
108
|
padding: '0',
|
|
56
109
|
border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))',
|
|
@@ -138,7 +191,7 @@ const statusStyle = {
|
|
|
138
191
|
/** The picker's bolt glyph: DeepSeek palette gradient + slim stroke. */
|
|
139
192
|
function BoltIcon() {
|
|
140
193
|
return (
|
|
141
|
-
<svg viewBox="0 0 24 24" width="
|
|
194
|
+
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" style={{ display: 'block' }}>
|
|
142
195
|
<defs>
|
|
143
196
|
<linearGradient id="dsh-sp-bolt-grad" x1="0" y1="0" x2="0" y2="1">
|
|
144
197
|
<stop offset="0%" stopColor="var(--dsw-static-deepseek-400, rgb(103, 158, 254))" />
|
|
@@ -149,7 +202,7 @@ function BoltIcon() {
|
|
|
149
202
|
d="M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66.19-.34.05-.08.07-.12C8.48 10.94 10.42 7.54 13 3h1l-1 7h3.5c.49 0 .56.33.47.51l-.07.15C12.96 17.55 11 21 11 21z"
|
|
150
203
|
fill="url(#dsh-sp-bolt-grad)"
|
|
151
204
|
stroke="var(--dsw-static-deepseek-600, rgb(72, 104, 178))"
|
|
152
|
-
strokeWidth="
|
|
205
|
+
strokeWidth="1"
|
|
153
206
|
strokeLinejoin="round"
|
|
154
207
|
/>
|
|
155
208
|
</svg>
|
|
@@ -167,7 +220,9 @@ function SkillPickerButton(props) {
|
|
|
167
220
|
const [error, setError] = useState(undefined)
|
|
168
221
|
const [query, setQuery] = useState('')
|
|
169
222
|
const [usage, setUsage] = useState(() => loadUsage())
|
|
223
|
+
const [active, setActive] = useState(0)
|
|
170
224
|
const boxRef = useRef(null)
|
|
225
|
+
const itemRefs = useRef([])
|
|
171
226
|
|
|
172
227
|
const load = useCallback(async () => {
|
|
173
228
|
if (skills !== undefined || error !== undefined) return
|
|
@@ -243,27 +298,53 @@ function SkillPickerButton(props) {
|
|
|
243
298
|
return () => document.removeEventListener('mousedown', onDown)
|
|
244
299
|
}, [open])
|
|
245
300
|
|
|
246
|
-
// Usage ordering: last picked first, then most frequent, then by name
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const ub = usage[b.name]
|
|
250
|
-
const la = ua?.lastUsed ?? 0
|
|
251
|
-
const lb = ub?.lastUsed ?? 0
|
|
252
|
-
if (la !== lb) return lb - la
|
|
253
|
-
const ca = ua?.count ?? 0
|
|
254
|
-
const cb = ub?.count ?? 0
|
|
255
|
-
if (ca !== cb) return cb - ca
|
|
256
|
-
return a.name.localeCompare(b.name)
|
|
257
|
-
})
|
|
301
|
+
// Usage ordering: last picked first, then most frequent, then by name
|
|
302
|
+
// (shared with the `/` completion — one rule everywhere).
|
|
303
|
+
const ordered = rankByUsage(skills ?? [], usage)
|
|
258
304
|
|
|
259
305
|
const filtered = ordered
|
|
260
306
|
.filter((skill) => {
|
|
261
307
|
const q = query.trim().toLowerCase()
|
|
262
308
|
if (q === '') return true
|
|
263
|
-
return
|
|
309
|
+
return (
|
|
310
|
+
skill.name.toLowerCase().includes(q) ||
|
|
311
|
+
String(skill.description ?? '').toLowerCase().includes(q) ||
|
|
312
|
+
skillPinyinText(skill.name, skill.description ?? '').toLowerCase().includes(q)
|
|
313
|
+
)
|
|
264
314
|
})
|
|
265
315
|
.slice(0, 60)
|
|
266
316
|
|
|
317
|
+
// Keyboard navigation (#1): reset highlight when the query changes, keep it
|
|
318
|
+
// in range when the result list shrinks, and keep the highlighted row visible.
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
setActive(0)
|
|
321
|
+
}, [query])
|
|
322
|
+
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
setActive((cur) => Math.min(cur, Math.max(0, filtered.length - 1)))
|
|
325
|
+
}, [filtered.length])
|
|
326
|
+
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
itemRefs.current[active]?.scrollIntoView({ block: 'nearest' })
|
|
329
|
+
}, [active, filtered.length])
|
|
330
|
+
|
|
331
|
+
const onKeyDown = (event) => {
|
|
332
|
+
if (event.key === 'ArrowDown') {
|
|
333
|
+
event.preventDefault()
|
|
334
|
+
setActive((i) => Math.min(i + 1, filtered.length - 1))
|
|
335
|
+
} else if (event.key === 'ArrowUp') {
|
|
336
|
+
event.preventDefault()
|
|
337
|
+
setActive((i) => Math.max(i - 1, 0))
|
|
338
|
+
} else if (event.key === 'Enter') {
|
|
339
|
+
event.preventDefault()
|
|
340
|
+
const skill = filtered[active]
|
|
341
|
+
if (skill !== undefined) pick(skill.name)
|
|
342
|
+
} else if (event.key === 'Escape') {
|
|
343
|
+
event.preventDefault()
|
|
344
|
+
setOpen(false)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
267
348
|
return (
|
|
268
349
|
<div ref={boxRef} style={{ position: 'relative', display: 'inline-flex', flex: 'none' }}>
|
|
269
350
|
<button
|
|
@@ -283,7 +364,8 @@ function SkillPickerButton(props) {
|
|
|
283
364
|
<input
|
|
284
365
|
value={query}
|
|
285
366
|
onChange={(event) => setQuery(event.target.value)}
|
|
286
|
-
|
|
367
|
+
onKeyDown={onKeyDown}
|
|
368
|
+
placeholder="搜索技能…(↑↓ 选择,Enter 插入)"
|
|
287
369
|
style={searchStyle}
|
|
288
370
|
autoFocus
|
|
289
371
|
/>
|
|
@@ -296,18 +378,27 @@ function SkillPickerButton(props) {
|
|
|
296
378
|
{filtered.length === 0 ? (
|
|
297
379
|
<div style={statusStyle}>没有匹配的技能</div>
|
|
298
380
|
) : (
|
|
299
|
-
filtered.map((skill) => (
|
|
381
|
+
filtered.map((skill, index) => (
|
|
300
382
|
<button
|
|
301
383
|
key={skill.name}
|
|
302
384
|
type="button"
|
|
385
|
+
ref={(el) => {
|
|
386
|
+
itemRefs.current[index] = el
|
|
387
|
+
}}
|
|
303
388
|
onClick={() => pick(skill.name)}
|
|
304
389
|
onMouseEnter={(event) => {
|
|
390
|
+
setActive(index)
|
|
305
391
|
event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.12))'
|
|
306
392
|
}}
|
|
307
393
|
onMouseLeave={(event) => {
|
|
308
394
|
event.currentTarget.style.background = 'transparent'
|
|
309
395
|
}}
|
|
310
|
-
style={
|
|
396
|
+
style={{
|
|
397
|
+
...itemStyle,
|
|
398
|
+
...(index === active
|
|
399
|
+
? { background: 'var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.12))' }
|
|
400
|
+
: {}),
|
|
401
|
+
}}
|
|
311
402
|
>
|
|
312
403
|
<span style={nameStyle}>{`/${skill.name}`}</span>
|
|
313
404
|
<span style={descStyle}>{skill.description ?? ''}</span>
|
|
@@ -375,6 +466,25 @@ export function apply(ctx) {
|
|
|
375
466
|
// official ui-skill prefix source too — typing `/` matches name AND
|
|
376
467
|
// description anywhere, with usage ordering (same rule as the ⚡ panel).
|
|
377
468
|
ctx.effect(() => {
|
|
469
|
+
// Skills cache per session, so both candidates and the lexicon (chip
|
|
470
|
+
// decoration of `/skill-name` in the draft) have the same names.
|
|
471
|
+
const namesCache = new Map() // sessionId -> string[] (skill names)
|
|
472
|
+
const lexiconListeners = new Map() // sessionId -> Set<listener>
|
|
473
|
+
const notifyLexicon = (sessionId) => {
|
|
474
|
+
for (const fn of [...(lexiconListeners.get(sessionId) ?? [])]) {
|
|
475
|
+
try { fn() } catch (err) { console.error('[dsh-skill-picker] lexicon listener failed:', err) }
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const refreshNames = async (sessionId) => {
|
|
479
|
+
try {
|
|
480
|
+
const skills = await listSkills(sessionId)
|
|
481
|
+
namesCache.set(sessionId, (Array.isArray(skills) ? skills : []).map((s) => s.name))
|
|
482
|
+
notifyLexicon(sessionId)
|
|
483
|
+
} catch {
|
|
484
|
+
/* keep last cache; lexicon stays empty until a successful fetch */
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
378
488
|
const source = {
|
|
379
489
|
trigger: '/',
|
|
380
490
|
name: 'skill-fuzzy',
|
|
@@ -382,43 +492,70 @@ export function apply(ctx) {
|
|
|
382
492
|
async candidates(session, { query, signal }) {
|
|
383
493
|
const skills = await listSkills(session.sessionId)
|
|
384
494
|
if (signal.aborted) return []
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const ordered = skills.slice().sort((a, b) => {
|
|
389
|
-
const ua = usage[a.name]
|
|
390
|
-
const ub = usage[b.name]
|
|
391
|
-
const la = ua?.lastUsed ?? 0
|
|
392
|
-
const lb = ub?.lastUsed ?? 0
|
|
393
|
-
if (la !== lb) return lb - la
|
|
394
|
-
const ca = ua?.count ?? 0
|
|
395
|
-
const cb = ub?.count ?? 0
|
|
396
|
-
if (ca !== cb) return cb - ca
|
|
397
|
-
return a.name.localeCompare(b.name)
|
|
398
|
-
})
|
|
495
|
+
namesCache.set(session.sessionId, skills.map((s) => s.name))
|
|
496
|
+
notifyLexicon(session.sessionId)
|
|
497
|
+
const ordered = rankByUsage(skills, loadUsage())
|
|
399
498
|
const q = String(query ?? '').trim().toLowerCase()
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
499
|
+
if (q === '') {
|
|
500
|
+
// Empty `/` surfaces ALL skills, usage first (same rule as ⚡ panel).
|
|
501
|
+
return ordered.map((s) => ({ name: s.name, description: s.description }))
|
|
502
|
+
}
|
|
503
|
+
// Typing uses fuzzysort (subsequence matching + relevance score) over
|
|
504
|
+
// name AND description, so partial/gappy queries and keywords match.
|
|
505
|
+
// The search string also carries the skill's pinyin forms (spaced,
|
|
506
|
+
// joined, initials — name and description), so `ji yi` / `jiyi` / `jy`
|
|
507
|
+
// match Chinese skill names & descriptions.
|
|
508
|
+
const targets = ordered.map((s) => ({
|
|
509
|
+
s,
|
|
510
|
+
search: `${s.name} ${s.description ?? ''} ${skillPinyinText(s.name, s.description ?? '')}`,
|
|
511
|
+
}))
|
|
512
|
+
const results = fuzzysort.go(q, targets, {
|
|
513
|
+
key: 'search',
|
|
514
|
+
limit: 12,
|
|
515
|
+
threshold: -10000,
|
|
516
|
+
})
|
|
517
|
+
return results
|
|
518
|
+
.filter((r) => r.score > 0)
|
|
519
|
+
.map((r) => ({ name: r.obj.s.name, description: r.obj.s.description }))
|
|
411
520
|
},
|
|
412
521
|
warm(session) {
|
|
413
|
-
|
|
522
|
+
refreshNames(session.sessionId)
|
|
523
|
+
},
|
|
524
|
+
lexicon(session) {
|
|
525
|
+
return namesCache.get(session.sessionId)
|
|
526
|
+
},
|
|
527
|
+
subscribeLexicon(session, listener) {
|
|
528
|
+
const key = session.sessionId
|
|
529
|
+
const set = lexiconListeners.get(key) ?? new Set()
|
|
530
|
+
set.add(listener)
|
|
531
|
+
lexiconListeners.set(key, set)
|
|
532
|
+
return () => {
|
|
533
|
+
const cur = lexiconListeners.get(key)
|
|
534
|
+
if (cur !== undefined) {
|
|
535
|
+
cur.delete(listener)
|
|
536
|
+
if (cur.size === 0) lexiconListeners.delete(key)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
414
539
|
},
|
|
415
540
|
onPick({ candidate }) {
|
|
541
|
+
// Record usage (same rule as the ⚡ panel) so slash-picked skills
|
|
542
|
+
// rank higher the next time they open `/`.
|
|
543
|
+
try {
|
|
544
|
+
const usage = loadUsage()
|
|
545
|
+
const name = candidate.name
|
|
546
|
+
const next = { ...usage, [name]: { count: (usage[name]?.count ?? 0) + 1, lastUsed: Date.now() } }
|
|
547
|
+
saveUsage(next)
|
|
548
|
+
} catch {
|
|
549
|
+
/* usage recording is best-effort */
|
|
550
|
+
}
|
|
416
551
|
return { text: `/${candidate.name} ` }
|
|
417
552
|
},
|
|
418
553
|
}
|
|
419
554
|
const unregister = ctx.inputTriggers.registerSource(source)
|
|
420
555
|
return () => {
|
|
421
556
|
unregister()
|
|
557
|
+
namesCache.clear()
|
|
558
|
+
lexiconListeners.clear()
|
|
422
559
|
}
|
|
423
560
|
}, 'dsh-skill-picker: fuzzy / source')
|
|
424
561
|
}
|