dsh-skill-picker 0.3.3 → 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.3",
4
+ "version": "0.4.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -445,12 +445,15 @@ 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
- // Primary skill source: the official host skills API (the exact RPC ui-skill
449
- // feeds DSH's own `/` completion with session-scoped, all skill layers).
448
+ // Primary skill source: the official host skills API. In DSH 0.1.2-alpha.x
449
+ // the RPC moved from `connection.api.skills` (rc.x) to `remote.skills`
450
+ // (used by the official ui-skill plugin); try both before falling back.
450
451
  const listSkills = async (sessionId) => {
451
- const skills = ctx.connection?.api?.skills
452
+ const remoteSkills = ctx.remote?.skills
453
+ const connectionSkills = ctx.connection?.api?.skills
454
+ const skills = remoteSkills ?? connectionSkills
452
455
  if (skills === undefined || typeof skills.list !== 'function') {
453
- throw new Error('connection.api.skills unavailable')
456
+ throw new Error('skills RPC unavailable (remote.skills / connection.api.skills)')
454
457
  }
455
458
  const controller = new AbortController()
456
459
  const { result } = await skills.list({ sessionId }, controller.signal)
@@ -471,6 +474,7 @@ export function apply(ctx) {
471
474
  currentCwd = ''
472
475
  }
473
476
  }
477
+
474
478
  syncCwd()
475
479
  const unsubscribe = ctx.sessions.list.subscribe(syncCwd)
476
480
  ctx.effect(() => {
@@ -491,101 +495,34 @@ export function apply(ctx) {
491
495
  }
492
496
  }, 'dsh-skill-picker: composer input slot')
493
497
 
494
- // Fuzzy `/` completion source: negative order puts the skill group ABOVE
495
- // the slash commands (command source sits at order 0), and beats the
496
- // official ui-skill prefix source too typing `/` matches name AND
497
- // 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).
498
503
  ctx.effect(() => {
499
- // Skills cache per session, so both candidates and the lexicon (chip
500
- // decoration of `/skill-name` in the draft) have the same names.
501
- const namesCache = new Map() // sessionId -> string[] (skill names)
502
- const lexiconListeners = new Map() // sessionId -> Set<listener>
503
- const notifyLexicon = (sessionId) => {
504
- for (const fn of [...(lexiconListeners.get(sessionId) ?? [])]) {
505
- try { fn() } catch (err) { console.error('[dsh-skill-picker] lexicon listener failed:', err) }
506
- }
507
- }
508
- const refreshNames = async (sessionId) => {
509
- try {
510
- const skills = await listSkills(sessionId)
511
- namesCache.set(sessionId, (Array.isArray(skills) ? skills : []).map((s) => s.name))
512
- notifyLexicon(sessionId)
513
- } catch {
514
- /* keep last cache; lexicon stays empty until a successful fetch */
515
- }
516
- }
517
-
518
- const source = {
519
- trigger: '/',
520
- name: 'skill-fuzzy',
521
- order: -10,
522
- async candidates(session, { query, signal }) {
523
- const skills = await listSkills(session.sessionId)
524
- if (signal.aborted) return []
525
- namesCache.set(session.sessionId, skills.map((s) => s.name))
526
- notifyLexicon(session.sessionId)
527
- const ordered = rankByUsage(skills, loadUsage())
528
- const q = String(query ?? '').trim().toLowerCase()
529
- if (q === '') {
530
- // Empty `/` surfaces ALL skills, usage first (same rule as ⚡ panel).
531
- return ordered.map((s) => ({ name: s.name, description: s.description }))
532
- }
533
- // Typing uses fuzzysort (subsequence matching + relevance score) over
534
- // name AND description, so partial/gappy queries and keywords match.
535
- // The search string also carries the skill's pinyin forms (spaced,
536
- // joined, initials — name and description), so `ji yi` / `jiyi` / `jy`
537
- // match Chinese skill names & descriptions.
538
- const targets = ordered.map((s) => ({
539
- s,
540
- search: `${s.name} ${s.description ?? ''} ${skillPinyinText(s.name, s.description ?? '')}`,
541
- }))
542
- const results = fuzzysort.go(q, targets, {
543
- key: 'search',
544
- limit: 12,
545
- threshold: -10000,
546
- })
547
- return results
548
- .filter((r) => r.score > 0)
549
- .map((r) => ({ name: r.obj.s.name, description: r.obj.s.description }))
550
- },
551
- warm(session) {
552
- refreshNames(session.sessionId)
553
- },
554
- lexicon(session) {
555
- return namesCache.get(session.sessionId)
556
- },
557
- subscribeLexicon(session, listener) {
558
- const key = session.sessionId
559
- const set = lexiconListeners.get(key) ?? new Set()
560
- set.add(listener)
561
- lexiconListeners.set(key, set)
562
- return () => {
563
- const cur = lexiconListeners.get(key)
564
- if (cur !== undefined) {
565
- cur.delete(listener)
566
- if (cur.size === 0) lexiconListeners.delete(key)
567
- }
568
- }
569
- },
570
- onPick({ candidate }) {
571
- // Record usage (same rule as the ⚡ panel) so slash-picked skills
572
- // rank higher the next time they open `/`.
573
- try {
574
- const usage = loadUsage()
575
- const name = candidate.name
576
- const next = { ...usage, [name]: { count: (usage[name]?.count ?? 0) + 1, lastUsed: Date.now() } }
577
- saveUsage(next)
578
- } catch {
579
- /* usage recording is best-effort */
580
- }
581
- return { text: `/${candidate.name} ` }
582
- },
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)
583
522
  }
584
- const unregister = ctx.inputTriggers.registerSource(source)
523
+ window.__dshSkillPickerFuzzy = fuzzyMatch
585
524
  return () => {
586
- unregister()
587
- namesCache.clear()
588
- lexiconListeners.clear()
525
+ if (window.__dshSkillPickerFuzzy === fuzzyMatch) delete window.__dshSkillPickerFuzzy
589
526
  }
590
- }, 'dsh-skill-picker: fuzzy / source')
527
+ }, 'dsh-skill-picker: fuzzy matcher for official / source')
591
528
  }