paperclipai/paperclip · error
Could not read viewer styles from
Error message
Could not read viewer styles from ${templatePath} What it means
The v2 wireframe generator builds the HTML viewer by reading the shared site template and extracting its `<style>...</style>` block with the regex `/<style>[\s\S]*?<\/style>/`. If the template file is readable but contains no such block, `style` is undefined and the script throws this error naming the template path rather than emitting an HTML viewer with no styles.
Solutions
- Inspect the template at the path in the message and confirm whether a `<style>` block exists; restore it if it was removed by accident.
- If styles moved to an external CSS file, update the generator to read the CSS file instead of regex-matching `<style>`.
- Relax the regex to allow attributes, e.g. `/<style[^>]*>[\s\S]*?<\/style>/`, if the tag now carries attributes.
- Check out the template version contemporaneous with the v2 generator (git log on the asset) and re-run.
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 (!template.includes("<style")) {
throw new Error(`Template ${templatePath} has no inline <style> block; regenerate or point at the CSS file`);
} Type guard
const templateHasStyles = (html) => /<style[^>]*>[\s\S]*?<\/style>/.test(html);
Try / catch
const style = template.match(/<style[^>]*>[\s\S]*?<\/style>/)?.[0];
if (!style) {
console.error(`No <style> in ${templatePath}; check for external-CSS refactor`);
} Prevention
- When refactoring the site template, grep doc/plans/chat-adapters/generate-wireframes-*.mjs for `<style>` regex dependencies and update all versions.
- Keep the inline style block as part of the template's documented contract.
- Test template changes by running the oldest generator version too.
When it happens
Trigger: Running the generator when `packages/skills-catalog/catalog/bundled/product/wireframe/assets/site-template.html` no longer contains a literal `<style>` tag — e.g. the template was refactored to use an external `.css` file, `<link>`, or `<style >` with attributes, or the file was replaced.
Common situations: The site-template was updated by a design-system change that moved styles out of an inline tag; working on an older generator (v2) against a newer template; a bad merge dropped the style block; regex expects unadorned `<style>` and the template now has `<style media=...>`.
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
- Could not load wireframe viewer styles
- Could not load wireframe viewer styles
- Could not load wireframe viewer styles
- Could not load wireframe viewer styles
- Could not load wireframe viewer styles
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/ca4cdfa4b753d4ca.
Report an issue: GitHub.
Appendix: source
Thrown at doc/plans/chat-adapters/generate-wireframes-v2.mjs:294
function taskMobile(screen) {
return baseSvg(375,812,`${mobileHeader("PAP-1842")}${multiline(16,96,wrap(screen.title,32,2),20,"#000",24,'font-weight="700"')}${tx(16,150,"Created from Slack",12,"#666")}${rc(16,174,343,96,'fill="#e6e6e6"')}${tx(36,206,"#customer-support · Refund",14,"#000",'font-weight="700"')}${tx(36,234,"Assigned to Maya · Open Slack",12,"#666")}${rc(16,286,343,104)}${tx(36,316,"Ari S. · External · Linked",12,"#666",'font-weight="600"')}${tx(36,352,"The refund step is timing out again.",14,"#000")}${rc(16,406,343,120,'fill="#e6e6e6"')}${tx(36,436,"Maya · Agent",12,"#666",'font-weight="600"')}${multiline(36,470,["I found the failing retry boundary", "and prepared a patch."],14,"#000",22)}${tx(36,514,"Delivered to Slack",12,"#666")}${rc(16,542,343,168)}${tx(36,574,"Internal note",12,"#666")}${ln(36,598,339,598,'stroke="#666"')}${tx(36,638,"□ Send to channel",14,"#000",'font-weight="600"')}${button(207,650,132,"Comment",true)}${annotations([{x:8,y:166,w:359,h:112},{x:8,y:278,w:359,h:120},{x:8,y:398,w:359,h:136},{x:8,y:534,w:359,h:184}],true)}`);
}
function agentChannelsMobile(screen) {
return baseSvg(375,812,`${mobileHeader("Maya")}${tx(16,96,"Channels",20,"#000",'font-weight="700"')}${tx(16,124,"Every place people can reach Maya.",12,"#666")}${rc(16,158,343,48)}${tx(32,188,"Channels",14,"#000",'font-weight="600"')}${tx(335,188,"⌄",14,"#666",'text-anchor="end"')}${button(16,222,343,"Connect a channel",true)}${[["Slack · @maya","Acme · 2 channels · Healthy"],["Telegram · @maya_helper_bot","Support + DMs · Needs attention"]].map((r,i)=>{const y=294+i*112;return `${rc(16,y,343,96,i===0?'fill="#e6e6e6"':'fill="#fff"')}${tx(36,y+32,r[0],14,"#000",'font-weight="700"')}${tx(36,y+62,r[1],12,"#666")}${tx(335,y+48,"›",14,"#000",'text-anchor="end"')}`;}).join("\n")}${tx(16,546,"Recent channel tasks",14,"#000",'font-weight="700"')}${rc(16,566,343,120)}${tx(36,600,"PAP-1842 · Refund workflow",14,"#000",'font-weight="600"')}${tx(335,600,"Slack",12,"#666",'text-anchor="end"')}${ln(32,622,343,622,'stroke="#e6e6e6"')}${tx(36,656,"PAP-1839 · Customer question",14,"#000",'font-weight="600"')}${annotations([{x:8,y:150,w:359,h:64},{x:8,y:214,w:359,h:64},{x:8,y:286,w:359,h:224},{x:8,y:538,w:359,h:156}],true)}`);
}
function flowSvg() {
const node = (x,y,w,title,sub,fill=false) => `${rc(x,y,w,88,fill?'fill="#e6e6e6"':'fill="#fff"')}${tx(x+16,y+32,title,14,"#000",'font-weight="700"')}${tx(x+16,y+58,sub,12,"#666")}`;
const arrow = (x1,y1,x2,y2,dashed=false) => `${ln(x1,y1,x2,y2,dashed?'stroke-dasharray="6 4"':'')}<polygon points="${x2},${y2} ${x2-9},${y2-6} ${x2-9},${y2+6}" fill="#000" stroke="none"/>`;
return baseSvg(1280,800,`${tx(48,48,"Chat connectors · simplified Paperclip product flow",28,"#000",'font-weight="700"')}${tx(48,76,"The provider handoff completes setup; all policy editing happens later in the existing connector-detail shell.",14,"#666")}${node(48,128,176,"/apps","Choose connector",true)}${arrow(224,172,264,172)}${node(264,128,192,"Purpose?","Only when ambiguous")}${arrow(456,172,496,172)}${node(496,128,184,"Choose agent","Exactly one")}${arrow(680,172,720,172)}${node(720,128,184,"Invite bot","Provider handoff")}${arrow(904,172,944,172)}${node(944,128,240,"Connector overview","Connected with defaults",true)}${arrow(360,216,360,290,true)}${node(264,290,192,"Existing tool flow","Credential + access")}${tx(48,258,"CHAT PATH",12,"#666",'font-weight="700"')}${tx(264,274,"TOOL BRANCH",12,"#666",'font-weight="700"')}${node(48,402,168,"Channels","Reach + threads")}${node(232,402,168,"Access","People + authority")}${node(416,402,168,"Behavior","Output + overlap")}${node(600,402,184,"Conversations","Thread ↔ issue",true)}${node(800,402,168,"Activity","Delivery ledger")}${node(984,402,200,"Agent · Channels","Endpoint summary")}${`<path d="M 1064 216 C 1064 336, 692 320, 692 394" fill="none" stroke="#000" stroke-dasharray="6 4"/><polygon points="692,394 686,385 698,385" fill="#000" stroke="none"/>`}${arrow(692,490,692,558)}${node(600,558,184,"Bound task","Normal task UI",true)}${arrow(784,602,896,602)}${node(896,558,224,"Provider thread","Safe publication")}${node(48,650,168,"Identity link","From Access")}${node(232,650,168,"Relay drawer","When private")}${node(416,650,168,"Detach/rebind","From conversation")}${arrow(416,694,592,626,true)}${annotations([{x:40,y:120,w:1152,h:184},{x:256,y:282,w:208,h:104},{x:40,y:394,w:1152,h:104},{x:40,y:542,w:1096,h:204}])}`);
}
function viewerHtml() {
const templatePath = join(root, "../../../packages/skills-catalog/catalog/bundled/product/wireframe/assets/site-template.html");
const template = readFileSync(templatePath, "utf8");
const style = template.match(/<style>[\s\S]*?<\/style>/)?.[0];
if (!style) throw new Error(`Could not read viewer styles from ${templatePath}`);
const viewerScreens = [...screens, ...providerScreens];
const toc = viewerScreens.map((screen) => `<a href="#s${screen.id}"><span class="num">${Number(screen.id)}</span>${esc(screen.title)}</a>`).join("\n");
const sections = viewerScreens.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">${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>1280×800 · desktop</span></div><img src="wireframes-v2/${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>375×812</span></div><img src="wireframes-v2/${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");
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 — provider planning</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}code{font-size:.92em}</style></head><body><div class="shell"><details class="toc"><summary class="toc-summary"><span><span class="crumb">Chat adapters · v2</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">Shared flow + provider deep dives</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-v2.md"><span class="num">U</span>Shared UI specification</a><a href="2026-09-04-chat-adapters-platform-surfaces.md"><span class="num">P</span>Platform specification</a><a href="2026-09-03-chat-adapters-research-notes.md"><span class="num">R</span>Research notes</a><h2>Flow</h2><a href="#flow"><span class="num">⤳</span>Product flow</a><h2>Screens</h2>${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 · Provider planning</div><h1>Connect simply. Configure precisely.</h1><p>Chat connectors live in the existing <code>/apps</code> catalog. The shared path asks for an agent and provider invite; detailed Slack, GitHub, Teams, and Telegram setup and behavior stay provider-specific.</p><div class="decision"><b>Minimum chat path</b><span>Connectors → purpose only if ambiguous → agent → provider handoff → connected.</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-v2.md">Shared UI specification</a><a href="2026-09-04-chat-adapters-platform-surfaces.md">Platform-specific specification</a><a href="2026-09-03-chat-adapters-ui-surfaces.md">v1 requirements inventory</a></div><div class="pills"><span class="pill">24 focused surfaces</span><span class="pill">4 provider deep dives</span><span class="pill">Setup + settings + interactions</span><span class="pill">Desktop + mobile</span></div></header><div class="notice" role="note"><p><b>Review convention:</b> red dashed marks and numbered circles are annotations only. They are not proposed Paperclip interface elements.</p></div><section id="flow" class="flow-section"><div class="lede">Navigation and product flow</div><h2>Setup ends at the provider handoff</h2><p class="desc">The tool branch returns to the existing tool-connection flow. The chat branch creates a default endpoint, then uses the ordinary connector-detail shell. Screens 13–24 detail the external setup, settings, and native interaction model for each launch provider.</p><div class="wire" data-zoom data-caption="Simplified chat connector product flow"><div class="label"><span>flow.svg</span><span>1280×800</span></div><img src="wireframes-v2/flow.svg" alt="Simplified chat connector navigation and product flow"/></div><div class="notes"><h3>Annotations</h3><ul><li><b>1</b> — Primary chat path from the existing catalog through provider invite.</li><li><b>2</b> — Conditional tool-purpose branch into the existing setup flow.</li><li><b>3</b> — Connector detail navigation and the agent-summary route.</li><li><b>4</b> — Bound task, publication, identity, relay, and detach branches.</li></ul></div></section>${sections}<section id="coverage"><div class="lede">Decisions and coverage</div><h2>What this version resolves</h2><div class="notes"><ul><li><b>One catalog:</b> Connectors remains the only discovery, setup, and management entry point.</li><li><b>Minimal shared wizard:</b> chat-only providers ask for agent and provider handoff; dual-purpose providers add one direction choice.</li><li><b>Real external handoffs:</b> each provider setup separates Paperclip-owned fields from provider app registration, policy, consent, installation, and verification.</li><li><b>Provider-native task boundaries:</b> Slack thread, GitHub object/review thread, Teams post/conversation, and Telegram active chat/topic behavior are explicit.</li><li><b>Least privilege:</b> GitHub code/tool access, Teams RSC/Graph access, Slack optional rich scopes, and Telegram topic administration are never implied by basic chat setup.</li><li><b>Exact fallbacks:</b> unsupported streaming, files, buttons, modals, ephemeral replies, history, or ambient visibility degrade to a named behavior.</li><li><b>Research pins:</b> Paperclip <code>b84964e5a2fa8b1e6498a1ccb471f6adba97d470</code>; Vercel Chat SDK <code>51322dde8f4aafd8a7fc7a20cbfd7ae45cafaa5c</code>; OpenTag <code>6a770d862349f8e996c23c145aef6d6275914a23</code>.</li></ul></div></section><div class="footer">Generated from Paperclip's bundled <code>wireframe</code> skill viewer template. Wires use black 1.5 strokes, white surfaces, grayscale placeholders, an 8px rhythm, and 12/14/20/28 type sizes. Red is reserved for review annotations.</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.tagName==='IMG'?el: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>`;
}
for (const screen of screens) {
const desktop = screen.kind === "catalog" ? catalogDesktop(screen)
: screen.kind === "purpose" ? purposeDesktop(screen)
: screen.kind === "agent" ? chooseAgentDesktop(screen)
: screen.kind === "invite" ? inviteDesktop(screen)
: screen.kind === "detail" ? detailDesktop(screen)
: screen.kind === "task" ? taskDesktop(screen)
: agentChannelsDesktop(screen);
const mobile = screen.kind === "catalog" ? catalogMobile(screen)View on GitHub (pinned to 3f1d897a7c)