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

  1. Open the artifact's template.html and find the tag reported by the surrounding renderFragment scan; add the missing '>' to close the opening tag.
  2. Balance every attribute quote — verify each opening '"' or "'" has a matching closing quote before the tag's '>'.
  3. If the template was produced by an agent run, regenerate the artifact so the streaming output is not truncated.
  4. 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

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


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/7f6215b2efc60e53. Report an issue: GitHub.