aegiscode 5.2.33 → 6.1.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/src/fuzzy.js ADDED
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ // Fuzzy command matching for the "/" palette (and anything else that needs it).
4
+ //
5
+ // The real Claude Code palette ranks matches by relevance rather than plain
6
+ // prefix: an exact match beats a prefix, a prefix beats a substring, and a
7
+ // substring beats characters scattered through the name. Ties fall back to
8
+ // the source (registry) order so the palette stays stable for empty queries.
9
+ //
10
+ // Scoring tiers (deliberately spaced so no position/length detail in a lower
11
+ // tier can overtake a higher tier):
12
+ // exact match 1000
13
+ // prefix match 900
14
+ // substring 700 − position (earlier substring wins)
15
+ // scattered 50 + bonuses (consecutive runs, name-start hits)
16
+ //
17
+ // Ported verbatim from aegiscodex-dev/src/fuzzy.js (ESM → CommonJS). The tier
18
+ // numbers are a contract: cli-fuzzy.test.mjs asserts the ordering they encode.
19
+
20
+ function fuzzyScore(query, name) {
21
+ const q = String(query).toLowerCase();
22
+ const n = String(name).toLowerCase();
23
+ if (!q) return { matched: true, score: 0 };
24
+ if (n === q) return { matched: true, score: 1000 };
25
+ if (n.startsWith(q)) return { matched: true, score: 900 };
26
+ const at = n.indexOf(q);
27
+ if (at !== -1) return { matched: true, score: 700 - at };
28
+
29
+ // Scattered subsequence: every query char must appear in order.
30
+ let score = 50;
31
+ let prev = -2;
32
+ let last = -1;
33
+ for (const ch of q) {
34
+ const k = n.indexOf(ch, last + 1);
35
+ if (k === -1) return { matched: false, score: 0 };
36
+ if (k === prev + 1) score += 15; // consecutive run — strong signal
37
+ else score += 5;
38
+ if (k === 0) score += 10; // starts at the name — weak signal
39
+ prev = k;
40
+ last = k;
41
+ }
42
+ return { matched: true, score };
43
+ }
44
+
45
+ /**
46
+ * The indices (in the original `name`) of the characters that matched
47
+ * `query`, mirroring fuzzyScore's tiers — exact = every index, prefix =
48
+ * leading run, substring = that run, scattered = the scattered picks.
49
+ * Returns [] for an empty query and null when nothing matches.
50
+ * Drives the palette's bold-matched-chars rendering.
51
+ */
52
+ function fuzzyMatchPositions(query, name) {
53
+ const q = String(query).toLowerCase();
54
+ const n = String(name).toLowerCase();
55
+ if (!q) return [];
56
+ if (n === q) return Array.from({ length: q.length }, (_, i) => i);
57
+ if (n.startsWith(q)) return Array.from({ length: q.length }, (_, i) => i);
58
+ const at = n.indexOf(q);
59
+ if (at !== -1) return Array.from({ length: q.length }, (_, i) => at + i);
60
+ const pos = [];
61
+ let last = -1;
62
+ for (const ch of q) {
63
+ const k = n.indexOf(ch, last + 1);
64
+ if (k === -1) return null;
65
+ pos.push(k);
66
+ last = k;
67
+ }
68
+ return pos;
69
+ }
70
+
71
+ /**
72
+ * Rank `items` by how well `nameOf(item)` matches `query`.
73
+ * Empty query returns the items unchanged (preserves registry/palette order).
74
+ * Non-matches are dropped; matches sort by score desc, then source order.
75
+ */
76
+ function fuzzyRank(query, items, nameOf) {
77
+ const q = String(query || '');
78
+ if (!q) return items;
79
+ return items
80
+ .map((item, idx) => ({ item, idx, m: fuzzyScore(q, nameOf(item)) }))
81
+ .filter((x) => x.m.matched)
82
+ .sort((a, b) => b.m.score - a.m.score || a.idx - b.idx)
83
+ .map((x) => x.item);
84
+ }
85
+
86
+ /**
87
+ * Alias-aware ranking. The reference palette searches the command name and its
88
+ * aliases (name weight 3, aliases weight 2.5 — Fuse.js in the reference; here
89
+ * the best fuzzy score across both wins). An item matches when the query scores
90
+ * against at least one of its names; ties keep source order so the palette
91
+ * stays stable.
92
+ */
93
+ function fuzzyRankWithAliases(query, items, nameOf, aliasesOf) {
94
+ const q = String(query || '');
95
+ if (!q) return items;
96
+ return items
97
+ .map((item, idx) => {
98
+ const names = [nameOf(item), ...((aliasesOf && aliasesOf(item)) || [])];
99
+ let best = null;
100
+ for (const n of names) {
101
+ const m = fuzzyScore(q, n);
102
+ if (m.matched && (!best || m.score > best.score)) best = m;
103
+ }
104
+ return { item, idx, m: best };
105
+ })
106
+ .filter((x) => x.m)
107
+ .sort((a, b) => b.m.score - a.m.score || a.idx - b.idx)
108
+ .map((x) => x.item);
109
+ }
110
+
111
+ module.exports = {
112
+ fuzzyScore,
113
+ fuzzyMatchPositions,
114
+ fuzzyRank,
115
+ fuzzyRankWithAliases,
116
+ };
@@ -0,0 +1,303 @@
1
+ 'use strict';
2
+
3
+ // Minimal markdown → styled span lines, in the spirit of Claude Code's
4
+ // transcript rendering (monokai-flavored code, bold text, inline code).
5
+ //
6
+ // Ported from aegiscodex-dev/src/markdown.js (ESM → CommonJS). The one
7
+ // structural change: instead of importing `span`/`w` from screen.js — which a
8
+ // concurrent workstream is rewriting — this module carries a local cell-width
9
+ // function and a local `span()` that produce the *same* shape the rest of the
10
+ // design system consumes: { t: text, s: style-prefix, w: cell-width }. That
11
+ // shape is a contract with whoever renders these lines.
12
+ //
13
+ // Colours come from the theme tokens (via themeOf(ctx)) rather than a bare
14
+ // palette, so light/dark both work.
15
+
16
+ const {
17
+ BOLD,
18
+ BOLD_OFF,
19
+ DIM,
20
+ ITALIC,
21
+ ITALIC_OFF,
22
+ RESET_BG,
23
+ RESET_FG,
24
+ themeOf,
25
+ } = require('./theme.js');
26
+
27
+ // Real terminal cell width, mirroring aegiscodex-dev/src/screen.js's w():
28
+ // combining marks / variation selectors are zero-width, CJK fullwidth forms and
29
+ // emoji are two cells, everything else one. A plain codepoint count misaligns
30
+ // every width decision the moment a line contains CJK or emoji.
31
+ const RE_ZERO = /[\u0300-\u036F\u1AB0-\u1AFF\u1DC0-\u1DFF\u20D0-\u20FF\uFE00-\uFE0F\uFE20-\uFE2F]/u;
32
+ const RE_WIDE = /[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uA000-\uA4CF\uA960-\uA97F\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6\u{1F1E6}-\u{1F1FF}\u{1F300}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{1FA70}-\u{1FAFF}\u{20000}-\u{3FFFD}]/u;
33
+ function w(t) {
34
+ let n = 0;
35
+ for (const ch of String(t == null ? '' : t)) {
36
+ if (RE_ZERO.test(ch)) continue;
37
+ n += RE_WIDE.test(ch) ? 2 : 1;
38
+ }
39
+ return n;
40
+ }
41
+
42
+ /** A styled text run. Shape { t, s, w } is shared with the render model. */
43
+ function span(style, t) {
44
+ return { t, s: style, w: w(t) };
45
+ }
46
+
47
+ // Turn an fg colour token into its background twin (38;2;R;G;B → 48;2;R;G;B),
48
+ // so a code block sits on the theme's own code-block tint without typing a
49
+ // colour inline — it stays theme-aware for free.
50
+ function asBg(fgSgr) {
51
+ return fgSgr.replace('[38;2;', '[48;2;');
52
+ }
53
+
54
+ // Rendering rules verified against a live capture of Claude Code 2.1.211:
55
+ // - code lines: two-space indent, no fence label; a theme-tinted background
56
+ // so the block reads as a distinct surface
57
+ // - "- item" → " - item" (hyphen kept, two-space indent)
58
+ // - "> quote" → " ▎ quote" (dim ▎, italic text)
59
+ function renderMarkdown(text, width, ctx) {
60
+ const t = themeOf(ctx);
61
+ const codeStyle = asBg(t.black) + t.white; // code-block surface
62
+ const lines = String(text == null ? '' : text).split('\n');
63
+ const out = [];
64
+ let inCode = false;
65
+ for (const raw of lines) {
66
+ const line = raw.replace(/\r$/, '');
67
+ if (line.startsWith('```')) {
68
+ inCode = !inCode;
69
+ continue; // fences are not rendered
70
+ }
71
+ if (inCode) {
72
+ out.push([span(codeStyle, ' ' + (line || ''))]);
73
+ continue;
74
+ }
75
+ out.push(...inlineLine(line, width, t));
76
+ }
77
+ return out;
78
+ }
79
+
80
+ // Render one plain line with inline **bold** / `code` / "- " bullets / quotes.
81
+ function inlineLine(text, width, t) {
82
+ const ts = text.trimStart();
83
+ let body = text;
84
+ let prefix = null;
85
+ let isQuote = false;
86
+ // "- item" → " - item": keep the hyphen marker with a two-space indent.
87
+ if (ts.startsWith('- ')) {
88
+ prefix = span(t.white, ' - ');
89
+ body = ts.slice(2);
90
+ } else if (ts.startsWith(' - ')) {
91
+ prefix = span(t.white, ' - ');
92
+ body = ts.slice(4);
93
+ } else if (ts.startsWith('> ')) {
94
+ // "> quote" → " ▎ quote" (dim bar, italic body).
95
+ prefix = span(DIM, ' ▎ ');
96
+ body = ts.slice(2);
97
+ isQuote = true;
98
+ }
99
+
100
+ const spans = [];
101
+ const re = /(\*\*[^*]+\*\*|`[^`]+`)/g;
102
+ let last = 0;
103
+ let m;
104
+ while ((m = re.exec(body))) {
105
+ if (m.index > last) pushText(spans, body.slice(last, m.index), t);
106
+ const tok = m[0];
107
+ if (tok.startsWith('**')) pushText(spans, tok.slice(2, -2), t, BOLD);
108
+ else pushText(spans, tok.slice(1, -1), t, t.green);
109
+ last = m.index + tok.length;
110
+ }
111
+ if (last < body.length) pushText(spans, body.slice(last), t);
112
+ if (isQuote) {
113
+ // italicize the body of a quote line
114
+ for (const sp of spans) sp.s = ITALIC + sp.s + ITALIC_OFF;
115
+ }
116
+ return wrapInline(spans, width, prefix);
117
+ }
118
+
119
+ function pushText(spans, text, t, style) {
120
+ // Emit only non-space words; wrapInline adds separators. This avoids
121
+ // double-spacing when text arrives in chunks.
122
+ const s = style == null ? t.white : style;
123
+ const parts = text.split(/[ \t]+/).filter(Boolean);
124
+ for (const p of parts) {
125
+ spans.push(span(s, p));
126
+ }
127
+ }
128
+
129
+ // Wrap the word spans, prepending `prefix` verbatim to the first line only
130
+ // (so " - " / " ▎ " markers keep their exact spacing).
131
+ function wrapInline(line, width, prefix) {
132
+ const res = [];
133
+ let cur = prefix ? [prefix] : [];
134
+ let curW = prefix ? prefix.w : 0;
135
+ // Does the accumulator's last character end in whitespace? This mirrors the
136
+ // old /\s$/.test(cur.map(join)) check but tracks incrementally — the join
137
+ // version was O(line) PER WORD, making a paragraph render O(n²) and freezing
138
+ // the UI on long streaming answers.
139
+ let endsSpace = prefix ? /\s$/.test(prefix.t) : false;
140
+ const flush = () => {
141
+ if (cur.length) {
142
+ res.push(cur);
143
+ cur = [];
144
+ curW = 0;
145
+ endsSpace = false;
146
+ }
147
+ };
148
+ for (const sp of line) {
149
+ const words = sp.t.split(' ');
150
+ for (let i = 0; i < words.length; i++) {
151
+ const word = words[i];
152
+ const ww = w(word);
153
+ // Only insert a separator between words when nothing before already
154
+ // ends in whitespace (prevents "· " + word becoming "· word").
155
+ const needSep = curW > 0 && i === 0 && /^\S/.test(word) && !endsSpace;
156
+ if (curW + ww + (needSep ? 1 : 0) > width && curW > 0) {
157
+ flush();
158
+ }
159
+ if (ww > width) {
160
+ // An unbreakable token wider than the terminal: emit one width-sized
161
+ // row per piece. Slice once into code-point arrays (O(n)) rather than
162
+ // re-slicing the remainder each piece (O(n²)), and cap the rows so a
163
+ // pathological token can't balloon the transcript either.
164
+ const chars = [...word];
165
+ const cws = chars.map((c) => w(c)); // cell width per code point, once
166
+ let total = 0;
167
+ for (const cw of cws) total += cw;
168
+ const MAX_PIECES = 400;
169
+ let j = 0;
170
+ let piece = 0;
171
+ while (total > width) {
172
+ // Take whole code points until the row is full — a codepoint slice
173
+ // (width chars) would split 2-cell CJK/emoji mid-glyph.
174
+ let take = 0;
175
+ let hw = 0;
176
+ while (j + take < chars.length && hw + cws[j + take] <= width) {
177
+ hw += cws[j + take];
178
+ take++;
179
+ }
180
+ if (!take) take = 1; // single glyph wider than the terminal
181
+ const head = chars.slice(j, j + take).join('');
182
+ if (curW) {
183
+ cur.push(span('', ' '));
184
+ curW++;
185
+ endsSpace = true;
186
+ }
187
+ cur.push(span(sp.s, head));
188
+ curW += hw;
189
+ flush();
190
+ j += take;
191
+ total -= hw;
192
+ if (++piece >= MAX_PIECES) {
193
+ if (curW) {
194
+ cur.push(span('', ' '));
195
+ curW++;
196
+ }
197
+ cur.push(span(sp.s, '…'));
198
+ flush();
199
+ return res.length ? res : [[span('', '')]];
200
+ }
201
+ }
202
+ const rest = chars.slice(j).join('');
203
+ if (rest) {
204
+ cur.push(span(sp.s, rest));
205
+ curW += w(rest);
206
+ endsSpace = /\s$/.test(rest);
207
+ }
208
+ } else {
209
+ if (curW > 0 && needSep) {
210
+ cur.push(span('', ' '));
211
+ curW++;
212
+ endsSpace = true;
213
+ }
214
+ cur.push(span(sp.s, word));
215
+ curW += ww;
216
+ endsSpace = /\s$/.test(word);
217
+ }
218
+ }
219
+ }
220
+ flush();
221
+ return res.length ? res : [[span('', '')]];
222
+ }
223
+
224
+ // The Monokai Extended diff preview shown on the theme picker (verbatim from
225
+ // the reference capture; the literals are that theme's colours, not this one's).
226
+ function renderDiffPreview(width, ctx) {
227
+ const t = themeOf(ctx);
228
+ const div = '╌'.repeat(Math.max(10, width - 4));
229
+ const num = (s) => span(t.gray + '\x1b[2m', s);
230
+ const plain = (s) => span(t.white, s);
231
+ const kw = (s) => span(RGB_LEGACY(102, 217, 239), s); // monokai: function
232
+ const fn = (s) => span(RGB_LEGACY(166, 226, 46), s);
233
+ const str = (s) => span(RGB_LEGACY(230, 219, 116), s);
234
+ const del = (s) => span('\x1b[38;2;220;90;90m\x1b[48;2;61;1;0m', s);
235
+ const delHi = (s) => span('\x1b[38;2;248;248;242m\x1b[48;2;92;2;0m', s);
236
+ const add = (s) => span('\x1b[38;2;80;200;80m\x1b[48;2;2;40;0m', s);
237
+ const addHi = (s) => span('\x1b[38;2;255;255;255m\x1b[48;2;4;71;0m', s);
238
+
239
+ const W = Math.max(28, width - 8);
240
+ const padBg = (spans, bg) => {
241
+ let cur = 0;
242
+ const out = [...spans];
243
+ for (const sp of out) cur += sp.w;
244
+ if (cur < W) out.push(span(bg + ' ', ' '.repeat(W - cur)));
245
+ out.push(span(RESET_BG, ''));
246
+ return out;
247
+ };
248
+
249
+ const lines = [];
250
+ lines.push([span(t.dim, div)]);
251
+ lines.push([num(' 1 '), kw('function'), plain(' '), fn('greet'), plain('()'), span(t.white, '{')]);
252
+ lines.push(
253
+ padBg(
254
+ [
255
+ num(' 2 '),
256
+ del('-'),
257
+ plain(' '),
258
+ del('console'),
259
+ span('\x1b[38;2;166;226;46m\x1b[48;2;61;1;0m', '.log'),
260
+ del('('),
261
+ str('"Hello, '),
262
+ delHi('World'),
263
+ del('!");'),
264
+ ],
265
+ '\x1b[48;2;61;1;0m'
266
+ )
267
+ );
268
+ lines.push(
269
+ padBg(
270
+ [
271
+ num(' 2 '),
272
+ add('+'),
273
+ plain(' '),
274
+ add('console'),
275
+ span('\x1b[38;2;166;226;46m\x1b[48;2;2;40;0m', '.log'),
276
+ add('('),
277
+ str('"Hello, '),
278
+ addHi('Claude'),
279
+ add('!");'),
280
+ ],
281
+ '\x1b[48;2;2;40;0m'
282
+ )
283
+ );
284
+ lines.push([num(' 3 '), plain('}')]);
285
+ lines.push([span(t.dim, div)]);
286
+ return lines;
287
+ }
288
+
289
+ function RGB_LEGACY(r, g, b) {
290
+ return `\x1b[38;2;${r};${g};${b}m`;
291
+ }
292
+
293
+ module.exports = {
294
+ renderMarkdown,
295
+ renderDiffPreview,
296
+ // Re-exported style tokens the reference exposed for legacy consumers.
297
+ RESET_FG,
298
+ BOLD,
299
+ BOLD_OFF,
300
+ // Also expose the local model helpers so consumers/tests can reuse them.
301
+ span,
302
+ w,
303
+ };