@hncode/hncode 0.5.6 → 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 -502
package/src/tex/svg.js ADDED
@@ -0,0 +1,237 @@
1
+ /**
2
+ * TeX → SVG.
3
+ *
4
+ * Every glyph is a `<path>` built from the actual STIX Two Math outline, so the document is vector art in
5
+ * the same sense MathJax's SVG output is: no text elements, no font references, nothing that depends on what
6
+ * the viewer has installed. Rules (fraction bars, radical bars) are `<rect>`, and the delimiters a matrix or
7
+ * `cases` needs are the font's own bracket and brace glyphs stretched to the height they must span.
8
+ *
9
+ * The layout is not this module's business: `layout.js` has already measured the formula in font units, so
10
+ * this only has to place what it is given. That is deliberate — the terminal bitmap and the SVG come out of
11
+ * ONE layout, so a picture on screen and an SVG exported from it cannot disagree.
12
+ */
13
+ import { loadBundledFont } from './font.js';
14
+ import { parseTex } from './parse.js';
15
+ import { layout } from './layout.js';
16
+
17
+ /** Margin around the ink, in em fractions. Bracket arms sit on the box edge, so this cannot be zero. */
18
+ const PAD_EM = 0.16;
19
+ /** Stroke width for a rule (fraction bar, radical bar), in em fractions. */
20
+ const RULE_EM = 0.045;
21
+ /** Editor/version tag the SVG carries; also what makes the output reproducible byte for byte. */
22
+ const GENERATOR = 'hncode tex-svg';
23
+
24
+ /** Two decimals is more precision than a pixel ever shows, and it keeps the file readable. */
25
+ const n = (v) => {
26
+ const r = Math.round(v * 100) / 100;
27
+ return Object.is(r, -0) ? '0' : String(r);
28
+ };
29
+
30
+ let fontCache = null;
31
+
32
+ function font() {
33
+ if (!fontCache) fontCache = loadBundledFont();
34
+ return fontCache;
35
+ }
36
+
37
+ /**
38
+ * One glyph outline as an SVG path, with the font's up-is-positive y flipped to SVG's down-is-positive.
39
+ *
40
+ * TrueType contours mix on-curve and off-curve points; two off-curve points in a row imply an on-curve point
41
+ * halfway between them, which is why the midpoint case has to be emitted explicitly rather than skipped.
42
+ *
43
+ * @param {Array<Array<{x:number,y:number,on:boolean}>>} contours
44
+ * @param {(x:number)=>number} tx
45
+ * @param {(y:number)=>number} ty
46
+ * @returns {string}
47
+ */
48
+ export function outlineToPath(contours, tx, ty) {
49
+ const parts = [];
50
+ for (const contour of contours) {
51
+ const count = contour.length;
52
+ if (count < 2) continue;
53
+
54
+ // A contour need not begin on a curve point. When it does not, it begins at the midpoint between the
55
+ // first two off-curve points, which is where the closing segment has to end too.
56
+ let startAt = contour.findIndex((p) => p.on);
57
+ let start;
58
+ if (startAt < 0) {
59
+ start = { x: (contour[0].x + contour[1].x) / 2, y: (contour[0].y + contour[1].y) / 2 };
60
+ startAt = 0;
61
+ } else {
62
+ start = contour[startAt];
63
+ }
64
+
65
+ parts.push(`M${n(tx(start.x))} ${n(ty(start.y))}`);
66
+ let control = null;
67
+ for (let k = 1; k <= count; k++) {
68
+ const point = contour[(startAt + k) % count];
69
+ if (point.on) {
70
+ if (control) {
71
+ parts.push(`Q${n(tx(control.x))} ${n(ty(control.y))} ${n(tx(point.x))} ${n(ty(point.y))}`);
72
+ control = null;
73
+ } else {
74
+ parts.push(`L${n(tx(point.x))} ${n(ty(point.y))}`);
75
+ }
76
+ } else if (control) {
77
+ const midX = (control.x + point.x) / 2;
78
+ const midY = (control.y + point.y) / 2;
79
+ parts.push(`Q${n(tx(control.x))} ${n(ty(control.y))} ${n(tx(midX))} ${n(ty(midY))}`);
80
+ control = point;
81
+ } else {
82
+ control = point;
83
+ }
84
+ }
85
+ if (control) parts.push(`Q${n(tx(control.x))} ${n(ty(control.y))} ${n(tx(start.x))} ${n(ty(start.y))}`);
86
+ parts.push('Z');
87
+ }
88
+ return parts.join('');
89
+ }
90
+
91
+ /**
92
+ * A delimiter drawn from the font's own glyph, stretched vertically to the height it must span.
93
+ *
94
+ * A matrix's bracket is taller than any single glyph the font ships at text size, and the extensible pieces
95
+ * in the MATH table are out of scope here. Stretching the base glyph is what a fallback typesetter does, and
96
+ * it reads correctly because the arms keep their shape and only the straight run grows.
97
+ *
98
+ * @param {string} ch The delimiter character.
99
+ * @param {{x:number,y:number,width:number,height:number,depth:number}} box Where it has to land, in pixels.
100
+ * @param {number} px Em size in pixels.
101
+ */
102
+ function stretchedDelimiter(ch, box, px) {
103
+ const face = font();
104
+ const gid = face.glyphIndex(ch.codePointAt(0));
105
+ if (!gid) return '';
106
+ const contours = face.outline(gid);
107
+ if (!contours.length) return '';
108
+
109
+ // The glyph's own ink box, so the stretch is measured against what it actually draws.
110
+ let left = Infinity;
111
+ let right = -Infinity;
112
+ let top = -Infinity;
113
+ let bottom = Infinity;
114
+ for (const contour of contours) {
115
+ for (const p of contour) {
116
+ if (p.x < left) left = p.x;
117
+ if (p.x > right) right = p.x;
118
+ if (p.y > top) top = p.y;
119
+ if (p.y < bottom) bottom = p.y;
120
+ }
121
+ }
122
+ if (!(right > left) || !(top > bottom)) return '';
123
+
124
+ const inkWidth = (right - left) / face.unitsPerEm * px;
125
+ const inkHeight = (top - bottom) / face.unitsPerEm * px;
126
+ // Fit inside the box the layout reserved, keeping the glyph's own width when there is room.
127
+ const scaleX = (box.width || inkWidth) / inkWidth;
128
+ const scaleY = (box.height + box.depth) / inkHeight;
129
+ const originX = box.x;
130
+ const originY = box.y + box.height; // the top of the box, in SVG coordinates
131
+
132
+ // y is flipped for SVG, so the glyph's ink bottom maps to the BOTTOM of the box.
133
+ const tx = (x) => originX + (x - left) * scaleX;
134
+ const ty = (y) => originY + (top - y) * scaleY;
135
+ return outlineToPath(contours, tx, ty);
136
+ }
137
+
138
+ /** Run the same stretch over the outermost style the SVG root carries. */
139
+ /**
140
+ * The SVG document for one formula.
141
+ *
142
+ * @param {string} tex TeX without delimiters.
143
+ * @param {object} [opts]
144
+ * @param {number} [opts.px] Em size in pixels. Defaults to 16, a browser body-text size.
145
+ * @param {string} [opts.colour] Ink colour as a CSS colour. Defaults to `currentColor`, so the document
146
+ * inherits the colour of whatever displays it.
147
+ * @returns {{svg: string, width: number, height: number, baseline: number}|null} Null when the TeX is outside
148
+ * the subset this project can lay out.
149
+ */
150
+ export function texToSvg(tex, opts = {}) {
151
+ let box;
152
+ try {
153
+ box = layout(parseTex(tex), font(), 1000);
154
+ } catch {
155
+ return null;
156
+ }
157
+ if (!(box.width > 0) || !(box.height + box.depth > 0)) return null;
158
+
159
+ const face = font();
160
+ const px = Math.max(6, opts.px || 16);
161
+ const color = opts.colour || 'currentColor';
162
+ const pad = Math.round(px * PAD_EM);
163
+
164
+ // Em unit → pixel, and the baseline row the whole tree hangs from.
165
+ const em = px / 1000;
166
+ const width = Math.ceil(box.width * em) + pad * 2;
167
+ const height = Math.ceil((box.height + box.depth) * em) + pad * 2;
168
+ const baseline = Math.ceil(box.height * em) + pad;
169
+ const stroke = Math.max(0.6, px * RULE_EM);
170
+
171
+ const paths = [];
172
+ const rects = [];
173
+
174
+ const walk = (node, ox, oy) => {
175
+ if (!node) return;
176
+ const x = ox + (node.x || 0) * em;
177
+ const y = oy - (node.y || 0) * em; // font y grows up, SVG y grows down
178
+
179
+ if (node.kind === 'glyph' && node.gid) {
180
+ const contours = face.outline(node.gid);
181
+ if (contours.length) {
182
+ const k = (node.size * em) / face.unitsPerEm;
183
+ const d = outlineToPath(contours, (gx) => x + gx * k, (gy) => y - gy * k);
184
+ if (d) paths.push(d);
185
+ }
186
+ } else if (node.kind === 'rule') {
187
+ rects.push({
188
+ x,
189
+ y,
190
+ width: Math.max(0.5, node.width * em),
191
+ height: stroke,
192
+ });
193
+ } else if (node.kind === 'bracket' || node.kind === 'brace') {
194
+ // The layout reserves the box; the font supplies the shape.
195
+ const ch = node.kind === 'brace' ? '{' : (node.side === 'right' ? ']' : '[');
196
+ const d = stretchedDelimiter(ch, {
197
+ x,
198
+ y,
199
+ width: (node.width || px * 0.3) * em,
200
+ height: node.height * em,
201
+ depth: node.depth * em,
202
+ }, px);
203
+ if (d) paths.push(d);
204
+ }
205
+
206
+ if (node.items) for (const child of node.items) walk(child, x, y);
207
+ };
208
+ walk(box, pad, baseline);
209
+
210
+ const body = [
211
+ `<g fill="${color}" stroke="none">`,
212
+ ...rects.map((r) => `<rect x="${n(r.x)}" y="${n(r.y)}" width="${n(r.width)}" height="${n(r.height)}"/>`),
213
+ ...paths.map((d) => `<path d="${d}"/>`),
214
+ '</g>',
215
+ ].join('\n');
216
+
217
+ const svg = [
218
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"`
219
+ + ` viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeAttr(tex.trim())}">`,
220
+ `<title>${escapeText(tex.trim())}</title>`,
221
+ `<desc>${GENERATOR}: STIX Two Math glyph outlines</desc>`,
222
+ body,
223
+ '</svg>',
224
+ ].join('\n');
225
+
226
+ return { svg, width, height, baseline };
227
+ }
228
+
229
+ /** Text content: the three characters that end an element or start a reference. */
230
+ function escapeText(s) {
231
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
232
+ }
233
+
234
+ /** Attribute content: the same plus the quote that delimits it. */
235
+ function escapeAttr(s) {
236
+ return escapeText(s).replace(/"/g, '&quot;');
237
+ }
@@ -107,7 +107,7 @@ export function spillNotice(spilled) {
107
107
  `Preview (first ${fmtBytes(PREVIEW_BYTES)}):`,
108
108
  spilled.preview,
109
109
  '',
110
- `Read the file above to see the rest — use line_offset / n_lines to page through it.`,
110
+ `Read the file above to see the rest — use start_line / end_line to page through it.`,
111
111
  ].join('\n');
112
112
  }
