paperclipai/paperclip · error

Could not load wireframe viewer styles

Error message

Could not load wireframe viewer styles

What it means

The v3 wireframe generator's `viewerHtml()` extracts the shared `<style>` block from the bundled site template via regex. When the match returns undefined — the template exists but has no inline `<style>...</style>` section — the function throws this fixed message instead of generating a styleless viewer.

Solutions

  1. Check the template file for a `<style>` block; restore it if removed unintentionally.
  2. Update the regex to `/<style[^>]*>[\s\S]*?<\/style>/` if the tag now has attributes.
  3. If the template now uses an external CSS file, change the generator to read that CSS file and inline it.
  4. Regenerate and verify index.html renders with styles.

Example fix

// before
const style = template.match(/<style>[\s\S]*?<\/style>/)?.[0];
// after
const style = template.match(/<style[^>]*>[\s\S]*?<\/style>/)?.[0];
Defensive patterns

Strategy: validation

Validate before calling

const template = readFileSync(templatePath, "utf8");
if (!/<style[^>]*>[\s\S]*?<\/style>/.test(template)) {
  throw new Error(`site-template.html lost its inline <style>; cannot build v3 viewer`);
}

Type guard

const hasInlineStyle = (html) => /<style[^>]*>[\s\S]*?<\/style>/.test(html);

Try / catch

try {
  viewerHtml();
} catch (err) {
  if (err.message === "Could not load wireframe viewer styles") {
    console.error("site-template.html has no <style> block; restore it or update the generator to read CSS.");
  } else throw err;
}

Prevention

When it happens

Trigger: Running `node doc/plans/chat-adapters/generate-wireframes-v3.mjs` when `packages/skills-catalog/catalog/bundled/product/wireframe/assets/site-template.html` has no literal `<style>` opening tag: styles moved to a linked stylesheet, tag gained attributes, or the style section was lost in a merge. The file WAS read; only the pattern missed.

Common situations: Template upgraded to external CSS while old generator versions (v3–v7) still expect inline styles; merge conflict resolution dropped the style block; someone reformatted `<style>` to `<style type="text/css">`.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/7959fc2d27994b8a. Report an issue: GitHub.

Appendix: source

Thrown at doc/plans/chat-adapters/generate-wireframes-v3.mjs:384

    screen.mobileSize = "375×1320";
  }
}
writeFileSync(join(out, "flow.svg"), `${flowSvg()}\n`);

const groups = [
  ["Start", sharedScreens.filter((screen) => screen.group === "Start")],
  ["Slack", providerScreens.filter((screen) => screen.provider === "Slack")],
  ["GitHub", providerScreens.filter((screen) => screen.provider === "GitHub")],
  ["Microsoft Teams", providerScreens.filter((screen) => screen.provider === "Microsoft Teams")],
  ["Telegram", providerScreens.filter((screen) => screen.provider === "Telegram")],
  ["Paperclip", sharedScreens.filter((screen) => screen.group === "Paperclip")]
];
const orderedScreens = groups.flatMap(([, screens]) => screens);

