paperclipai/paperclip · error

Could not load wireframe viewer styles

Error message

Could not load wireframe viewer styles

What it means

The v7 generator's `viewerHtml()` extracts the inline `<style>` block from the shared site template with a regex and throws this fixed message when no match is found, preventing generation of an unstyled review viewer. Same guard family as generators v2–v6.

Solutions

  1. Confirm the template still has a complete inline `<style>` block; restore it if removed accidentally.
  2. Adjust the regex to `/<style[^>]*>[\s\S]*?<\/style>/` if the tag now has attributes.
  3. If styles now live in a CSS file, read that file in `viewerHtml()` and inline it in the generated head.
  4. Re-run the generator and inspect the built index.html.

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 has no inline <style>; v7 viewer cannot be generated");
}

Type guard

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

Try / catch

try {
  writeViewer();
} catch (err) {
  if (err.message === "Could not load wireframe viewer styles") {
    console.error("Restore inline <style> in site-template.html or read the external CSS instead.");
  } else throw err;
}

Prevention

When it happens

Trigger: Running `node doc/plans/chat-adapters/generate-wireframes-v7.mjs` when `packages/skills-catalog/catalog/bundled/product/wireframe/assets/site-template.html` no longer contains a literal `<style>...</style>` section: styles extracted to external CSS, style tag given attributes, or asset truncated/damaged in a merge.

Common situations: Template refactors outpacing the plan-directory generator snapshots; partial checkouts; hand edits that drop the closing `</style>` tag so the non-greedy regex fails.

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/bf2432e7adf0ad6f. Report an issue: GitHub.

Appendix: source

Thrown at doc/plans/chat-adapters/generate-wireframes-v7.mjs:300

  ...providers.map((provider) => [provider.name, [
    ...setupScreens.filter((screen) => screen.provider === provider.name),
    ...detailScreens.filter((screen) => screen.provider === provider.name)
  ]]),
  ["Paperclip", sharedScreens.filter((screen) => screen.group === "Paperclip")]
];
const orderedScreens = groups.flatMap(([, screens]) => screens);

function uiDocument() {
  const inventory = orderedScreens.map((screen) => `| ${screen.id} | ${screen.group} | ${screen.tab} | ${screen.title} | ${screen.desktopSize} | ${screen.mobileSize} |`).join("\n");
  const details = orderedScreens.map((screen) => `### ${screen.id} · ${screen.title}\n\nPurpose: ${screen.subtitle}\n\n${screen.annotations.map((item, index) => `${index + 1}. ${item}`).join("\n")}\n\n${screen.actions ? `Actions:\n\n${screen.actions.map(([label, effect]) => `- **${label}:** ${effect}`).join("\n")}\n\n` : ""}Rationale: ${screen.rationale}`).join("\n\n");
  const defaults = fixedBehavior.map(([name, behavior]) => `- **${name}:** ${behavior}`).join("\n");
  return `# Paperclip Chat Adapters UI Surfaces — v7\n\nDate: 2026-09-04  \nPaperclip base: \`${baseSha}\`  \nReview viewer: [\`index.html\`](./index.html)  \nWireframes: [\`wireframes-v7/\`](./wireframes-v7/)\n\n## Product decision\n\nOverview is removed. Activated connectors open on Settings and expose only four management tabs: Settings, Access, Conversations, and Activity. Settings contains only destination reach that a user can plausibly change.\n\n${defaults}\n\n## Settings inventory\n\n- Slack: allowed channels and an Allow direct messages toggle.\n- GitHub: allowed repositories only.\n- Microsoft Teams: allowed channels, Allow direct messages, and Allow group chats.\n- Telegram: allowed groups/topics and an Allow direct messages toggle.\n\n## Screen inventory\n\n| ID | Group | Surface | Title | Desktop | Mobile |\n|---|---|---|---|---|---|\n${inventory}\n\n## Annotation and action notes\n\n${details}\n`;
}

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 surfaces" : `${esc(label)} connector`}</h2></div>${screens.map((screen) => {
    const notes = screen.annotations.map((note, index) => `<li><b>${index + 1}</b> — ${esc(note)}</li>`).join("\n");
    const actions = screen.actions ? `<h3>What the actions do</h3><ul>${screen.actions.map(([name, effect]) => `<li><b>${esc(name)}</b> — ${esc(effect)}</li>`).join("\n")}</ul>` : "";
    return `<section id="s${screen.id}"><div class="lede">${esc(screen.group)} · ${esc(screen.tab)}</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-v7/${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-v7/${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>${actions}<div class="why"><b>Rationale:</b> ${esc(screen.rationale)}</div></div></div></div></section>`;
  }).join("\n")}`).join("\n");
  const defaults = fixedBehavior.map(([name, behavior]) => `<li><b>${esc(name)}</b> — ${esc(behavior)}</li>`).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 — configurable reach 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 · v7</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">Configurable reach 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-v7.md"><span class="num">U</span>UI specification v7</a><a href="2026-09-04-chat-adapters-minimum-setup-v6.md"><span class="num">M</span>Minimum setup</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="#decisions"><span class="num">✓</span>Fixed product behavior</a></nav></details><main><header class="hero"><div class="crumb">Paperclip · Connectors · Settings v7</div><h1>Configure reach. Default everything else.</h1><p>The read-only Overview tab is gone. Settings contains only destinations and private-conversation toggles that an operator can plausibly change.</p><div class="decision"><b>Product rule</b><span>Task boundaries, delivery, credentials, installation drift, and provider capabilities are fixed behavior or conditional repair state—not 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-v7.md">UI specification v7</a><a href="2026-09-04-chat-adapters-platform-surfaces.md">Platform decisions</a></div><div class="pills"><span class="pill">${orderedScreens.length} product surfaces</span><span class="pill">4 management tabs</span><span class="pill">Reach-only 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.</p></div><section id="flow" class="flow-section"><div class="lede">Navigation and product flow</div><h2>Setup, then reach-only management</h2><p class="desc">Activated connectors open on Settings. Diagnostics and conditional repairs live in Activity.</p><div class="wire" data-zoom data-caption="Chat connector product flow"><div class="label"><span>flow.svg</span><span>1280×880</span></div><img src="wireframes-v7/flow.svg" alt="Chat connector product flow"/></div></section>${sections}<section id="decisions"><div class="lede">Product decisions</div><h2>Behaviors removed from Settings</h2><div class="notes"><ul>${defaults}</ul></div></section><div class="footer">Generated with Paperclip’s wireframe contract. The current viewer contains no Overview page or non-configurable settings rows.</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, "2026-09-04-chat-adapters-ui-surfaces-v7.md"), `${uiDocument()}\n`);
writeFileSync(join(root, "index.html"), `${viewerHtml()}\n`);
console.log(`Generated ${orderedScreens.length * 2 + 1} v7 SVGs across ${orderedScreens.length} product surfaces.`);

View on GitHub (pinned to 3f1d897a7c)