113
113
 
@@ -262,7 +262,7 @@ export function applyTrim(messages, plan, sessionId) {
262
262
  m.content = spilled
263
263
  ? `[Output elided to save context — ${fmtBytes(original.length)} from ${tool}.\n`
264
264
  + `Full text saved to: ${spilled.file}\n`
265
- + `Read that file (with line_offset/n_lines) if you need this output again.]`
265
+ + `Read that file (with start_line/end_line) if you need this output again.]`
266
266
  : `[Output elided to save context — ${fmtBytes(original.length)} from ${tool}; the full text could not be saved.]`;
267
267
  out.elided++;
268
268
  out.elidedBytes += original.length;
@@ -92,23 +92,11 @@ export function planSwarm(args) {
92
92
 
93
93
  export const spec = {
94
94
  name: 'AgentSwarm',
95
- description: `Launch multiple subagents from one prompt template, existing agent resumes, or both.
95
+ description: `Run the same task over many inputs, one subagent each, in parallel. Only exists while swarm mode is on (/swarm).
96
96
 
97
- Use AgentSwarm when many subagents should run the same kind of task over different inputs. The placeholder is exactly \`${PROMPT_TEMPLATE_PLACEHOLDER}\`. For example, with \`prompt_template\` set to \`Review ${PROMPT_TEMPLATE_PLACEHOLDER} for likely regressions.\` and \`items\` set to \`["src/a.ts", "src/b.ts"]\`, AgentSwarm launches two new subagents with those two concrete prompts. For a few differently-shaped tasks, make separate \`Agent\` calls in one message instead.
97
+ Give \`prompt_template\` containing \`${PROMPT_TEMPLATE_PLACEHOLDER}\` and \`items\`; each item replaces the placeholder in one prompt. At least 2 items are required unless you pass \`resume_agent_ids\`, and the filled-in prompts must differ. Use \`resume_agent_ids\` (id → prompt, usually "continue") to revive subagents from earlier work; it can be combined with \`items\`. Up to ${MAX_AGENT_SWARM_SUBAGENTS}; launches queue themselves.
98
98
 
99
- Use \`resume_agent_ids\` to continue subagents that already exist from earlier work, such as ones that failed or timed out: map each agent id to the prompt for that resumed subagent (usually \`continue\` if no extra information is needed). You may combine \`resume_agent_ids\` with \`items\` in the same call. Do not duplicate resumed work in \`items\`.
100
-
101
- Each of these is enforced — a violation is rejected before any subagent starts: provide at least 2 \`items\` unless you pass \`resume_agent_ids\`; whenever \`items\` are present, \`prompt_template\` is required and must contain \`${PROMPT_TEMPLATE_PLACEHOLDER}\`; and the filled-in prompts must be distinct.
102
-
103
- Use enough subagents to keep the work focused and parallel. AgentSwarm supports up to ${MAX_AGENT_SWARM_SUBAGENTS} subagents, and launches are queued automatically, so it is safe to split large tasks into many clear, independent items.
104
-
105
- When NOT to use AgentSwarm:
106
- - It only exists while swarm mode is ON (\`/swarm\`). With the mode off the tool is not offered at all and calling it is refused. If a fan-out would genuinely help, say so and ask the user to turn the mode on.
107
- - Never call this on your own initiative. It requires the same explicit permission as \`Agent\`: the user, or the applicable AGENTS.md / project instructions, must have asked for sub-agents, delegation, or parallel agent work. Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do NOT count as permission.
108
- - Do not use it for two or three differently-shaped tasks: use separate \`Agent\` calls instead. Note that several \`Agent\` calls in one message run ONE AT A TIME — only read-only tools batch — so a swarm is the only way to overlap subagents, and a 3-item swarm really does run 3 at once.
109
- - Do not use it to fan out work you could finish yourself in a few steps.
110
-
111
- If \`AgentSwarm\` is called, that call must be the only tool call in the response.`,
99
+ For two or three differently-shaped tasks use separate \`Agent\` calls instead — though several Agent calls in one message run one at a time, so a swarm is the only way to overlap three subagents at once. Never start one on your own initiative: it needs the same explicit permission as \`Agent\`. If this is called, it must be the ONLY tool call in the response.`,
112
100
  parameters: {
113
101
  type: 'object',
114
102
  properties: {