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

  1. Inspect the template at the path in the message and confirm whether a `<style>` block exists; restore it if it was removed by accident.
  2. If styles moved to an external CSS file, update the generator to read the CSS file instead of regex-matching `<style>`.
  3. Relax the regex to allow attributes, e.g. `/<style[^>]*>[\s\S]*?<\/style>/`, if the tag now carries attributes.
  4. 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 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


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)