@hncode/hncode 0.5.5 → 0.6.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.
Files changed (56) hide show
  1. package/package.json +37 -38
  2. package/src/agent.js +67 -2
  3. package/src/image-row.js +111 -0
  4. package/src/latex.js +1557 -0
  5. package/src/llm.js +19 -4
  6. package/src/mermaid/ansi.js +55 -0
  7. package/src/mermaid/canvas.js +449 -0
  8. package/src/mermaid/class-style.js +57 -0
  9. package/src/mermaid/css-colors.js +153 -0
  10. package/src/mermaid/diagrams/class.js +289 -0
  11. package/src/mermaid/diagrams/er.js +201 -0
  12. package/src/mermaid/diagrams/flowchart.js +488 -0
  13. package/src/mermaid/diagrams/gitgraph.js +205 -0
  14. package/src/mermaid/diagrams/mindmap.js +101 -0
  15. package/src/mermaid/diagrams/pie.js +104 -0
  16. package/src/mermaid/diagrams/sequence.js +282 -0
  17. package/src/mermaid/diagrams/state.js +292 -0
  18. package/src/mermaid/diagrams/timeline.js +99 -0
  19. package/src/mermaid/graph.js +138 -0
  20. package/src/mermaid/index.js +59 -0
  21. package/src/mermaid/labels.js +339 -0
  22. package/src/mermaid/layout-seq.js +225 -0
  23. package/src/mermaid/layout.js +1454 -0
  24. package/src/mermaid/registry.js +54 -0
  25. package/src/mermaid/source-box.js +102 -0
  26. package/src/mermaid/statements.js +235 -0
  27. package/src/mermaid/types.js +1 -0
  28. package/src/mermaid/width.js +29 -0
  29. package/src/screen-diff.js +512 -0
  30. package/src/screen-paint.js +128 -0
  31. package/src/screen.js +277 -0
  32. package/src/subagent-types.js +136 -147
  33. package/src/term-caps.js +10 -4
  34. package/src/tex/font.js +292 -0
  35. package/src/tex/fonts/LICENSE.txt +11 -0
  36. package/src/tex/fonts/STIX2Math.ttf +0 -0
  37. package/src/tex/index.js +293 -0
  38. package/src/tex/kitty.js +186 -0
  39. package/src/tex/layout.js +446 -0
  40. package/src/tex/parse.js +331 -0
  41. package/src/tex/raster.js +216 -0
  42. package/src/tex/render.js +167 -0
  43. package/src/tex/sixel.js +326 -0
  44. package/src/tex/svg.js +237 -0
  45. package/src/tool-result.js +2 -2
  46. package/src/tools/agent-swarm.js +3 -15
  47. package/src/tools/agent.js +454 -464
  48. package/src/tools/ask-user-question.js +195 -206
  49. package/src/tools/bash.js +39 -3
  50. package/src/tools/edit.js +10 -112
  51. package/src/tools/fetch-url.js +130 -120
  52. package/src/tools/memory.js +80 -88
  53. package/src/tools/read.js +27 -23
  54. package/src/tools/todo.js +8 -13
  55. package/src/tools/validate-args.js +78 -0
  56. package/src/tui.js +1346 -496
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The diagram registry: one entry per supported diagram type.
3
+ *
4
+ * `diagramKind` and `render` both resolve through this table, so the header
5
+ * test each parser gates on and the one `diagramKind` reports are the same
6
+ * function by construction. Adding a diagram type is one module in
7
+ * `diagrams/` plus one entry here.
8
+ */
9
+ import { classDiagram } from './diagrams/class.js';
10
+ import { er } from './diagrams/er.js';
11
+ import { flowchart } from './diagrams/flowchart.js';
12
+ import { gitgraph } from './diagrams/gitgraph.js';
13
+ import { mindmap } from './diagrams/mindmap.js';
14
+ import { pie } from './diagrams/pie.js';
15
+ import { sequence } from './diagrams/sequence.js';
16
+ import { state } from './diagrams/state.js';
17
+ import { timeline } from './diagrams/timeline.js';
18
+ import { stripControls } from './labels.js';
19
+ import { headerKind, statementsOf } from './statements.js';
20
+ export const DIAGRAMS = [
21
+ flowchart,
22
+ state,
23
+ classDiagram,
24
+ er,
25
+ sequence,
26
+ pie,
27
+ mindmap,
28
+ timeline,
29
+ gitgraph,
30
+ ];
31
+ /** The registry entry `src`'s header declares, or `null`. */
32
+ export function diagramFor(src) {
33
+ const header = headerKind(statementsOf(src));
34
+ if (header === null)
35
+ return null;
36
+ return DIAGRAMS.find((d) => d.headers.includes(header)) ?? null;
37
+ }
38
+ /**
39
+ * The kind of diagram `src` declares, or `null` if its header names no type
40
+ * this renderer draws.
41
+ *
42
+ * Reads the header only — it says nothing about whether the body parses. Pair
43
+ * it with `render` to tell a source this renderer will never draw from one that
44
+ * is merely malformed:
45
+ *
46
+ * ```ts
47
+ * render(src) === null && diagramKind(src) !== null // syntax error
48
+ * ```
49
+ */
50
+ export function diagramKind(src) {
51
+ // The same strip `render` applies, so the two entry points agree on any src.
52
+ return diagramFor(stripControls(src))?.kind ?? null;
53
+ }
54
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The raw source in a framed box.
3
+ *
4
+ * What to show when `render` returns `null`, or returns art too wide for the
5
+ * space at hand. Both are the caller's call, so this is theirs to invoke — and
6
+ * theirs to caption, since only they know whether some other view of the
7
+ * diagram exists to point the reader at.
8
+ */
9
+ import { srcLines, stripControls } from './labels.js';
10
+ import { sat } from './layout.js';
11
+ import { measured, stringWidth } from './width.js';
12
+ /**
13
+ * Frame `src` in a titled box, hard-wrapping its lines to `maxWidth` columns.
14
+ *
15
+ * The result can still exceed `maxWidth`: the body wraps to
16
+ * `max(8, maxWidth - 4)` and the ` mermaid: <kind> ` title is never truncated,
17
+ * so a long first token sets a floor. Check `width` if it matters.
18
+ */
19
+ export function sourceBox(src, maxWidth) {
20
+ src = stripControls(src);
21
+ const header = src.split(/\s+/).filter((w) => w !== '')[0] ?? 'diagram';
22
+ const title = ` mermaid: ${header} `;
23
+ const limit = maxWidth === undefined ? undefined : Math.max(8, sat(maxWidth, 4));
24
+ const body = srcLines(src)
25
+ .map((l) => expandTabs(l).replace(/\s+$/, ''))
26
+ .reduce((acc, l) => {
27
+ if (!acc.started && l === '')
28
+ return acc;
29
+ acc.started = true;
30
+ acc.lines.push(...chunkLine(l, limit));
31
+ return acc;
32
+ }, { started: false, lines: [] }).lines;
33
+ // reduce, not a spread: a spread of stringWidths overflows the arg limit
34
+ // on very large sources.
35
+ const contentW = body.reduce((w, l) => Math.max(w, stringWidth(l)), stringWidth(title));
36
+ const inner = contentW + 2;
37
+ const plain = [];
38
+ const styled = [];
39
+ const rule = '─'.repeat(sat(inner, stringWidth(title)));
40
+ plain.push(`╭${title}${rule}╮`);
41
+ styled.push([
42
+ { text: '╭', role: 'border' },
43
+ { text: title, role: 'title' },
44
+ { text: `${rule}╮`, role: 'border' },
45
+ ]);
46
+ for (const line of body) {
47
+ const pad = ' '.repeat(sat(contentW, stringWidth(line)));
48
+ plain.push(`│ ${line}${pad} │`);
49
+ styled.push([
50
+ { text: '│ ', role: 'border' },
51
+ { text: line, role: 'text' },
52
+ { text: `${pad} │`, role: 'border' },
53
+ ]);
54
+ }
55
+ const bottom = `╰${'─'.repeat(inner)}╯`;
56
+ plain.push(bottom);
57
+ styled.push([{ text: bottom, role: 'border' }]);
58
+ return { plain, styled, width: inner + 2, classDefs: {}, warnings: [] };
59
+ }
60
+ /**
61
+ * Expand tabs to 4-column stops. A literal tab measures one column here but
62
+ * whatever the terminal's tab stops say there, so the frame would misalign.
63
+ */
64
+ function expandTabs(line) {
65
+ if (!line.includes('\t'))
66
+ return line;
67
+ let out = '';
68
+ let col = 0;
69
+ for (const [c, cw] of measured(line)) {
70
+ if (c === '\t') {
71
+ const pad = 4 - (col % 4);
72
+ out += ' '.repeat(pad);
73
+ col += pad;
74
+ }
75
+ else {
76
+ out += c;
77
+ col += cw;
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ /** Hard-break a line at `limit` columns, never splitting a wide glyph. */
83
+ function chunkLine(line, limit) {
84
+ if (limit === undefined || stringWidth(line) <= limit)
85
+ return [line];
86
+ const out = [];
87
+ let cur = '';
88
+ let curW = 0;
89
+ for (const [c, cw] of measured(line)) {
90
+ if (curW + cw > limit && cur !== '') {
91
+ out.push(cur);
92
+ cur = '';
93
+ curW = 0;
94
+ }
95
+ cur += c;
96
+ curW += cw;
97
+ }
98
+ if (cur !== '')
99
+ out.push(cur);
100
+ return out;
101
+ }
102
+ //# sourceMappingURL=source-box.js.map
@@ -0,0 +1,235 @@
1
+ /**
2
+ * The shared statement layer: source text to statements, plus the small
3
+ * string-reading helpers every grammar leans on.
4
+ */
5
+ import { asciiLower, srcLines } from './labels.js';
6
+ function flushStatement(cur, out) {
7
+ const trimmed = cur.trim();
8
+ if (trimmed !== '')
9
+ out.push(trimmed);
10
+ return '';
11
+ }
12
+ /**
13
+ * Split one source line into statements on `;`, stopping at a `%%` comment.
14
+ *
15
+ * Quoted spans are opaque, so a label may contain `;` and `%%`.
16
+ */
17
+ export function splitStatements(line, out) {
18
+ const chars = [...line];
19
+ let cur = '';
20
+ let inQuotes = false;
21
+ for (let i = 0; i < chars.length; i++) {
22
+ const c = chars[i];
23
+ if (inQuotes) {
24
+ if (c === '"')
25
+ inQuotes = false;
26
+ cur += c;
27
+ }
28
+ else if (c === '"') {
29
+ inQuotes = true;
30
+ cur += c;
31
+ }
32
+ else if (c === '%' && chars[i + 1] === '%') {
33
+ break;
34
+ }
35
+ else if (c === ';') {
36
+ cur = flushStatement(cur, out);
37
+ }
38
+ else {
39
+ cur += c;
40
+ }
41
+ }
42
+ flushStatement(cur, out);
43
+ }
44
+ /**
45
+ * Index just past a leading YAML frontmatter block (`---` … `---`), or 0 when
46
+ * there is none. While the block is still unterminated everything is
47
+ * frontmatter, so a streamed diagram stays blank until it closes.
48
+ */
49
+ export function frontmatterEnd(lines) {
50
+ let i = 0;
51
+ while (i < lines.length && lines[i].trim() === '')
52
+ i++;
53
+ if (lines[i]?.trim() !== '---')
54
+ return 0;
55
+ i++;
56
+ while (i < lines.length && lines[i].trim() !== '---')
57
+ i++;
58
+ return i + 1;
59
+ }
60
+ /**
61
+ * All statements in a source block, in order. A leading YAML frontmatter
62
+ * block, part of the mermaid grammar since v10, is skipped.
63
+ */
64
+ export function statementsOf(src) {
65
+ const lines = srcLines(src);
66
+ const out = [];
67
+ for (const line of lines.slice(frontmatterEnd(lines)))
68
+ splitStatements(line, out);
69
+ return out;
70
+ }
71
+ /**
72
+ * The `title:` of a leading frontmatter block, or null. The one frontmatter
73
+ * key with terminal meaning — `config` and friends style mermaid's own
74
+ * renderers and are deliberately ignored.
75
+ */
76
+ export function frontmatterTitle(src) {
77
+ const lines = srcLines(src);
78
+ const end = frontmatterEnd(lines);
79
+ for (const line of lines.slice(0, end)) {
80
+ const kv = splitOnce(line, ':');
81
+ // Untrimmed on the left: an indented `title:` is nested under some other
82
+ // key, not the diagram's.
83
+ if (kv === null || kv[0].trimEnd() !== 'title')
84
+ continue;
85
+ const t = kv[1].trim();
86
+ const quoted = t.length > 1 &&
87
+ ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'")));
88
+ const title = (quoted ? t.slice(1, -1) : t).trim();
89
+ return title === '' ? null : title;
90
+ }
91
+ return null;
92
+ }
93
+ /** Per-char flags: 1 where the char lies inside a double-quoted span (quotes included). */
94
+ export function quoteMask(chars) {
95
+ const mask = new Uint8Array(chars.length);
96
+ let inQuotes = false;
97
+ for (let i = 0; i < chars.length; i++) {
98
+ if (chars[i] === '"') {
99
+ mask[i] = 1;
100
+ inQuotes = !inQuotes;
101
+ }
102
+ else if (inQuotes) {
103
+ mask[i] = 1;
104
+ }
105
+ }
106
+ return mask;
107
+ }
108
+ /**
109
+ * Split on separator chars sitting outside double quotes and parentheses,
110
+ * dropping empty segments — the one splitter every grammar shares, so quote
111
+ * rules cannot drift between them.
112
+ */
113
+ export function splitTop(s, isSep) {
114
+ const out = [];
115
+ let cur = '';
116
+ let inQuotes = false;
117
+ let depth = 0;
118
+ for (const c of s) {
119
+ if (c === '"')
120
+ inQuotes = !inQuotes;
121
+ else if (!inQuotes && c === '(')
122
+ depth++;
123
+ else if (!inQuotes && c === ')' && depth > 0)
124
+ depth--;
125
+ if (!inQuotes && depth === 0 && isSep(c)) {
126
+ if (cur !== '')
127
+ out.push(cur);
128
+ cur = '';
129
+ }
130
+ else {
131
+ cur += c;
132
+ }
133
+ }
134
+ if (cur !== '')
135
+ out.push(cur);
136
+ return out;
137
+ }
138
+ /**
139
+ * Split `head : rest` at the first label colon, skipping `:::` tag runs so
140
+ * `A:::hot : desc` keeps its tag with the id. `null` when there is no colon.
141
+ */
142
+ export function splitColon(s) {
143
+ const chars = [...s];
144
+ for (let i = 0; i < chars.length; i++) {
145
+ if (chars[i] !== ':')
146
+ continue;
147
+ let run = i;
148
+ while (run < chars.length && chars[run] === ':')
149
+ run++;
150
+ if (run - i >= 3) {
151
+ i = run - 1;
152
+ continue;
153
+ }
154
+ return [chars.slice(0, i).join(''), chars.slice(i + 1).join('')];
155
+ }
156
+ return null;
157
+ }
158
+ /** Strip trailing `:::name` tags from an id token: `A:::hot` → id `A`, classes `[hot]`. */
159
+ export function takeTags(token) {
160
+ const parts = token.split(':::');
161
+ if (parts.length === 1 || parts[0] === '')
162
+ return { id: token, classes: [] };
163
+ return { id: parts[0], classes: parts.slice(1).filter((c) => c !== '') };
164
+ }
165
+ /**
166
+ * The body of a `class A,B name` statement → `[ids, names]`. The last
167
+ * whitespace-separated token is the name list, everything before it the ids —
168
+ * so a space after a comma (`class A, B warn`) still reads as two ids.
169
+ */
170
+ export function parseClassAssign(rest) {
171
+ const body = rest.trim();
172
+ const ws = body.search(/\s\S*$/);
173
+ if (ws === -1)
174
+ return null;
175
+ const split = (s) => s
176
+ .split(',')
177
+ .map((t) => t.trim())
178
+ .filter((t) => t !== '');
179
+ return [split(body.slice(0, ws)), split(body.slice(ws))];
180
+ }
181
+ /**
182
+ * `A "url" [tooltip]` / `A href "url" …` → `[id, url]`. The callback forms
183
+ * (`call`/`callback`) return null — their quoted string is a tooltip.
184
+ */
185
+ export function parseHref(rest) {
186
+ const [id, second] = words(rest);
187
+ if (id === undefined || second === 'call' || second === 'callback')
188
+ return null;
189
+ const url = rest.match(/"([^"]+)"/);
190
+ return url === null ? null : [id, url[1]];
191
+ }
192
+ export const firstWord = (s) => s.split(/\s+/).filter((w) => w !== '')[0] ?? '';
193
+ export const words = (s) => s.split(/\s+/).filter((w) => w !== '');
194
+ /** Split on the first occurrence of `sep`, Rust's `split_once`. */
195
+ export function splitOnce(s, sep) {
196
+ const i = s.indexOf(sep);
197
+ return i === -1 ? null : [s.slice(0, i), s.slice(i + sep.length)];
198
+ }
199
+ export const nonEmpty = (s) => (s === '' ? null : s);
200
+ /** Diagram kind from the header statement, lowercased. */
201
+ export function headerKind(statements) {
202
+ const header = statements[0];
203
+ if (header === undefined)
204
+ return null;
205
+ const kind = firstWord(header);
206
+ return kind === '' ? null : asciiLower(kind);
207
+ }
208
+ /**
209
+ * Parse the body of a `classDef` statement: `name[,name2] k1:v1,k2:v2`.
210
+ * Values are kept verbatim; malformed pairs are skipped.
211
+ */
212
+ export function parseClassDef(rest) {
213
+ const body = rest.trim();
214
+ const ws = body.search(/\s/);
215
+ if (ws === -1)
216
+ return null;
217
+ const names = body
218
+ .slice(0, ws)
219
+ .split(',')
220
+ .map((s) => s.trim())
221
+ .filter((s) => s !== '');
222
+ const props = {};
223
+ // splitTop keeps `rgb(255,0,0)` whole; a bare comma still separates pairs.
224
+ for (const pair of splitTop(body.slice(ws).trim(), (c) => c === ',')) {
225
+ const kv = splitOnce(pair, ':');
226
+ if (kv === null)
227
+ continue;
228
+ const k = kv[0].trim();
229
+ const v = kv[1].trim();
230
+ if (k !== '' && v !== '')
231
+ props[k] = v;
232
+ }
233
+ return names.length === 0 ? null : { names, props };
234
+ }
235
+ //# sourceMappingURL=statements.js.map
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Display width for the diagram layout.
3
+ *
4
+ * The diagram engine measures a box in columns so its border lands on the right
5
+ * cell. `src/term.js` already answers that question for the rest of the UI, with
6
+ * the same grapheme clustering, so this module delegates rather than carrying a
7
+ * second Unicode table (the upstream one is 25 KB).
8
+ */
9
+ import { visualWidth } from '../term.js';
10
+ /** Columns occupied by one grapheme cluster. */
11
+ export function clusterWidth(cluster) {
12
+ return visualWidth(cluster);
13
+ }
14
+
15
+ /**
16
+ * Iterate clusters paired with their display width.
17
+ *
18
+ * Callers paint from this rather than by code point: a CJK character claims two
19
+ * cells of layout but contributes one character of output.
20
+ */
21
+ export function* measured(s) {
22
+ const seg = new Intl.Segmenter('en', { granularity: 'grapheme' });
23
+ for (const { segment } of seg.segment(s)) yield [segment, visualWidth(segment)];
24
+ }
25
+
26
+ /** Display columns of a string. */
27
+ export function stringWidth(s) {
28
+ return visualWidth(s);
29
+ }