dsh-skill-picker 0.3.4 → 0.4.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/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.3.4",
4
+ "version": "0.4.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -23,8 +23,7 @@
23
23
  "client": {
24
24
  "inject": [
25
25
  "@deepseek-ai/dsh-client-runtime",
26
- "@deepseek-ai/dsh-client-locale",
27
- "@deepseek-ai/dsh-client-ui-input-trigger"
26
+ "@deepseek-ai/dsh-client-locale"
28
27
  ],
29
28
  "platform": "web"
30
29
  },
@@ -445,12 +445,6 @@ function SkillPickerButton(props) {
445
445
 
446
446
  /** Apply the browser half: register the picker into the composer tool row. */
447
447
  export function apply(ctx) {
448
- // Resolve the trigger pipeline service at apply scope (not inside an effect),
449
- // exactly like the official ui-skill plugin does — in DSH 0.1.2-alpha.x the
450
- // service lives in @deepseek-ai/dsh-client-ui-input-trigger and is only
451
- // reachable from the plugin root context.
452
- const inputTriggers = (typeof ctx.get === 'function' ? ctx.get('inputTriggers') : ctx.inputTriggers)
453
-
454
448
  // Primary skill source: the official host skills API. In DSH 0.1.2-alpha.x
455
449
  // the RPC moved from `connection.api.skills` (rc.x) to `remote.skills`
456
450
  // (used by the official ui-skill plugin); try both before falling back.
@@ -481,28 +475,6 @@ export function apply(ctx) {
481
475
  }
482
476
  }
483
477
 
484
- // Host-route fallback: same scan the ⚡ panel uses (official provider roots).
485
- const fetchHostSkills = async () => {
486
- const cwd = typeof currentCwd === 'string' && currentCwd !== '' ? `?cwd=${encodeURIComponent(currentCwd)}` : ''
487
- const res = await fetch(`/dsh-skill-picker/skills${cwd}`, { headers: { accept: 'application/json' } })
488
- const json = await res.json()
489
- if (!json.ok) throw new Error(json.error || 'bad response')
490
- return Array.isArray(json.skills) ? json.skills : []
491
- }
492
-
493
- // Resolve skills by official RPC first, then the host scan route; never
494
- // throws (returns [] when both fail) so `/` completion degrades gracefully.
495
- const resolveSkills = async (sessionId) => {
496
- try {
497
- return await listSkills(sessionId)
498
- } catch {
499
- try {
500
- return await fetchHostSkills()
501
- } catch {
502
- return []
503
- }
504
- }
505
- }
506
478
  syncCwd()
507
479
  const unsubscribe = ctx.sessions.list.subscribe(syncCwd)
508
480
  ctx.effect(() => {
@@ -523,107 +495,34 @@ export function apply(ctx) {
523
495
  }
524
496
  }, 'dsh-skill-picker: composer input slot')
525
497
 
526
- // Fuzzy `/` completion source: negative order puts the skill group ABOVE
527
- // the slash commands (command source sits at order 0), and beats the
528
- // official ui-skill prefix source too typing `/` matches name AND
529
- // description anywhere, with usage ordering (same rule as the panel).
498
+ // Fuzzy `/` completion: instead of registering a parallel source group
499
+ // (which would appear as a second list next to the official one), expose a
500
+ // global matcher that the patched official ui-skill candidates calls. The
501
+ // official group stays THE single `/` list; only its matching behaviour is
502
+ // upgraded to fuzzy + pinyin (name AND description, subsequence scoring).
530
503
  ctx.effect(() => {
531
- // Skills cache per session, so both candidates and the lexicon (chip
532
- // decoration of `/skill-name` in the draft) have the same names.
533
- const namesCache = new Map() // sessionId -> string[] (skill names)
534
- const lexiconListeners = new Map() // sessionId -> Set<listener>
535
- const notifyLexicon = (sessionId) => {
536
- for (const fn of [...(lexiconListeners.get(sessionId) ?? [])]) {
537
- try { fn() } catch (err) { console.error('[dsh-skill-picker] lexicon listener failed:', err) }
538
- }
539
- }
540
- const refreshNames = async (sessionId) => {
541
- try {
542
- const skills = await resolveSkills(sessionId)
543
- namesCache.set(sessionId, (Array.isArray(skills) ? skills : []).map((s) => s.name))
544
- notifyLexicon(sessionId)
545
- } catch {
546
- /* keep last cache; lexicon stays empty until a successful fetch */
547
- }
548
- }
549
-
550
- const source = {
551
- trigger: '/',
552
- name: 'skill-fuzzy',
553
- order: -10,
554
- async candidates(session, { query, signal }) {
555
- const skills = await resolveSkills(session.sessionId)
556
- if (signal.aborted) return []
557
- namesCache.set(session.sessionId, skills.map((s) => s.name))
558
- notifyLexicon(session.sessionId)
559
- const ordered = rankByUsage(skills, loadUsage())
560
- const q = String(query ?? '').trim().toLowerCase()
561
- if (q === '') {
562
- // Empty `/` surfaces ALL skills, usage first (same rule as ⚡ panel).
563
- return ordered.map((s) => ({ name: s.name, description: s.description }))
564
- }
565
- // Typing uses fuzzysort (subsequence matching + relevance score) over
566
- // name AND description, so partial/gappy queries and keywords match.
567
- // The search string also carries the skill's pinyin forms (spaced,
568
- // joined, initials — name and description), so `ji yi` / `jiyi` / `jy`
569
- // match Chinese skill names & descriptions.
570
- const targets = ordered.map((s) => ({
571
- s,
572
- search: `${s.name} ${s.description ?? ''} ${skillPinyinText(s.name, s.description ?? '')}`,
573
- }))
574
- const results = fuzzysort.go(q, targets, {
575
- key: 'search',
576
- limit: 12,
577
- threshold: -10000,
578
- })
579
- return results
580
- .filter((r) => r.score > 0)
581
- .map((r) => ({ name: r.obj.s.name, description: r.obj.s.description }))
582
- },
583
- warm(session) {
584
- refreshNames(session.sessionId)
585
- },
586
- lexicon(session) {
587
- return namesCache.get(session.sessionId)
588
- },
589
- subscribeLexicon(session, listener) {
590
- const key = session.sessionId
591
- const set = lexiconListeners.get(key) ?? new Set()
592
- set.add(listener)
593
- lexiconListeners.set(key, set)
594
- return () => {
595
- const cur = lexiconListeners.get(key)
596
- if (cur !== undefined) {
597
- cur.delete(listener)
598
- if (cur.size === 0) lexiconListeners.delete(key)
599
- }
600
- }
601
- },
602
- onPick({ candidate }) {
603
- // Record usage (same rule as the ⚡ panel) so slash-picked skills
604
- // rank higher the next time they open `/`.
605
- try {
606
- const usage = loadUsage()
607
- const name = candidate.name
608
- const next = { ...usage, [name]: { count: (usage[name]?.count ?? 0) + 1, lastUsed: Date.now() } }
609
- saveUsage(next)
610
- } catch {
611
- /* usage recording is best-effort */
612
- }
613
- return { text: `/${candidate.name} ` }
614
- },
615
- }
616
- // inputTriggers is resolved at apply scope (see top of apply); fail loudly
617
- // instead of silently dropping the fuzzy `/` source.
618
- if (inputTriggers === undefined || typeof inputTriggers.registerSource !== 'function') {
619
- console.warn('[dsh-skill-picker] inputTriggers service unavailable; fuzzy / completion disabled (⚡ panel still works)')
620
- return
504
+ // Mirror the panel's `rankByUsage` semantics: recently/frequently used
505
+ // skills first, then the untouched rest so usage history keeps working.
506
+ const fuzzyMatch = (skills, query = '') => {
507
+ const ordered = rankByUsage(skills, loadUsage())
508
+ const q = String(query).trim().toLowerCase()
509
+ if (q === '') return ordered
510
+ const targets = ordered.map((s) => ({
511
+ s,
512
+ search: `${s.name} ${s.description ?? ''} ${skillPinyinText(s.name, s.description ?? '')}`,
513
+ }))
514
+ const results = fuzzysort.go(q, targets, {
515
+ key: 'search',
516
+ limit: 12,
517
+ threshold: -10000,
518
+ })
519
+ return results
520
+ .filter((r) => r.score > 0)
521
+ .map((r) => r.obj.s)
621
522
  }
622
- const unregister = inputTriggers.registerSource(source)
523
+ window.__dshSkillPickerFuzzy = fuzzyMatch
623
524
  return () => {
624
- unregister()
625
- namesCache.clear()
626
- lexiconListeners.clear()
525
+ if (window.__dshSkillPickerFuzzy === fuzzyMatch) delete window.__dshSkillPickerFuzzy
627
526
  }
628
- }, 'dsh-skill-picker: fuzzy / source')
527
+ }, 'dsh-skill-picker: fuzzy matcher for official / source')
629
528
  }