function viewerHtml() {
  const template = readFileSync(join(root, "../../../packages/skills-catalog/catalog/bundled/product/wireframe/assets/site-template.html"), "utf8");
  const style = template.match(/<style>[\s\S]*?<\/style>/)?.[0];
  if (!style) throw new Error("Could not load wireframe viewer styles");
  const toc = groups.map(([label, screens]) => `<h2>${esc(label)}</h2>${screens.map((screen) => `<a href="#s${screen.id}"><span class="num">${Number(screen.id)}</span>${esc(screen.title)}</a>`).join("\n")}`).join("\n");
  const sections = groups.map(([label, screens]) => `<div class="provider-break"><div class="lede">${esc(label)}</div><h2>${label === "Start" ? "Shared connection start" : label === "Paperclip" ? "Shared Paperclip operations" : `${esc(label)} planning`}</h2></div>${screens.map((screen) => {
    const notes = screen.annotations.map((note,index)=>`<li><b>${index+1}</b> — ${esc(note).replaceAll("**","")}</li>`).join("\n");
    return `<section id="s${screen.id}"><div class="lede">${esc(screen.group)}</div><h2><span class="step-num">${Number(screen.id)}.</span>${esc(screen.title)}</h2><p class="desc">${esc(screen.subtitle)}</p><div class="grid"><div class="wire" data-zoom data-caption="${screen.id} · ${esc(screen.title)} (desktop)"><div class="label"><span>${screen.id}-${screen.slug}.svg</span><span>${screen.desktopSize} · desktop</span></div><img src="wireframes-v3/${screen.id}-${screen.slug}.svg" alt="${esc(screen.title)} desktop wireframe" /></div><div class="wire mobile-wire mobile-col" data-zoom data-caption="${screen.id} · ${esc(screen.title)} (mobile)"><div class="label"><span>mobile</span><span>${screen.mobileSize}</span></div><img src="wireframes-v3/${screen.id}-${screen.slug}-mobile.svg" alt="${esc(screen.title)} mobile wireframe" /></div><div class="notes-col"><div class="notes"><h3>Annotations</h3><ul>${notes}</ul><div class="why"><b>Rationale:</b> ${esc(screen.rationale)}</div></div></div></div></section>`;
  }).join("\n")}`).join("\n");
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Paperclip chat adapters — consolidated provider review</title>${style}<style>.doc-links{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:16px}.doc-links a{min-height:48px;display:inline-flex;align-items:center;font-size:13px;font-weight:600}.notice{max-width:var(--maxw);margin:-32px 0 48px;padding:14px 18px;background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:4px}.notice p{margin:0}.decision{margin-top:24px;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:8px}.decision b{display:block;margin-bottom:6px}.provider-break{max-width:var(--maxw);margin:80px 0 8px;padding-top:24px;border-top:2px solid var(--ink)}.provider-break h2{font-size:28px;margin:6px 0 0}.toc-body h2{margin-top:18px}code{font-size:.92em}</style></head><body><div class="shell"><details class="toc"><summary class="toc-summary"><span><span class="crumb">Chat adapters · v3</span><br><span class="title">Jump to a screen</span></span><span class="chevron" aria-hidden="true"></span></summary><nav class="toc-body" aria-label="Section navigation"><h1>Chat adapters</h1><div style="font-size:13px;color:var(--muted);margin-bottom:16px">Consolidated provider review</div><h2>Documents</h2><a href="2026-09-03-chat-adapters-architecture.md"><span class="num">A</span>Architecture</a><a href="2026-09-04-chat-adapters-ui-surfaces-v3.md"><span class="num">U</span>Consolidated UI specification</a><a href="2026-09-04-chat-adapters-platform-surfaces.md"><span class="num">P</span>Platform research</a><h2>Flow</h2><a href="#flow"><span class="num">⤳</span>Product flow</a>${toc}<h2>Review</h2><a href="#coverage"><span class="num">✓</span>Decisions and coverage</a></nav></details><main><header class="hero"><div class="crumb">Paperclip · Connectors · Consolidated v3</div><h1>One start. Four provider paths.</h1><p>The generic connection screens appear once. Slack, GitHub, Microsoft Teams, and Telegram then own their setup, vertical settings, and plain-language conversation walkthroughs.</p><div class="decision"><b>No duplicate setup</b><span>“Invite Maya to Slack” appears only in the Slack group. Generic Channels and Behavior screens are replaced by each provider’s real settings.</span></div><div class="doc-links"><a href="2026-09-03-chat-adapters-architecture.md">Architecture plan</a><a href="2026-09-04-chat-adapters-ui-surfaces-v3.md">Consolidated UI specification</a><a href="2026-09-04-chat-adapters-platform-surfaces.md">Platform-specific research</a></div><div class="pills"><span class="pill">21 consolidated surfaces</span><span class="pill">4 provider groups</span><span class="pill">Vertical settings</span><span class="pill">Desktop + mobile</span></div></header><div class="notice" role="note"><p><b>Review convention:</b> red dashed marks are annotations, not proposed UI. Conversation walkthroughs explain what people see and what Paperclip creates; they are not standalone product pages.</p></div><section id="flow" class="flow-section"><div class="lede">Navigation and product flow</div><h2>Provider-specific after the shared start</h2><p class="desc">Connectors, conditional purpose, and agent selection are shared. The provider then owns its external handoff, settings, and conversation semantics. Paperclip operations appear once.</p><div class="wire" data-zoom data-caption="Consolidated chat connector product flow"><div class="label"><span>flow.svg</span><span>1280×900</span></div><img src="wireframes-v3/flow.svg" alt="Consolidated chat connector product flow"/></div></section>${sections}<section id="coverage"><div class="lede">Review</div><h2>What changed in v3</h2><div class="notes"><ul><li><b>Grouped navigation:</b> Slack, GitHub, Microsoft Teams, and Telegram each have their own left-navigation header and three screens.</li><li><b>No duplicate Slack handoff:</b> the earlier generic Invite screen is removed from the viewer; Slack Setup is the only version.</li><li><b>No generic settings duplicates:</b> the earlier generic Channels and Behavior screens are removed; provider settings own those decisions.</li><li><b>Normal settings layout:</b> every provider uses full-width vertical sections and ordinary setting rows, with tall canvases where needed.</li><li><b>Understandable interactions:</b> internal ingress/binding pipeline labels are replaced by the provider conversation people see beside the single Paperclip issue it creates.</li><li><b>Stable review links:</b> provider IDs 13–24 are retained, so existing anchors such as <code>#s15</code> still point to the same provider topic.</li><li><b>Paperclip base:</b> <code>7b094724e65c04949706df638d497afb02c84b62</code>, matching <code>origin/master</code> when generated.</li></ul></div></section><div class="footer">Generated with Paperclip’s wireframe contract. Provider settings intentionally use long canvases so detail remains legible in a conventional vertical settings hierarchy.</div></main></div><div class="lightbox" id="lb" aria-hidden="true"><span class="close" id="lbClose" role="button" aria-label="Close preview">×</span><img id="lbImg" alt=""/><div class="caption" id="lbCap"></div></div><script>const lb=document.getElementById('lb'),lbImg=document.getElementById('lbImg'),lbCap=document.getElementById('lbCap');document.querySelectorAll('[data-zoom]').forEach(el=>el.addEventListener('click',()=>{const target=el.querySelector('img');if(!target)return;lbImg.src=target.src;lbImg.alt=target.alt;lbCap.textContent=el.dataset.caption||target.alt||'';lb.classList.add('open');lb.setAttribute('aria-hidden','false')}));function closeLightbox(){lb.classList.remove('open');lb.setAttribute('aria-hidden','true')}lb.addEventListener('click',closeLightbox);document.getElementById('lbClose').addEventListener('click',closeLightbox);document.addEventListener('keydown',e=>{if(e.key==='Escape')closeLightbox()});const tocElement=document.querySelector('details.toc'),media=window.matchMedia('(max-width:900px)'),setToc=()=>{tocElement.open=!media.matches};setToc();media.addEventListener('change',setToc);tocElement.querySelectorAll('.toc-body a').forEach(link=>link.addEventListener('click',()=>{if(media.matches)tocElement.open=false}));</script></body></html>`;
}

writeFileSync(join(root, "index.html"), `${viewerHtml()}\n`);
console.log(`Generated ${orderedScreens.length * 2 + 1} consolidated SVGs and index.html`);

View on GitHub (pinned to 3f1d897a7c)