Closes the three rendering gaps in the transcript pane. Every step is there because Vicinae cannot do that part alone; the reasoning is in the new ## Rendering section of the README.
LaTeX (src/lib/latex.ts)
Vicinae maps cmark nodes to a fixed set of block components and there is no maths block among them, so $x^2$ reached the pane as literal TeX.
A paragraph is a TextEdit with textFormat: RichText (MdParagraph.qml) and markdown-model.cpp passes CMARK_NODE_HTML_INLINE through untouched, so sup, sub, b and i are available. The pane is also a launcher-width column, so this is a linearisation rather than a typesetter: \frac{a}{b} becomes a/b, \sqrt{x+1} becomes √(x+1), \sum_{i=1}^{n} becomes ∑ᵢ₌₁ⁿ, and a tag is used only where Unicode has no form (x^{a/b} becomes x<sup>a/b</sup>).
$...$, \(...\), $$...$$ and \[...\] on a line of their own, and a fence labelled math, are all read. A lone $ pair counts only when the inside looks like maths, so $5 and $10 and cd $A; echo $B are untouched, and an opening $$ with no closer is only a formula when what it swallowed reads as one.
Custom HTML (src/lib/html.ts)
Raw inline HTML is the only route past GFM, and it is also a hole. The tags and style declarations the renderer acts on are kept; script, style and the other element types whose body is not content are removed with it; an event handler, a url() in a style value and a javascript: destination are dropped; and a tag that is not on the list is shown as text rather than swallowed silently. An <a href> becomes a markdown link, the only form Vicinae makes clickable, and an <img> keeps its tag with its source judged instead, because the block extractor is the one place an image written as HTML is loaded for real.
A block of HTML is also moved into a paragraph. processHtmlNodes in markdown-model.cpp is not a renderer: it pulls out <img> and keeps the trimmed text of everything else, so <table><tr><td>a</td><td>b</td></tr></table> arrives as ab. A well-formed block is joined onto one line and wrapped in a <span>, which is not on CommonMark's list of block tags, so it arrives as the paragraph whose raw HTML is passed through. Markup that does not balance and a block holding an <img> are left alone.
Everything else (src/lib/code.ts)
A paragraph drawn with box characters is fenced, because a paragraph wraps and a reflowed diagram says nothing. A real GFM table is left to the table renderer, one stray pipe in prose is not art, and an indented block is already code.
A fence label KSyntaxHighlighting would print but never resolve is renamed (console, terminal and friends become bash), and one that carries no information (text, plaintext) is dropped.
Wiring
renderModelOutput runs the whole pass in one place, in render.ts: the fence repair first because it decides where the fences are, maths next, HTML last so the tags the maths pass adds go through the same allowlist as the ones the model wrote. A question goes through the maths and markup passes without the fence repair, since that repair is for model output.
The default system prompt now says maths and a small amount of HTML are drawn, and that a diagram belongs in a fence.
Verification
npm run typecheck, npm run lint:code and npm test all pass; 578 tests, 66 of them new across latex.test.ts, html.test.ts, render.test.ts and code.test.ts.
Closes the three rendering gaps in the transcript pane. Every step is there because Vicinae cannot do that part alone; the reasoning is in the new `## Rendering` section of the README.
### LaTeX (`src/lib/latex.ts`)
Vicinae maps cmark nodes to a fixed set of block components and there is no maths block among them, so `$x^2$` reached the pane as literal TeX.
A paragraph is a `TextEdit` with `textFormat: RichText` (`MdParagraph.qml`) and `markdown-model.cpp` passes `CMARK_NODE_HTML_INLINE` through untouched, so `sup`, `sub`, `b` and `i` are available. The pane is also a launcher-width column, so this is a linearisation rather than a typesetter: `\frac{a}{b}` becomes `a/b`, `\sqrt{x+1}` becomes `√(x+1)`, `\sum_{i=1}^{n}` becomes `∑ᵢ₌₁ⁿ`, and a tag is used only where Unicode has no form (`x^{a/b}` becomes `x<sup>a/b</sup>`).
`$...$`, `\(...\)`, `$$...$$` and `\[...\]` on a line of their own, and a fence labelled `math`, are all read. A lone `$` pair counts only when the inside looks like maths, so `$5 and $10` and `cd $A; echo $B` are untouched, and an opening `$$` with no closer is only a formula when what it swallowed reads as one.
### Custom HTML (`src/lib/html.ts`)
Raw inline HTML is the only route past GFM, and it is also a hole. The tags and style declarations the renderer acts on are kept; `script`, `style` and the other element types whose body is not content are removed with it; an event handler, a `url()` in a style value and a `javascript:` destination are dropped; and a tag that is not on the list is shown as text rather than swallowed silently. An `<a href>` becomes a markdown link, the only form Vicinae makes clickable, and an `<img>` keeps its tag with its source judged instead, because the block extractor is the one place an image written as HTML is loaded for real.
A block of HTML is also moved into a paragraph. `processHtmlNodes` in `markdown-model.cpp` is not a renderer: it pulls out `<img>` and keeps the trimmed text of everything else, so `<table><tr><td>a</td><td>b</td></tr></table>` arrives as `ab`. A well-formed block is joined onto one line and wrapped in a `<span>`, which is not on CommonMark's list of block tags, so it arrives as the paragraph whose raw HTML is passed through. Markup that does not balance and a block holding an `<img>` are left alone.
### Everything else (`src/lib/code.ts`)
- A paragraph drawn with box characters is fenced, because a paragraph wraps and a reflowed diagram says nothing. A real GFM table is left to the table renderer, one stray pipe in prose is not art, and an indented block is already code.
- A fence label KSyntaxHighlighting would print but never resolve is renamed (`console`, `terminal` and friends become `bash`), and one that carries no information (`text`, `plaintext`) is dropped.
### Wiring
`renderModelOutput` runs the whole pass in one place, in `render.ts`: the fence repair first because it decides where the fences are, maths next, HTML last so the tags the maths pass adds go through the same allowlist as the ones the model wrote. A question goes through the maths and markup passes without the fence repair, since that repair is for model output.
The default system prompt now says maths and a small amount of HTML are drawn, and that a diagram belongs in a fence.
### Verification
`npm run typecheck`, `npm run lint:code` and `npm test` all pass; 578 tests, 66 of them new across `latex.test.ts`, `html.test.ts`, `render.test.ts` and `code.test.ts`.
The transcript rendered whatever the model wrote as markdown, and three
things a model writes are not markdown the renderer draws.
LaTeX. Vicinae has no maths block, so `$x^2$` reached the pane as
literal TeX. `latex.ts` converts the subset models actually write into
inline markup the pane can draw: `sup`, `sub`, `b` and `i` are available
because `MdParagraph.qml` is a rich text `TextEdit` and
`markdown-model.cpp` passes `CMARK_NODE_HTML_INLINE` through untouched.
It is a linearisation rather than a typesetter, because the pane is a
launcher-width column: `\frac{a}{b}` becomes `a/b` and `\sqrt{x+1}`
becomes `√(x+1)`, with a Unicode script wherever one exists. A lone `$`
pair only counts when the inside looks like maths, so `$5 and $10` and
`cd $A; echo $B` are left as written.
Custom HTML. Raw HTML is the only route past GFM, and it is also a hole,
so `html.ts` filters it: the tags and style declarations the renderer
acts on are kept, `script` and `style` go with their content, an event
handler, a `url()` in a style value and a `javascript:` destination are
dropped, and a tag that is not on the list is shown as text rather than
swallowed. An `<a href>` becomes a markdown link, the only form Vicinae
makes clickable. A well-formed HTML block is moved into a paragraph,
because the block path keeps only the words of one.
Everything else. `code.ts` now fences a paragraph drawn with box
characters, since a paragraph wraps and a diagram that reflows says
nothing, and renames a fence label KSyntaxHighlighting would print but
never resolve.
The pass lives in `render.ts`, is plain TypeScript with no Vicinae
imports, and the default system prompt now says what the window draws.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes the three rendering gaps in the transcript pane. Every step is there because Vicinae cannot do that part alone; the reasoning is in the new
## Renderingsection of the README.LaTeX (
src/lib/latex.ts)Vicinae maps cmark nodes to a fixed set of block components and there is no maths block among them, so
$x^2$reached the pane as literal TeX.A paragraph is a
TextEditwithtextFormat: RichText(MdParagraph.qml) andmarkdown-model.cpppassesCMARK_NODE_HTML_INLINEthrough untouched, sosup,sub,bandiare available. The pane is also a launcher-width column, so this is a linearisation rather than a typesetter:\frac{a}{b}becomesa/b,\sqrt{x+1}becomes√(x+1),\sum_{i=1}^{n}becomes∑ᵢ₌₁ⁿ, and a tag is used only where Unicode has no form (x^{a/b}becomesx<sup>a/b</sup>).$...$,\(...\),$$...$$and\[...\]on a line of their own, and a fence labelledmath, are all read. A lone$pair counts only when the inside looks like maths, so$5 and $10andcd $A; echo $Bare untouched, and an opening$$with no closer is only a formula when what it swallowed reads as one.Custom HTML (
src/lib/html.ts)Raw inline HTML is the only route past GFM, and it is also a hole. The tags and style declarations the renderer acts on are kept;
script,styleand the other element types whose body is not content are removed with it; an event handler, aurl()in a style value and ajavascript:destination are dropped; and a tag that is not on the list is shown as text rather than swallowed silently. An<a href>becomes a markdown link, the only form Vicinae makes clickable, and an<img>keeps its tag with its source judged instead, because the block extractor is the one place an image written as HTML is loaded for real.A block of HTML is also moved into a paragraph.
processHtmlNodesinmarkdown-model.cppis not a renderer: it pulls out<img>and keeps the trimmed text of everything else, so<table><tr><td>a</td><td>b</td></tr></table>arrives asab. A well-formed block is joined onto one line and wrapped in a<span>, which is not on CommonMark's list of block tags, so it arrives as the paragraph whose raw HTML is passed through. Markup that does not balance and a block holding an<img>are left alone.Everything else (
src/lib/code.ts)console,terminaland friends becomebash), and one that carries no information (text,plaintext) is dropped.Wiring
renderModelOutputruns the whole pass in one place, inrender.ts: the fence repair first because it decides where the fences are, maths next, HTML last so the tags the maths pass adds go through the same allowlist as the ones the model wrote. A question goes through the maths and markup passes without the fence repair, since that repair is for model output.The default system prompt now says maths and a small amount of HTML are drawn, and that a diagram belongs in a fence.
Verification
npm run typecheck,npm run lint:codeandnpm testall pass; 578 tests, 66 of them new acrosslatex.test.ts,html.test.ts,render.test.tsandcode.test.ts.The transcript rendered whatever the model wrote as markdown, and three things a model writes are not markdown the renderer draws. LaTeX. Vicinae has no maths block, so `$x^2$` reached the pane as literal TeX. `latex.ts` converts the subset models actually write into inline markup the pane can draw: `sup`, `sub`, `b` and `i` are available because `MdParagraph.qml` is a rich text `TextEdit` and `markdown-model.cpp` passes `CMARK_NODE_HTML_INLINE` through untouched. It is a linearisation rather than a typesetter, because the pane is a launcher-width column: `\frac{a}{b}` becomes `a/b` and `\sqrt{x+1}` becomes `√(x+1)`, with a Unicode script wherever one exists. A lone `$` pair only counts when the inside looks like maths, so `$5 and $10` and `cd $A; echo $B` are left as written. Custom HTML. Raw HTML is the only route past GFM, and it is also a hole, so `html.ts` filters it: the tags and style declarations the renderer acts on are kept, `script` and `style` go with their content, an event handler, a `url()` in a style value and a `javascript:` destination are dropped, and a tag that is not on the list is shown as text rather than swallowed. An `<a href>` becomes a markdown link, the only form Vicinae makes clickable. A well-formed HTML block is moved into a paragraph, because the block path keeps only the words of one. Everything else. `code.ts` now fences a paragraph drawn with box characters, since a paragraph wraps and a diagram that reflows says nothing, and renames a fence label KSyntaxHighlighting would print but never resolve. The pass lives in `render.ts`, is plain TypeScript with no Vicinae imports, and the default system prompt now says what the window draws.