wisp-agents 0.2.2 → 0.3.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.
@@ -0,0 +1,100 @@
1
+ // Fuzzy matching for the command palette. Each query word has to appear in order
2
+ // (as a subsequence) somewhere in the item; matches at word starts, runs of
3
+ // consecutive letters and early positions score higher. The title is the field
4
+ // that gets highlighted; the other fields can still match, for a lower score.
5
+
6
+ const BOUNDARY = /[\s\-_./:#@()[\]]/;
7
+
8
+ function isStart(text, i) {
9
+ if (i === 0) return true;
10
+ const prev = text[i - 1];
11
+ return BOUNDARY.test(prev) || (prev === prev.toLowerCase() && text[i] !== text[i].toLowerCase());
12
+ }
13
+
14
+ // Score one needle against one haystack, or null if it doesn't match.
15
+ // Finds the tightest window ending at the first full match (fzf v1 style), then scores it.
16
+ export function matchOne(needle, text) {
17
+ if (!needle) return { score: 0, idx: [] };
18
+ const n = needle.toLowerCase();
19
+ const h = text.toLowerCase();
20
+ let end = -1;
21
+ for (let i = 0, j = 0; i < h.length; i++) {
22
+ if (h[i] === n[j] && ++j === n.length) {
23
+ end = i;
24
+ break;
25
+ }
26
+ }
27
+ if (end < 0) return null;
28
+ let start = end;
29
+ for (let j = n.length - 1; j >= 0; start--) if (h[start] === n[j]) j--;
30
+ start++;
31
+
32
+ // A plain substring that starts on a word boundary beats any scattered match.
33
+ const sub = h.indexOf(n);
34
+ if (sub >= 0) {
35
+ let at = sub;
36
+ for (let k = sub; k >= 0; k = h.indexOf(n, k + 1)) if (isStart(text, k)) { at = k; break; }
37
+ const idx = Array.from(n, (_, k) => at + k);
38
+ return { score: score(text, idx), idx };
39
+ }
40
+
41
+ const idx = [];
42
+ for (let i = start, j = 0; j < n.length; i++) if (h[i] === n[j]) idx.push(i), j++;
43
+ return { score: score(text, idx), idx };
44
+ }
45
+
46
+ function score(text, idx) {
47
+ let s = 0;
48
+ for (let k = 0; k < idx.length; k++) {
49
+ const i = idx[k];
50
+ s += 1;
51
+ if (isStart(text, i)) s += k === 0 ? 8 : 5;
52
+ if (k > 0) {
53
+ const gap = i - idx[k - 1] - 1;
54
+ s += gap === 0 ? 4 : -Math.min(gap, 6) * 0.6;
55
+ }
56
+ }
57
+ if (idx[0] === 0) s += 6;
58
+ return s - idx[0] * 0.15 - text.length * 0.02;
59
+ }
60
+
61
+ // Match a whole query against an item. `fields` is [title, ...rest]. Returns
62
+ // { score, idx } where idx are the highlighted title positions, or null.
63
+ export function fuzzy(query, fields) {
64
+ const words = query.trim().split(/\s+/).filter(Boolean);
65
+ if (!words.length) return { score: 0, idx: [] };
66
+ const [title = '', ...rest] = fields;
67
+ let total = 0;
68
+ const idx = new Set();
69
+ for (const w of words) {
70
+ const t = matchOne(w, title);
71
+ let best = t ? t.score * 1.5 : -Infinity;
72
+ for (const f of rest) {
73
+ const m = f && matchOne(w, f);
74
+ if (m && m.score > best) best = m.score;
75
+ }
76
+ if (best === -Infinity) return null;
77
+ if (t && t.score * 1.5 === best) t.idx.forEach((i) => idx.add(i));
78
+ total += best;
79
+ }
80
+ return { score: total, idx: [...idx].sort((a, b) => a - b) };
81
+ }
82
+
83
+ // Wrap matched positions in <mark>, escaping everything else.
84
+ export function highlight(text, idx, esc) {
85
+ if (!idx?.length) return esc(text);
86
+ const on = new Set(idx);
87
+ let out = '';
88
+ let run = '';
89
+ let inRun = false;
90
+ for (let i = 0; i <= text.length; i++) {
91
+ const hit = i < text.length && on.has(i);
92
+ if (hit !== inRun || i === text.length) {
93
+ out += inRun ? `<mark>${esc(run)}</mark>` : esc(run);
94
+ run = '';
95
+ inRun = hit;
96
+ }
97
+ if (i < text.length) run += text[i];
98
+ }
99
+ return out;
100
+ }
@@ -0,0 +1,110 @@
1
+ import { icon, el } from './blocks.js';
2
+ import { esc } from './md.js';
3
+ import { fuzzy, highlight } from './fuzzy.js';
4
+
5
+ // ⌘K command palette: one input over a grouped list of sessions, projects and
6
+ // actions. Generic over its items; app.js decides what's in it and what runs.
7
+ //
8
+ // item: { id, group, label, sub?, icon?, dot?, kbd?, keywords?, run, rank? }
9
+ // With an empty query each group shows its first `limit` items in the given order.
10
+
11
+ const GROUP_LIMIT = 6;
12
+
13
+ export function palette({ items, overlay, placeholder = 'Search sessions, projects and actions…' }) {
14
+ const node = el(`
15
+ <div class="palette" role="dialog" aria-label="Command palette">
16
+ <div class="pal-input">${icon('search')}<input type="text" placeholder="${esc(placeholder)}" spellcheck="false" autocomplete="off" role="combobox" aria-expanded="true" aria-controls="palList"></div>
17
+ <div class="pal-list" id="palList" role="listbox"></div>
18
+ <footer class="pal-foot"><span><kbd>↑</kbd><kbd>↓</kbd> navigate</span><span><kbd>↵</kbd> open</span><span><kbd>esc</kbd> close</span></footer>
19
+ </div>`);
20
+ const input = node.querySelector('input');
21
+ const list = node.querySelector('.pal-list');
22
+ let shown = [];
23
+ let sel = 0;
24
+
25
+ const results = (q) => {
26
+ if (!q.trim()) {
27
+ const per = new Map();
28
+ return items.filter((it) => {
29
+ const n = per.get(it.group) || 0;
30
+ per.set(it.group, n + 1);
31
+ return n < (it.limit ?? GROUP_LIMIT);
32
+ }).map((it) => ({ it, idx: [] }));
33
+ }
34
+ const hits = [];
35
+ for (const it of items) {
36
+ const m = fuzzy(q, [it.label, it.sub || '', it.keywords || '', it.group]);
37
+ if (m) hits.push({ it, idx: m.idx, score: m.score + (it.rank || 0) });
38
+ }
39
+ // Best match first, but keep groups together, ordered by their best hit.
40
+ hits.sort((a, b) => b.score - a.score);
41
+ const order = [...new Set(hits.map((h) => h.it.group))];
42
+ return order.flatMap((g) => hits.filter((h) => h.it.group === g).slice(0, 12));
43
+ };
44
+
45
+ const render = () => {
46
+ shown = results(input.value);
47
+ sel = Math.min(sel, Math.max(0, shown.length - 1));
48
+ if (!shown.length) {
49
+ list.innerHTML = `<div class="pal-empty">No results for “${esc(input.value.trim())}”</div>`;
50
+ return;
51
+ }
52
+ let group = null;
53
+ list.innerHTML = shown
54
+ .map(({ it, idx }, i) => {
55
+ const head = it.group !== group ? `<div class="pal-group">${esc((group = it.group))}</div>` : '';
56
+ const lead = it.dot ? `<i class="p-dot c-${esc(it.dot)}"></i>` : icon(it.icon || 'arrow');
57
+ return `${head}<div class="pal-row ${i === sel ? 'on' : ''}" role="option" data-i="${i}" aria-selected="${i === sel}">
58
+ <span class="pal-lead">${lead}</span>
59
+ <span class="pal-label">${highlight(it.label, idx, esc)}</span>
60
+ ${it.sub ? `<span class="pal-sub">${esc(it.sub)}</span>` : ''}
61
+ ${it.kbd ? `<span class="pal-kbd">${it.kbd.split(' ').map((k) => `<kbd>${esc(k)}</kbd>`).join('')}</span>` : ''}
62
+ </div>`;
63
+ })
64
+ .join('');
65
+ };
66
+
67
+ const move = (to) => {
68
+ if (!shown.length) return;
69
+ sel = (to + shown.length) % shown.length;
70
+ list.querySelectorAll('.pal-row').forEach((r, i) => {
71
+ r.classList.toggle('on', i === sel);
72
+ r.setAttribute('aria-selected', i === sel);
73
+ });
74
+ list.querySelector('.pal-row.on')?.scrollIntoView({ block: 'nearest' });
75
+ };
76
+
77
+ const close = overlay(node);
78
+ const pick = (i) => {
79
+ const hit = shown[i];
80
+ if (!hit) return;
81
+ close();
82
+ // Let the overlay start fading before the action moves focus or opens another modal.
83
+ setTimeout(() => hit.it.run(), 0);
84
+ };
85
+
86
+ input.addEventListener('input', () => ((sel = 0), render(), (list.scrollTop = 0)));
87
+ input.addEventListener('keydown', (e) => {
88
+ const ctrl = e.ctrlKey && !e.metaKey;
89
+ if (e.key === 'ArrowDown' || (ctrl && (e.key === 'n' || e.key === 'j'))) move(sel + 1);
90
+ else if (e.key === 'ArrowUp' || (ctrl && (e.key === 'p' || e.key === 'k'))) move(sel - 1);
91
+ else if (e.key === 'PageDown') move(Math.min(shown.length - 1, sel + 8));
92
+ else if (e.key === 'PageUp') move(Math.max(0, sel - 8));
93
+ else if (e.key === 'Enter' && !e.isComposing) pick(sel);
94
+ else return;
95
+ e.preventDefault();
96
+ });
97
+ // mousemove, not mouseenter: rows sliding under a still cursor while scrolling shouldn't steal the selection.
98
+ list.addEventListener('mousemove', (e) => {
99
+ const r = e.target.closest('.pal-row');
100
+ if (r && Number(r.dataset.i) !== sel) move(Number(r.dataset.i));
101
+ });
102
+ list.addEventListener('click', (e) => {
103
+ const r = e.target.closest('.pal-row');
104
+ if (r) pick(Number(r.dataset.i));
105
+ });
106
+
107
+ render();
108
+ input.focus();
109
+ return close;
110
+ }
@@ -0,0 +1,3 @@
1
+ Model and vendor logos from @lobehub/icons-static-svg 1.95.1
2
+ https://github.com/lobehub/lobe-icons — MIT License, Copyright (c) LobeHub.
3
+ The marks themselves are trademarks of their respective owners.
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Claude Code</title><path clip-rule="evenodd" d="M20.998 10.949H24v3.102h-3v3.028h-1.487V20H18v-2.921h-1.487V20H15v-2.921H9V20H7.488v-2.921H6V20H4.487v-2.921H3V14.05H0V10.95h3V5h17.998v5.949zM6 10.949h1.488V8.102H6v2.847zm10.51 0H18V8.102h-1.49v2.847z" fill="#D97757" fill-rule="evenodd"></path></svg>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Codex</title><path d="M19.503 0H4.496A4.496 4.496 0 000 4.496v15.007A4.496 4.496 0 004.496 24h15.007A4.496 4.496 0 0024 19.503V4.496A4.496 4.496 0 0019.503 0z" fill="#fff"></path><path d="M9.064 3.344a4.578 4.578 0 012.285-.312c1 .115 1.891.54 2.673 1.275.01.01.024.017.037.021a.09.09 0 00.043 0 4.55 4.55 0 013.046.275l.047.022.116.057a4.581 4.581 0 012.188 2.399c.209.51.313 1.041.315 1.595a4.24 4.24 0 01-.134 1.223.123.123 0 00.03.115c.594.607.988 1.33 1.183 2.17.289 1.425-.007 2.71-.887 3.854l-.136.166a4.548 4.548 0 01-2.201 1.388.123.123 0 00-.081.076c-.191.551-.383 1.023-.74 1.494-.9 1.187-2.222 1.846-3.711 1.838-1.187-.006-2.239-.44-3.157-1.302a.107.107 0 00-.105-.024c-.388.125-.78.143-1.204.138a4.441 4.441 0 01-1.945-.466 4.544 4.544 0 01-1.61-1.335c-.152-.202-.303-.392-.414-.617a5.81 5.81 0 01-.37-.961 4.582 4.582 0 01-.014-2.298.124.124 0 00.006-.056.085.085 0 00-.027-.048 4.467 4.467 0 01-1.034-1.651 3.896 3.896 0 01-.251-1.192 5.189 5.189 0 01.141-1.6c.337-1.112.982-1.985 1.933-2.618.212-.141.413-.251.601-.33.215-.089.43-.164.646-.227a.098.098 0 00.065-.066 4.51 4.51 0 01.829-1.615 4.535 4.535 0 011.837-1.388zm3.482 10.565a.637.637 0 000 1.272h3.636a.637.637 0 100-1.272h-3.636zM8.462 9.23a.637.637 0 00-1.106.631l1.272 2.224-1.266 2.136a.636.636 0 101.095.649l1.454-2.455a.636.636 0 00.005-.64L8.462 9.23z" fill="url(#lobe-icons-codex-_R_0_)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-codex-_R_0_" x1="12" x2="12" y1="3" y2="21"><stop stop-color="#B1A7FF"></stop><stop offset=".5" stop-color="#7A9DFF"></stop><stop offset="1" stop-color="#3941FF"></stop></linearGradient></defs></svg>
@@ -0,0 +1 @@
1
+ <svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cursor</title><path d="M22.106 5.68L12.5.135a.998.998 0 00-.998 0L1.893 5.68a.84.84 0 00-.419.726v11.186c0 .3.16.577.42.727l9.607 5.547a.999.999 0 00.998 0l9.608-5.547a.84.84 0 00.42-.727V6.407a.84.84 0 00-.42-.726zm-.603 1.176L12.228 22.92c-.063.108-.228.064-.228-.061V12.34a.59.59 0 00-.295-.51l-9.11-5.26c-.107-.062-.063-.228.062-.228h18.55c.264 0 .428.286.296.514z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>opencode</title><path d="M16 6H8v12h8V6zm4 16H4V2h16v20z"></path></svg>