nexu-io/open-design · error · Error
unterminated tag in live artifact template
Error message
unterminated tag in live artifact template
What it means
Thrown by findTagEnd() while scanning an HTML tag opening in a live artifact template (html_template_v1). The scanner walks character-by-character from the '<' at `start`, tracking single/double quote state, and expects to find a '>' that closes the opening tag. It throws when the string ends first — either because the tag literally never closes, or because an attribute's quote is never balanced so every subsequent '>' is swallowed as attribute content. This is a hard parse failure: the renderer cannot locate the element boundary, so it refuses to emit anything.
Source
Thrown at apps/daemon/src/live-artifacts/render.ts:118
}
function interpolateScalars(fragment: string, resolve: BindingResolver): string {
return fragment.replace(TEMPLATE_INTERPOLATION, (_match, rawBinding: string) => resolve(rawBinding.trim()));
}
/** Index of the `>` that closes the tag opening at `start`, respecting quotes. */
function findTagEnd(html: string, start: number): number {
let quote: string | null = null;
for (let i = start; i < html.length; i++) {
const ch = html[i];
if (quote) {
if (ch === quote) quote = null;
continue;
}
if (ch === '"' || ch === "'") quote = ch;
else if (ch === '>') return i;
}
throw new Error('unterminated tag in live artifact template');
}
/** Index just past the `</tagName>` that matches the element opened at `openTagEnd`. */
/**
* Whether `index` falls inside an HTML comment. Comment text is authored
* content: neither repeat directives nor same-tag tokens inside `<!-- -->`
* count as structure anywhere the renderer scans. Returns the position just
* past the comment so scanners can resume after it.
*/
function insideComment(html: string, index: number): { resumeAt: number } | null {
const start = html.lastIndexOf('<!--', index);
if (start === -1) return null;
const end = html.indexOf('-->', start);
if (end !== -1 && end < index) return null;
return { resumeAt: end === -1 ? html.length : end + 3 };
}
function findElementEnd(html: string, tagName: string, openTagEnd: number): number {View on GitHub (pinned to 5be4028344)
Solutions
- Open the artifact's template.html and find the tag reported by the surrounding renderFragment scan; add the missing '>' to close the opening tag.
- Balance every attribute quote — verify each opening '"' or "'" has a matching closing quote before the tag's '>'.
- If the template was produced by an agent run, regenerate the artifact so the streaming output is not truncated.
- Run the template through an HTML linter/validator before persisting it as a live artifact.
Example fix
// before (template.html)
<ul data-od-repeat="item in data.items" class="list
<li>{{item.label}}</li>
</ul>
// after
<ul data-od-repeat="item in data.items" class="list">
<li>{{item.label}}</li>
</ul> Defensive patterns
Strategy: validation
Validate before calling
// Reject malformed templates before persisting/rendering.
function assertTagsClosed(html: string): void {
let quote: string | null = null;
let inTag = false;
for (let i = 0; i < html.length; i++) {
const ch = html[i];
if (inTag) {
if (quote) {
if (ch === quote) quote = null;
} else if (ch === '"' || ch === "'") {
quote = ch;
} else if (ch === '>') {
inTag = false;
}
continue;
}
if (ch === '<') inTag = true;
}
if (inTag) throw new Error('template has an unterminated tag');
}
assertTagsClosed(input.templateHtml); Prevention
- Validate template.html structure before writing the artifact (run a tag-balance check or an HTML parser).
- Never truncate streamed template output mid-tag; finalize the stream before persisting.
- Lint agent-authored templates in a review step before they reach the renderer.
When it happens
Trigger: A template.html containing an opening tag with no closing '>', e.g. `<div data-od-repeat="item in data.items"` (truncated). Or an attribute whose quote is never closed, e.g. `<div title='oops>` — the quote stays open and the loop runs off the end of the string. Also triggered by a directive-bearing tag whose opening tag spans a malformed region the scanner cannot exit.
Common situations: Agent-authored template.html gets truncated mid-tag during streaming; an LLM emits a tag with an unbalanced quote when filling in attributes; hand-edited templates where the closing '>' was accidentally deleted; copy-pasting a fragment that ends inside a tag.
Related errors
- unbalanced data-od-repeat element <${tagName}>
- invalid data-od-repeat directive: "${directive.spec}" (expec
- data-od-repeat source is not an array: ${arrayPath}
- script elements are not supported in live artifact previews
- iframe elements are not supported in live artifact previews
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/7f6215b2efc60e53.
Report an issue: GitHub.