@aparte/plugin-artifacts 0.19.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/card.d.ts.map +1 -1
- package/dist/custom-elements.json +17 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -10
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +1 -1
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js.map +1 -1
- package/dist/options.d.ts +29 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/preview-document-CUKDtx2T.js.map +1 -1
- package/dist/segment.d.ts +6 -0
- package/dist/segment.d.ts.map +1 -1
- package/package.json +24 -4
package/dist/card.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../src/card.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAa3E,4FAA4F;AAC5F,MAAM,MAAM,qBAAqB,GAAG,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../src/card.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAa3E,4FAA4F;AAC5F,MAAM,MAAM,qBAAqB,GAAG,eAAe,CAAC;AAsBpD,eAAO,MAAM,gBAAgB,EAAE,qBAAqB,CAAC,eAAe,CA+UnE,CAAC"}
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
"type": {
|
|
144
144
|
"text": "AparteSegmentRenderer<ArtifactSegment>"
|
|
145
145
|
},
|
|
146
|
-
"default": "{ type: ARTIFACT_SEGMENT_TYPE, render: (segment: ArtifactSegment) => { const kind = (segment.artifactType || 'unknown').toLowerCase(); // Binary file kinds (xlsx/pdf/docx) follow a separate track: the generated JS // is implementation noise, so the file card shows progress, then the file. if (BINARY_FILE_KINDS.has(kind)) { return renderBinaryFileArtifact(segment, kind); } const title = segment.title?.trim() || labelForKind(kind); const displayLang = languageForKind(kind); const isStreaming = !!segment.isStreaming; const previewable = PREVIEWABLE_KINDS.has(kind) && previewEnabled(); // `t()`, not `getLocale().x ?? 'X'`: `t()` already falls back to // APARTE_DEFAULT_LOCALE, so the English lives in ONE place instead of being // re-typed at each call site — and `setLocale()` REPLACES rather than merges, // which makes a partial locale the normal case rather than an edge one. // `download` is title AND aria-label: they disagreed on the button next door, // whose title went through `t('copy')` while its aria-label said \"Copy\" in // every language. const cfg = contextConfig(); const downloadLabel = cfg.t('download'); const previewLabel = cfg.t('preview'); const codeLabel = cfg.t('code'); const isBinary = BINARY_FILE_KINDS.has(kind); const cleanContent = stripCodeFences(segment.content || ''); /* * Ids, so the tabs can POINT at their panels. * * The card announced `role=\"tablist\"` with `role=\"tab\"` buttons and shipped none * of the pattern's obligations: no `aria-controls`, no `role=\"tabpanel\"`, no ids * and no arrow keys. A role is a promise about behaviour — declaring tablist and * then behaving like two ordinary buttons tells a screen-reader user to expect a * relationship and a keyboard model that are not there, which is worse than the * plain buttons it actually was. * * A COUNTER, not the segment's id. The segment's id is the model's — a tool-call * segment is `tool-${toolCallId}` straight off the wire — so two calls answering * to one id gave two cards the same `id` and the same `aria-controls`, and * `getElementById` returns whichever parsed first: exactly the collision this * scoping exists to prevent. An author-chosen `id` is also the DOM-clobbering * shape core's sanitizer refuses on model markup. * * Escaped HERE rather than inside an id-building helper: `check:attr-escaping` * follows a local produced by an escaper and cannot see through a function, and * teaching it to trust the helper's NAME is precisely the hole that guard was * just tightened to close. (The value is digits; the escaper is what the reader * and the guard are owed, since the position is what makes the rule.) */ const cardId = escapeAttr(String(++cardIdSeq)); // The card ALWAYS opens on the code tab, and the preview frame is not built // here at all — it is mounted only when the user presses Preview // (`mountPreviewFrame`, called from the tab handler). // // It used to open on Preview for any artifact that was not streaming, i.e. // every render of a completed one — so reloading a persisted conversation // executed the model's JS with no gesture. Defaulting the tab is not enough // on its own either: a `display:none` iframe still loads and still runs // scripts, so the frame has to be ABSENT, not hidden. // // Ratified decision #8, applied to a tier-(c) affordance: content the app // did not produce does not get to act on its own. return ` <div class=\"aparte-segment aparte-card aparte-segment-artifact-card\" data-segment-id=\"${escapeHtml(segment.id)}\" data-artifact-type=\"${escapeHtml(kind)}\" data-streaming=\"${isStreaming ? 'true' : 'false'}\" data-tab=\"code\" data-previewable=\"${previewable ? 'true' : 'false'}\" data-binary=\"${isBinary ? 'true' : 'false'}\"> <header class=\"aparte-art-card__header\"> <div class=\"aparte-art-card__title-block\"> <span class=\"aparte-badge aparte-badge--outline aparte-art-card__kind\" data-kind=\"${escapeHtml(kind)}\">${escapeHtml(displayLang)}</span> <span class=\"aparte-art-card__title\">${escapeHtml(title)}</span> ${isStreaming ? `<span class=\"aparte-dot aparte-art-card__pulse\" role=\"img\" aria-label=\"${escapeAttr(cfg.t('generating'))}\"></span>` : ''} </div> <div class=\"aparte-art-card__actions\"> <button type=\"button\" class=\"aparte-btn aparte-btn--icon aparte-art-card__btn\" data-action=\"copy\" title=\"${escapeAttr(contextConfig().t('copy'))}\" aria-label=\"${escapeAttr(contextConfig().t('copy'))}\"> ${contextConfig().getIcon('copy')} </button> <button type=\"button\" class=\"aparte-btn aparte-btn--icon aparte-art-card__btn\" data-action=\"download\" title=\"${escapeAttr(downloadLabel)}\" aria-label=\"${escapeAttr(downloadLabel)}\" ${isStreaming ? 'disabled' : ''}> ${contextConfig().getIcon('download')} </button> </div> </header> <nav class=\"aparte-tabs aparte-tabs--underline aparte-art-card__tabs\" role=\"tablist\"> <button type=\"button\" class=\"aparte-tabs__tab\" role=\"tab\" id=\"aparte-art-${cardId}-tab-code\" aria-controls=\"aparte-art-${cardId}-pane-code\" aria-selected=\"true\" tabindex=\"0\" data-tab-target=\"code\">${escapeHtml(codeLabel)}</button> ${previewable ? `<button type=\"button\" class=\"aparte-tabs__tab\" role=\"tab\" id=\"aparte-art-${cardId}-tab-preview\" aria-controls=\"aparte-art-${cardId}-pane-preview\" aria-selected=\"false\" tabindex=\"-1\" data-tab-target=\"preview\" ${isStreaming ? 'disabled' : ''}>${escapeHtml(previewLabel)}</button>` : ''} </nav> <div class=\"aparte-art-card__body\"> <div class=\"aparte-art-card__pane\" role=\"tabpanel\" id=\"aparte-art-${cardId}-pane-code\" aria-labelledby=\"aparte-art-${cardId}-tab-code\" tabindex=\"0\" data-pane=\"code\"> <div class=\"aparte-code-content-wrapper\"> <pre><code class=\"language-${escapeHtml(displayLang)}\">${escapeHtml(cleanContent)}</code></pre> </div> </div> ${previewable ? ` <div class=\"aparte-art-card__pane\" role=\"tabpanel\" id=\"aparte-art-${cardId}-pane-preview\" aria-labelledby=\"aparte-art-${cardId}-tab-preview\" tabindex=\"0\" data-pane=\"preview\"> <div class=\"aparte-art-card__pending\">${escapeHtml(cfg.t('previewPending'))}</div> </div> ` : ''} </div> </div> `; }, /** * Every string this card shows: the copy button's tooltip, glyph and accessible * name, the download button's title and label, and the two tab names. They were * all hardcoded literals until they got locale keys — including the copy button's * `aria-label`, which said \"Copy\" while its own `title` one attribute away already * went through `t('copy')`, so a French reader got a French tooltip and an English * announcement. * * Nothing here touches the tab state or the preview pane: a mounted iframe is * running model-authored code, and re-rendering this card is exactly what the * hook exists to avoid. */ relabel: (element: HTMLElement) => { const cfg = contextConfig(); const copyBtn = element.querySelector('.aparte-art-card__btn[data-action=\"copy\"]'); if (copyBtn) { copyBtn.setAttribute('title', cfg.t('copy')); copyBtn.setAttribute('aria-label', cfg.t('copy')); copyBtn.innerHTML = cfg.getIcon('copy'); } const dl = element.querySelector('.aparte-art-card__btn[data-action=\"download\"]'); if (dl) { const label = cfg.t('download'); dl.setAttribute('title', label); dl.setAttribute('aria-label', label); } // Text only, and never `aria-selected` or `data-tab`: which pane is open is // the reader's state, not the locale's. A relabel that touched it would close // a preview somebody had opened. const previewTab = element.querySelector('[data-tab-target=\"preview\"]'); if (previewTab) previewTab.textContent = cfg.t('preview'); const codeTab = element.querySelector('[data-tab-target=\"code\"]'); if (codeTab) codeTab.textContent = cfg.t('code'); }, setup: (element: HTMLElement, segment: AparteArtifactSegment) => { latestSegment.set(element, segment); const kind = (segment.artifactType || '').toLowerCase(); if (BINARY_FILE_KINDS.has(kind)) { setupBinaryFileArtifact(element, segment, kind); return; } // Async highlight on the code pane const wrapper = element.querySelector('.aparte-code-content-wrapper'); if (wrapper) { const displayLang = languageForKind(kind); const cleanContent = stripCodeFences(segment.content || ''); void contextConfig().highlightCode(cleanContent, displayLang).then(html => { wrapper.innerHTML = html; }).catch(() => { /* best-effort: a failed highlight degrades silently */ }); } /* * Tab switching — and, for Preview, the one place the frame is created. * * A `tablist` is a SINGLE tab stop with arrow keys inside it, not N tab stops. * This shipped as two ordinary buttons under a tablist role: Tab landed on each * in turn and the arrows did nothing, so the role promised a keyboard model the * card did not have. The roving `tabindex` and the arrow handling below are that * model; `aria-controls` / `role=\"tabpanel\"` in the markup are the other half. */ const tabs = [...element.querySelectorAll<HTMLButtonElement>('[data-tab-target]')]; const select = (btn: HTMLButtonElement, focus: boolean): void => { const target = btn.getAttribute('data-tab-target'); if (!target || btn.disabled) return; if (target === 'preview') mountPreviewFrame(element, segment); element.setAttribute('data-tab', target); for (const b of tabs) { const on = b === btn; b.setAttribute('aria-selected', on ? 'true' : 'false'); b.tabIndex = on ? 0 : -1; } if (focus) btn.focus(); }; for (const [i, btn] of tabs.entries()) { btn.addEventListener('click', () => select(btn, false)); btn.addEventListener('keydown', (e) => { // Home/End as well as the arrows: both are in the pattern, and on a // two-tab list they are the fastest way to the one you are not on. const step = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0; let next = -1; if (step) next = (i + step + tabs.length) % tabs.length; else if (e.key === 'Home') next = 0; else if (e.key === 'End') next = tabs.length - 1; if (next < 0) return; e.preventDefault(); // Skip a disabled tab rather than trapping focus on it: Preview is // disabled while the artifact still streams. for (let n = 0; n < tabs.length; n++) { const candidate = tabs[(next + n * (step || 1) + tabs.length) % tabs.length]; if (candidate && !candidate.disabled) { select(candidate, true); return; } } }); } // Copy const copyBtn = element.querySelector<HTMLButtonElement>('[data-action=\"copy\"]'); if (copyBtn) { copyBtn.addEventListener('click', () => { // Late execution (user click) — resolve from the element. const code = stripCodeFences(segment.content || ''); void copyText(code).catch(() => { /* best-effort: a failed clipboard write degrades silently */ }); const original = copyBtn.innerHTML; copyBtn.innerHTML = contextConfig(copyBtn).getIcon('check'); copyBtn.setAttribute('title', contextConfig(copyBtn).t('copied')); setTimeout(() => { copyBtn.innerHTML = original; copyBtn.setAttribute('title', contextConfig(copyBtn).t('copy')); }, 1500); }); } // Download — a text artifact downloads from its own content. (A binary one // never reaches here: its setup returned above, and its file card owns the // button.) The LATEST segment, not the one this closure captured. const dlBtn = element.querySelector<HTMLButtonElement>('[data-action=\"download\"]'); if (dlBtn) { dlBtn.addEventListener('click', () => { if (dlBtn.disabled) return; downloadTextArtifact(latestSegment.get(element) ?? segment); }); } }, update: (element: HTMLElement, segment: AparteArtifactSegment) => { // Keep the click handler's view of the artifact current, and throw away a // frame that is now showing stale content so the next press rebuilds it. const previous = latestSegment.get(element); latestSegment.set(element, segment); if (previous && previous.content !== segment.content) { element.querySelector('.aparte-art-card__pane[data-pane=\"preview\"] iframe')?.remove(); } const isStreaming = !!segment.isStreaming; const kind = (segment.artifactType || '').toLowerCase(); if (BINARY_FILE_KINDS.has(kind)) { updateBinaryFileArtifact(element, segment, isStreaming); return; } const wasStreaming = element.getAttribute('data-streaming') === 'true'; const cleanContent = stripCodeFences(segment.content || ''); // 1. Live-update the code pane during streaming. // // One call owns both halves — the plain tail every token and the coloured // prefix on a throttle. Setting `textContent` here as well is what made the // pane flicker: it erased the highlighter's spans on every token. if (isStreaming) { const segId = element.getAttribute('data-segment-id') ?? segment.id; streamHighlight(element, '.aparte-code-content-wrapper', cleanContent, languageForKind(kind), segId); } else { const codeEl = element.querySelector('.aparte-code-content-wrapper code'); if (codeEl) { codeEl.textContent = cleanContent; } else { const wrapper = element.querySelector('.aparte-code-content-wrapper'); if (wrapper) { const displayLang = languageForKind(kind); wrapper.innerHTML = `<pre><code class=\"language-${escapeHtml(displayLang)}\">${escapeHtml(cleanContent)}</code></pre>`; } } } // 2. On stream-completion: highlight + build preview iframe + auto-switch if (wasStreaming && !isStreaming) { element.setAttribute('data-streaming', 'false'); // The pulse says \"the model is still writing this\". `render()` painted it // and nothing ever took it away, so a finished document kept claiming to be // in flight — forever, at 1.2s a cycle. Unnoticed until the artifact demo // actually streamed: the indicator had never been exercised. element.querySelector('.aparte-art-card__pulse')?.remove(); // Re-run syntax highlight now that content is final const wrapper = element.querySelector('.aparte-code-content-wrapper'); if (wrapper) { const displayLang = languageForKind(kind); void contextConfig().highlightCode(cleanContent, displayLang).then(html => { wrapper.innerHTML = html; }).catch(() => { /* best-effort: a failed highlight degrades silently */ }); } // Enable previously-disabled buttons (download, preview tab) element.querySelectorAll<HTMLButtonElement>('button[disabled]').forEach(b => { b.disabled = false; }); // Nothing to build and nothing to switch: enabling the Preview button // above is the whole of it. The frame is mounted by the tab handler, on // a real user press — see the note at `initialTab`. } }, // The card's sheet, injected once per document by core — the seam a renderer that // is not core's has onto the page. It is a real `.css` file, inlined at build. getStyles: () => artifactStyles, }"
|
|
146
|
+
"default": "{ type: ARTIFACT_SEGMENT_TYPE, render: (segment: ArtifactSegment) => { const kind = (segment.artifactType || 'unknown').toLowerCase(); // Binary file kinds (xlsx/pdf/docx) follow a separate track: the generated JS // is implementation noise, so the file card shows progress, then the file. if (BINARY_FILE_KINDS.has(kind)) { return renderBinaryFileArtifact(segment, kind); } const title = segment.title?.trim() || labelForKind(kind); const displayLang = languageForKind(kind); const isStreaming = !!segment.isStreaming; const previewable = PREVIEWABLE_KINDS.has(kind) && previewEnabled(); // `t()`, not `getLocale().x ?? 'X'`: `t()` already falls back to // APARTE_DEFAULT_LOCALE, so the English lives in ONE place instead of being // re-typed at each call site — and `setLocale()` REPLACES rather than merges, // which makes a partial locale the normal case rather than an edge one. // `download` is title AND aria-label: they disagreed on the button next door, // whose title went through `t('copy')` while its aria-label said \"Copy\" in // every language. const cfg = contextConfig(); const downloadLabel = cfg.t('download'); const openLabel = cfg.t('openArtifact'); const openable = renderOptions(cfg).open === true; const previewLabel = cfg.t('preview'); const codeLabel = cfg.t('code'); const isBinary = BINARY_FILE_KINDS.has(kind); const cleanContent = stripCodeFences(segment.content || ''); /* * Ids, so the tabs can POINT at their panels. * * The card announced `role=\"tablist\"` with `role=\"tab\"` buttons and shipped none * of the pattern's obligations: no `aria-controls`, no `role=\"tabpanel\"`, no ids * and no arrow keys. A role is a promise about behaviour — declaring tablist and * then behaving like two ordinary buttons tells a screen-reader user to expect a * relationship and a keyboard model that are not there, which is worse than the * plain buttons it actually was. * * A COUNTER, not the segment's id. The segment's id is the model's — a tool-call * segment is `tool-${toolCallId}` straight off the wire — so two calls answering * to one id gave two cards the same `id` and the same `aria-controls`, and * `getElementById` returns whichever parsed first: exactly the collision this * scoping exists to prevent. An author-chosen `id` is also the DOM-clobbering * shape core's sanitizer refuses on model markup. * * Escaped HERE rather than inside an id-building helper: `check:attr-escaping` * follows a local produced by an escaper and cannot see through a function, and * teaching it to trust the helper's NAME is precisely the hole that guard was * just tightened to close. (The value is digits; the escaper is what the reader * and the guard are owed, since the position is what makes the rule.) */ const cardId = escapeAttr(String(++cardIdSeq)); // The card ALWAYS opens on the code tab, and the preview frame is not built // here at all — it is mounted only when the user presses Preview // (`mountPreviewFrame`, called from the tab handler). // // It used to open on Preview for any artifact that was not streaming, i.e. // every render of a completed one — so reloading a persisted conversation // executed the model's JS with no gesture. Defaulting the tab is not enough // on its own either: a `display:none` iframe still loads and still runs // scripts, so the frame has to be ABSENT, not hidden. // // Ratified decision #8, applied to a tier-(c) affordance: content the app // did not produce does not get to act on its own. return ` <div class=\"aparte-segment aparte-card aparte-segment-artifact-card\" data-segment-id=\"${escapeHtml(segment.id)}\" data-artifact-type=\"${escapeHtml(kind)}\" data-streaming=\"${isStreaming ? 'true' : 'false'}\" data-tab=\"code\" data-previewable=\"${previewable ? 'true' : 'false'}\" data-binary=\"${isBinary ? 'true' : 'false'}\"> <header class=\"aparte-art-card__header\"> <div class=\"aparte-art-card__title-block\"> <span class=\"aparte-badge aparte-badge--outline aparte-art-card__kind\" data-kind=\"${escapeHtml(kind)}\">${escapeHtml(displayLang)}</span> <span class=\"aparte-art-card__title\">${escapeHtml(title)}</span> ${isStreaming ? `<span class=\"aparte-dot aparte-art-card__pulse\" role=\"img\" aria-label=\"${escapeAttr(cfg.t('generating'))}\"></span>` : ''} </div> <div class=\"aparte-art-card__actions\"> <button type=\"button\" class=\"aparte-btn aparte-btn--icon aparte-art-card__btn\" data-action=\"copy\" title=\"${escapeAttr(contextConfig().t('copy'))}\" aria-label=\"${escapeAttr(contextConfig().t('copy'))}\"> ${contextConfig().getIcon('copy')} </button> <button type=\"button\" class=\"aparte-btn aparte-btn--icon aparte-art-card__btn\" data-action=\"download\" title=\"${escapeAttr(downloadLabel)}\" aria-label=\"${escapeAttr(downloadLabel)}\" ${isStreaming ? 'disabled' : ''}> ${contextConfig().getIcon('download')} </button> ${openable ? `<button type=\"button\" class=\"aparte-btn aparte-btn--icon aparte-art-card__btn\" data-action=\"open\" title=\"${escapeAttr(openLabel)}\" aria-label=\"${escapeAttr(openLabel)}\" ${isStreaming ? 'disabled' : ''}>${contextConfig().getIcon('expand')}</button>` : ''} </div> </header> <nav class=\"aparte-tabs aparte-tabs--underline aparte-art-card__tabs\" role=\"tablist\"> <button type=\"button\" class=\"aparte-tabs__tab\" role=\"tab\" id=\"aparte-art-${cardId}-tab-code\" aria-controls=\"aparte-art-${cardId}-pane-code\" aria-selected=\"true\" tabindex=\"0\" data-tab-target=\"code\">${escapeHtml(codeLabel)}</button> ${previewable ? `<button type=\"button\" class=\"aparte-tabs__tab\" role=\"tab\" id=\"aparte-art-${cardId}-tab-preview\" aria-controls=\"aparte-art-${cardId}-pane-preview\" aria-selected=\"false\" tabindex=\"-1\" data-tab-target=\"preview\" ${isStreaming ? 'disabled' : ''}>${escapeHtml(previewLabel)}</button>` : ''} </nav> <div class=\"aparte-art-card__body\"> <div class=\"aparte-art-card__pane\" role=\"tabpanel\" id=\"aparte-art-${cardId}-pane-code\" aria-labelledby=\"aparte-art-${cardId}-tab-code\" tabindex=\"0\" data-pane=\"code\"> <div class=\"aparte-code-content-wrapper\"> <pre><code class=\"language-${escapeHtml(displayLang)}\">${escapeHtml(cleanContent)}</code></pre> </div> </div> ${previewable ? ` <div class=\"aparte-art-card__pane\" role=\"tabpanel\" id=\"aparte-art-${cardId}-pane-preview\" aria-labelledby=\"aparte-art-${cardId}-tab-preview\" tabindex=\"0\" data-pane=\"preview\"> <div class=\"aparte-art-card__pending\">${escapeHtml(cfg.t('previewPending'))}</div> </div> ` : ''} </div> </div> `; }, /** * Every string this card shows: the copy button's tooltip, glyph and accessible * name, the download button's title and label, and the two tab names. They were * all hardcoded literals until they got locale keys — including the copy button's * `aria-label`, which said \"Copy\" while its own `title` one attribute away already * went through `t('copy')`, so a French reader got a French tooltip and an English * announcement. * * Nothing here touches the tab state or the preview pane: a mounted iframe is * running model-authored code, and re-rendering this card is exactly what the * hook exists to avoid. */ relabel: (element: HTMLElement) => { const cfg = contextConfig(); const copyBtn = element.querySelector('.aparte-art-card__btn[data-action=\"copy\"]'); if (copyBtn) { copyBtn.setAttribute('title', cfg.t('copy')); copyBtn.setAttribute('aria-label', cfg.t('copy')); copyBtn.innerHTML = cfg.getIcon('copy'); } const dl = element.querySelector('.aparte-art-card__btn[data-action=\"download\"]'); if (dl) { const label = cfg.t('download'); dl.setAttribute('title', label); dl.setAttribute('aria-label', label); } const open = element.querySelector('.aparte-art-card__btn[data-action=\"open\"]'); if (open) { const label = cfg.t('openArtifact'); open.setAttribute('title', label); open.setAttribute('aria-label', label); } // Text only, and never `aria-selected` or `data-tab`: which pane is open is // the reader's state, not the locale's. A relabel that touched it would close // a preview somebody had opened. const previewTab = element.querySelector('[data-tab-target=\"preview\"]'); if (previewTab) previewTab.textContent = cfg.t('preview'); const codeTab = element.querySelector('[data-tab-target=\"code\"]'); if (codeTab) codeTab.textContent = cfg.t('code'); }, setup: (element: HTMLElement, segment: AparteArtifactSegment) => { latestSegment.set(element, segment); const kind = (segment.artifactType || '').toLowerCase(); if (BINARY_FILE_KINDS.has(kind)) { setupBinaryFileArtifact(element, segment, kind); return; } // Async highlight on the code pane const wrapper = element.querySelector('.aparte-code-content-wrapper'); if (wrapper) { const displayLang = languageForKind(kind); const cleanContent = stripCodeFences(segment.content || ''); paneContent.set(element, cleanContent); void contextConfig().highlightCode(cleanContent, displayLang).then(html => { wrapper.innerHTML = html; }).catch(() => { /* best-effort: a failed highlight degrades silently */ }); } /* * Tab switching — and, for Preview, the one place the frame is created. * * A `tablist` is a SINGLE tab stop with arrow keys inside it, not N tab stops. * This shipped as two ordinary buttons under a tablist role: Tab landed on each * in turn and the arrows did nothing, so the role promised a keyboard model the * card did not have. The roving `tabindex` and the arrow handling below are that * model; `aria-controls` / `role=\"tabpanel\"` in the markup are the other half. */ const tabs = [...element.querySelectorAll<HTMLButtonElement>('[data-tab-target]')]; const select = (btn: HTMLButtonElement, focus: boolean): void => { const target = btn.getAttribute('data-tab-target'); if (!target || btn.disabled) return; if (target === 'preview') mountPreviewFrame(element, segment); element.setAttribute('data-tab', target); for (const b of tabs) { const on = b === btn; b.setAttribute('aria-selected', on ? 'true' : 'false'); b.tabIndex = on ? 0 : -1; } if (focus) btn.focus(); }; for (const [i, btn] of tabs.entries()) { btn.addEventListener('click', () => select(btn, false)); btn.addEventListener('keydown', (e) => { // Home/End as well as the arrows: both are in the pattern, and on a // two-tab list they are the fastest way to the one you are not on. const step = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0; let next = -1; if (step) next = (i + step + tabs.length) % tabs.length; else if (e.key === 'Home') next = 0; else if (e.key === 'End') next = tabs.length - 1; if (next < 0) return; e.preventDefault(); // Skip a disabled tab rather than trapping focus on it: Preview is // disabled while the artifact still streams. for (let n = 0; n < tabs.length; n++) { const candidate = tabs[(next + n * (step || 1) + tabs.length) % tabs.length]; if (candidate && !candidate.disabled) { select(candidate, true); return; } } }); } // Open — handed to the app, which shows it where it wants. The LATEST segment. const openBtn = element.querySelector<HTMLButtonElement>('[data-action=\"open\"]'); if (openBtn) { openBtn.addEventListener('click', () => { // Not while it streams, like Download: the app would get a half-written document. if (openBtn.disabled) return; const latest = latestSegment.get(element) ?? segment; // As the card shows it, copies it and downloads it: without the code fences a // model may wrap the content in. const artifact = { ...latest, content: stripCodeFences(latest.content || '') }; element.dispatchEvent(new CustomEvent<ArtifactOpenEventDetail>('aparte-artifact-open', { bubbles: true, composed: true, detail: { artifact }, })); }); } // Copy const copyBtn = element.querySelector<HTMLButtonElement>('[data-action=\"copy\"]'); if (copyBtn) { copyBtn.addEventListener('click', () => { // Late execution (user click) — resolve from the element, and copy the // latest segment, not the one this closure captured: see `latestSegment`. const code = stripCodeFences((latestSegment.get(element) ?? segment).content || ''); void copyText(code).catch(() => { /* best-effort: a failed clipboard write degrades silently */ }); const original = copyBtn.innerHTML; copyBtn.innerHTML = contextConfig(copyBtn).getIcon('check'); copyBtn.setAttribute('title', contextConfig(copyBtn).t('copied')); setTimeout(() => { copyBtn.innerHTML = original; copyBtn.setAttribute('title', contextConfig(copyBtn).t('copy')); }, 1500); }); } // Download — a text artifact downloads from its own content. (A binary one // never reaches here: its setup returned above, and its file card owns the // button.) The LATEST segment, not the one this closure captured. const dlBtn = element.querySelector<HTMLButtonElement>('[data-action=\"download\"]'); if (dlBtn) { dlBtn.addEventListener('click', () => { if (dlBtn.disabled) return; downloadTextArtifact(latestSegment.get(element) ?? segment); }); } }, update: (element: HTMLElement, segment: AparteArtifactSegment) => { // Keep the click handler's view of the artifact current, and throw away a // frame that is now showing stale content so the next press rebuilds it. const previous = latestSegment.get(element); latestSegment.set(element, segment); if (previous && previous.content !== segment.content) { element.querySelector('.aparte-art-card__pane[data-pane=\"preview\"] iframe')?.remove(); } const isStreaming = !!segment.isStreaming; const kind = (segment.artifactType || '').toLowerCase(); if (BINARY_FILE_KINDS.has(kind)) { updateBinaryFileArtifact(element, segment, isStreaming); return; } const wasStreaming = element.getAttribute('data-streaming') === 'true'; const cleanContent = stripCodeFences(segment.content || ''); // 1. Live-update the code pane during streaming. // // One call owns both halves — the plain tail every token and the coloured // prefix on a throttle. Setting `textContent` here as well is what made the // pane flicker: it erased the highlighter's spans on every token. if (isStreaming) { const segId = element.getAttribute('data-segment-id') ?? segment.id; streamHighlight(element, '.aparte-code-content-wrapper', cleanContent, languageForKind(kind), segId); } else if (paneContent.get(element) !== cleanContent) { // Only a CHANGED content is written. A card built from a tool result is settled // from its first paint and still receives every update the tool segment gets; // writing `textContent` each time wiped the token spans `setup` had painted, and // with no stream ending there was no second highlight — a plain block in a Shiki // frame. A changed one is written and coloured again. paneContent.set(element, cleanContent); const displayLang = languageForKind(kind); const wrapper = element.querySelector('.aparte-code-content-wrapper'); const codeEl = wrapper?.querySelector('code'); if (codeEl) { codeEl.textContent = cleanContent; } else if (wrapper) { wrapper.innerHTML = `<pre><code class=\"language-${escapeHtml(displayLang)}\">${escapeHtml(cleanContent)}</code></pre>`; } if (wrapper && !wasStreaming) { void contextConfig().highlightCode(cleanContent, displayLang).then(html => { if (paneContent.get(element) === cleanContent) wrapper.innerHTML = html; }).catch(() => { /* best-effort: a failed highlight degrades silently */ }); } } // 2. On stream-completion: highlight + build preview iframe + auto-switch if (wasStreaming && !isStreaming) { element.setAttribute('data-streaming', 'false'); // The pulse says \"the model is still writing this\". `render()` painted it // and nothing ever took it away, so a finished document kept claiming to be // in flight — forever, at 1.2s a cycle. Unnoticed until the artifact demo // actually streamed: the indicator had never been exercised. element.querySelector('.aparte-art-card__pulse')?.remove(); // Re-run syntax highlight now that content is final const wrapper = element.querySelector('.aparte-code-content-wrapper'); if (wrapper) { const displayLang = languageForKind(kind); void contextConfig().highlightCode(cleanContent, displayLang).then(html => { wrapper.innerHTML = html; }).catch(() => { /* best-effort: a failed highlight degrades silently */ }); } // Enable previously-disabled buttons (download, preview tab) element.querySelectorAll<HTMLButtonElement>('button[disabled]').forEach(b => { b.disabled = false; }); // Nothing to build and nothing to switch: enabling the Preview button // above is the whole of it. The frame is mounted by the tab handler, on // a real user press — see the note at `initialTab`. } }, // The card's sheet, injected once per document by core — the seam a renderer that // is not core's has onto the page. It is a real `.css` file, inlined at build. getStyles: () => artifactStyles, }"
|
|
147
147
|
}
|
|
148
148
|
],
|
|
149
149
|
"exports": [
|
|
@@ -445,6 +445,14 @@
|
|
|
445
445
|
"module": "./options.js"
|
|
446
446
|
}
|
|
447
447
|
},
|
|
448
|
+
{
|
|
449
|
+
"kind": "js",
|
|
450
|
+
"name": "ArtifactOpenEventDetail",
|
|
451
|
+
"declaration": {
|
|
452
|
+
"name": "ArtifactOpenEventDetail",
|
|
453
|
+
"module": "./options.js"
|
|
454
|
+
}
|
|
455
|
+
},
|
|
448
456
|
{
|
|
449
457
|
"kind": "js",
|
|
450
458
|
"name": "AparteTool",
|
|
@@ -687,6 +695,14 @@
|
|
|
687
695
|
"module": "./options.js"
|
|
688
696
|
}
|
|
689
697
|
},
|
|
698
|
+
{
|
|
699
|
+
"kind": "js",
|
|
700
|
+
"name": "ArtifactOpenEventDetail",
|
|
701
|
+
"declaration": {
|
|
702
|
+
"name": "ArtifactOpenEventDetail",
|
|
703
|
+
"module": "./options.js"
|
|
704
|
+
}
|
|
705
|
+
},
|
|
690
706
|
{
|
|
691
707
|
"kind": "js",
|
|
692
708
|
"name": "AparteTool",
|
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,6 @@ export { artifactBlock, artifactSegment, artifactFromToolCall, ARTIFACT_TAG, ART
|
|
|
34
34
|
export type { ArtifactSegment, ArtifactInput } from './segment.js';
|
|
35
35
|
export { deriveArtifactKind } from './kinds.js';
|
|
36
36
|
export { buildSafePreviewDocument, PREVIEW_CSP } from './preview-document.js';
|
|
37
|
-
export type { ArtifactRenderOptions, ArtifactPreviewBuilder, ArtifactBinary, ArtifactBinaryResolver, ArtifactsSetupOptions } from './options.js';
|
|
37
|
+
export type { ArtifactRenderOptions, ArtifactPreviewBuilder, ArtifactBinary, ArtifactBinaryResolver, ArtifactsSetupOptions, ArtifactOpenEventDetail } from './options.js';
|
|
38
38
|
export type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';
|
|
39
39
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAA0E,KAAK,YAAY,EAA2B,MAAM,cAAc,CAAC;AAIlJ,OAAO,EAAwC,KAAK,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAEhG;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,EAAE,MAAM,GAAE,YAAiC,GAAG,MAAM,IAAI,CA4BzH;AAED,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACxF,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,YAAY,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACzH,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC9E,YAAY,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,cAAc,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAA0E,KAAK,YAAY,EAA2B,MAAM,cAAc,CAAC;AAIlJ,OAAO,EAAwC,KAAK,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAEhG;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,EAAE,MAAM,GAAE,YAAiC,GAAG,MAAM,IAAI,CA4BzH;AAED,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACxF,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,YAAY,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACzH,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC9E,YAAY,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,cAAc,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAC1K,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -349,6 +349,7 @@ const artifactStyles = '/*\n * @aparte/plugin-artifacts — the artifact card an
|
|
|
349
349
|
const PREVIEWABLE_KINDS = /* @__PURE__ */ new Set(["react", "html", "svg", "js", "css"]);
|
|
350
350
|
let cardIdSeq = 0;
|
|
351
351
|
const BINARY_FILE_KINDS = /* @__PURE__ */ new Set(["pdf", "xlsx", "docx"]);
|
|
352
|
+
const paneContent = /* @__PURE__ */ new WeakMap();
|
|
352
353
|
function previewEnabled() {
|
|
353
354
|
return renderOptions(contextConfig()).preview !== false;
|
|
354
355
|
}
|
|
@@ -365,6 +366,8 @@ const artifactRenderer = {
|
|
|
365
366
|
const previewable = PREVIEWABLE_KINDS.has(kind2) && previewEnabled();
|
|
366
367
|
const cfg = contextConfig();
|
|
367
368
|
const downloadLabel = cfg.t("download");
|
|
369
|
+
const openLabel = cfg.t("openArtifact");
|
|
370
|
+
const openable = renderOptions(cfg).open === true;
|
|
368
371
|
const previewLabel = cfg.t("preview");
|
|
369
372
|
const codeLabel = cfg.t("code");
|
|
370
373
|
const isBinary = BINARY_FILE_KINDS.has(kind2);
|
|
@@ -391,6 +394,7 @@ const artifactRenderer = {
|
|
|
391
394
|
<button type="button" class="aparte-btn aparte-btn--icon aparte-art-card__btn" data-action="download" title="${escapeAttr(downloadLabel)}" aria-label="${escapeAttr(downloadLabel)}" ${isStreaming ? "disabled" : ""}>
|
|
392
395
|
${contextConfig().getIcon("download")}
|
|
393
396
|
</button>
|
|
397
|
+
${openable ? `<button type="button" class="aparte-btn aparte-btn--icon aparte-art-card__btn" data-action="open" title="${escapeAttr(openLabel)}" aria-label="${escapeAttr(openLabel)}" ${isStreaming ? "disabled" : ""}>${contextConfig().getIcon("expand")}</button>` : ""}
|
|
394
398
|
</div>
|
|
395
399
|
</header>
|
|
396
400
|
<nav class="aparte-tabs aparte-tabs--underline aparte-art-card__tabs" role="tablist">
|
|
@@ -438,6 +442,12 @@ const artifactRenderer = {
|
|
|
438
442
|
dl.setAttribute("title", label);
|
|
439
443
|
dl.setAttribute("aria-label", label);
|
|
440
444
|
}
|
|
445
|
+
const open = element.querySelector('.aparte-art-card__btn[data-action="open"]');
|
|
446
|
+
if (open) {
|
|
447
|
+
const label = cfg.t("openArtifact");
|
|
448
|
+
open.setAttribute("title", label);
|
|
449
|
+
open.setAttribute("aria-label", label);
|
|
450
|
+
}
|
|
441
451
|
const previewTab = element.querySelector('[data-tab-target="preview"]');
|
|
442
452
|
if (previewTab) previewTab.textContent = cfg.t("preview");
|
|
443
453
|
const codeTab = element.querySelector('[data-tab-target="code"]');
|
|
@@ -454,6 +464,7 @@ const artifactRenderer = {
|
|
|
454
464
|
if (wrapper) {
|
|
455
465
|
const displayLang = languageForKind(kind2);
|
|
456
466
|
const cleanContent = stripCodeFences(segment.content || "");
|
|
467
|
+
paneContent.set(element, cleanContent);
|
|
457
468
|
void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
|
|
458
469
|
wrapper.innerHTML = html;
|
|
459
470
|
}).catch(() => {
|
|
@@ -491,10 +502,23 @@ const artifactRenderer = {
|
|
|
491
502
|
}
|
|
492
503
|
});
|
|
493
504
|
}
|
|
505
|
+
const openBtn = element.querySelector('[data-action="open"]');
|
|
506
|
+
if (openBtn) {
|
|
507
|
+
openBtn.addEventListener("click", () => {
|
|
508
|
+
if (openBtn.disabled) return;
|
|
509
|
+
const latest = latestSegment.get(element) ?? segment;
|
|
510
|
+
const artifact = { ...latest, content: stripCodeFences(latest.content || "") };
|
|
511
|
+
element.dispatchEvent(new CustomEvent("aparte-artifact-open", {
|
|
512
|
+
bubbles: true,
|
|
513
|
+
composed: true,
|
|
514
|
+
detail: { artifact }
|
|
515
|
+
}));
|
|
516
|
+
});
|
|
517
|
+
}
|
|
494
518
|
const copyBtn = element.querySelector('[data-action="copy"]');
|
|
495
519
|
if (copyBtn) {
|
|
496
520
|
copyBtn.addEventListener("click", () => {
|
|
497
|
-
const code = stripCodeFences(segment.content || "");
|
|
521
|
+
const code = stripCodeFences((latestSegment.get(element) ?? segment).content || "");
|
|
498
522
|
void copyText(code).catch(() => {
|
|
499
523
|
});
|
|
500
524
|
const original = copyBtn.innerHTML;
|
|
@@ -531,16 +555,21 @@ const artifactRenderer = {
|
|
|
531
555
|
if (isStreaming) {
|
|
532
556
|
const segId = element.getAttribute("data-segment-id") ?? segment.id;
|
|
533
557
|
streamHighlight(element, ".aparte-code-content-wrapper", cleanContent, languageForKind(kind2), segId);
|
|
534
|
-
} else {
|
|
535
|
-
|
|
558
|
+
} else if (paneContent.get(element) !== cleanContent) {
|
|
559
|
+
paneContent.set(element, cleanContent);
|
|
560
|
+
const displayLang = languageForKind(kind2);
|
|
561
|
+
const wrapper = element.querySelector(".aparte-code-content-wrapper");
|
|
562
|
+
const codeEl = wrapper?.querySelector("code");
|
|
536
563
|
if (codeEl) {
|
|
537
564
|
codeEl.textContent = cleanContent;
|
|
538
|
-
} else {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
565
|
+
} else if (wrapper) {
|
|
566
|
+
wrapper.innerHTML = `<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>`;
|
|
567
|
+
}
|
|
568
|
+
if (wrapper && !wasStreaming) {
|
|
569
|
+
void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
|
|
570
|
+
if (paneContent.get(element) === cleanContent) wrapper.innerHTML = html;
|
|
571
|
+
}).catch(() => {
|
|
572
|
+
});
|
|
544
573
|
}
|
|
545
574
|
}
|
|
546
575
|
if (wasStreaming && !isStreaming) {
|
|
@@ -637,7 +666,7 @@ function mountPreviewFrame(element, fallback) {
|
|
|
637
666
|
}
|
|
638
667
|
function setupArtifacts(options = {}, config = aparteGlobalConfig) {
|
|
639
668
|
const tool = createArtifactTool(options);
|
|
640
|
-
setRenderOptions(config, { preview: options.preview, onBinary: options.onBinary });
|
|
669
|
+
setRenderOptions(config, { preview: options.preview, onBinary: options.onBinary, open: options.open });
|
|
641
670
|
config.registerTool(tool, artifactHandler);
|
|
642
671
|
const toolRenderer = {
|
|
643
672
|
render: (segment) => artifactRenderer.render(artifactFromToolCall(segment)),
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/shared.ts","../src/highlight.ts","../src/options.ts","../src/binary-file.ts","../src/artifact.css?raw","../src/card.ts","../src/index.ts"],"sourcesContent":["/**\n * The leaves the artifact card and the binary-file path BOTH use.\n *\n * Scoped by a call-site census, not by intuition. The obvious-looking candidates are\n * not here: `BINARY_FILE_KINDS` reads as shared and is used only by the card (which\n * consults it to delegate), `PREVIEWABLE_KINDS`, `languageForKind` and\n * `downloadTextArtifact` are card-only, and `FILE_ICON_LABEL` and `formatBytes` are\n * binary-only. A first draft of this module took all of them and would have been a\n * grab-bag named for one of its three jobs.\n *\n * What is genuinely shared is small: fence stripping (which the plain code renderer\n * needs too), the kind→label map, and the throttle pair behind the debounced\n * re-highlight.\n */\n\n\n/**\n * Strip leading/trailing markdown code fences (``` or ~~~, optional lang tag).\n * Also strips any content that appears after the closing fence (small models\n * frequently duplicate lines after the closing ``` block).\n *\n * Char-based scanner — no regex.\n */\nexport function stripCodeFences(content: string): string {\n let s = content;\n\n // Opening fence: leading ``` or ~~~ (3+) optionally followed by language tag,\n // then a newline. Walk the start of the string only.\n if (s.startsWith('```') || s.startsWith('~~~')) {\n const fenceChar = s[0];\n let i = 0;\n while (i < s.length && s[i] === fenceChar) i++;\n // Skip language tag chars (anything until newline)\n while (i < s.length && s[i] !== '\\n') i++;\n // Skip the newline itself if present\n if (i < s.length && s[i] === '\\n') i++;\n s = s.slice(i);\n }\n\n // Closing fence: scan forward to find a line that is exclusively `````/`~~~`+\n // optionally followed by trailing whitespace; cut there + everything after.\n const closeAt = findClosingFence(s);\n if (closeAt !== -1) s = s.slice(0, closeAt);\n\n return s.trim();\n}\n\n/** Find the byte offset where a closing fence line begins, or -1 if none. */\nfunction findClosingFence(s: string): number {\n let i = 0;\n while (i < s.length) {\n // Find start of next line\n const lineStart = i;\n // Skip leading whitespace on this line (indentation)\n let k = lineStart;\n while (k < s.length && (s[k] === ' ' || s[k] === '\\t')) k++;\n if (k < s.length && (s[k] === '`' || s[k] === '~')) {\n const fenceChar = s[k];\n let runs = 0;\n while (k < s.length && s[k] === fenceChar) { runs++; k++; }\n if (runs >= 3) {\n // The rest of the line should be whitespace only — otherwise\n // it's not a closing fence (e.g. inline backtick text).\n let onlyWs = true;\n while (k < s.length && s[k] !== '\\n') {\n if (s[k] !== ' ' && s[k] !== '\\t' && s[k] !== '\\r') { onlyWs = false; break; }\n k++;\n }\n if (onlyWs) {\n // Cut at start of this line; if a `\\n` precedes, drop it too\n let cut = lineStart;\n if (cut > 0 && s[cut - 1] === '\\n') cut--;\n return cut;\n }\n }\n }\n // Advance to next line\n while (i < s.length && s[i] !== '\\n') i++;\n if (i < s.length) i++;\n }\n return -1;\n}\n\nexport function labelForKind(kind: string): string {\n switch (kind) {\n case 'react': return 'React component';\n case 'html': return 'HTML document';\n case 'svg': return 'SVG image';\n case 'js': return 'JavaScript snippet';\n case 'css': return 'CSS stylesheet';\n case 'json': return 'JSON document';\n case 'markdown': return 'Markdown document';\n case 'csv': return 'CSV table';\n case 'text': return 'Text file';\n case 'python': return 'Python script';\n case 'typescript': return 'TypeScript file';\n case 'bash': return 'Bash script';\n case 'sql': return 'SQL query';\n case 'pdf': return 'PDF generator';\n case 'xlsx': return 'Excel generator';\n case 'docx': return 'Word generator';\n default: return 'Artifact';\n }\n}\n\n/** Cap a segmentId→timestamp throttle map so a long session can't grow it without\n * bound (one entry per streamed segment). Values are timestamps (tiny), so the cap\n * is generous; delete-then-set refreshes recency, evict the oldest key when full.\n * Evicting a stale entry costs at most one extra highlight/dispatch — never\n * incorrect, since both debounce windows are far shorter than the cap horizon. */\n\n","/**\n * Progressive syntax highlighting for a code pane that is STILL STREAMING.\n *\n * The previous shape — `codeEl.textContent = content` on every token, plus a\n * debounced full re-highlight — made the pane flicker between plain and coloured:\n * assigning `textContent` destroys the highlighter's `<span>`s, so each token\n * erased the colours the last debounce had painted, and the reader saw plain text\n * most of the time with a coloured frame every 400ms. The debounce was not the\n * problem; rewriting the whole block on every token was.\n *\n * So the pane is split at the last newline:\n *\n * <pre><code> …highlighted COMPLETE lines… <span data-aparte-tail>partial line</span>\n *\n * A token only rewrites the tail, which costs one text assignment and leaves the\n * coloured prefix untouched. The prefix advances at most once per\n * `HIGHLIGHT_DEBOUNCE_MS`, and only when a new line has actually completed —\n * highlighting a half-written line is also what made the colours wrong as well as\n * flickering, because an unterminated string or brace re-tokenises everything\n * after it.\n *\n * `data-aparte-hl-len` on the pane is the boundary, and the DOM is deliberately\n * the source of truth for it rather than a module-level map: it makes the value\n * monotonic across a slow highlight that resolves out of order, and it cannot go\n * stale when the pane is rebuilt underneath us.\n */\n// Copied from core's `renderers/highlight-stream.ts` (the only other reader is the\n// code segment's own renderer): two readers is not yet a layer to export.\nimport { contextConfig, escapeHtml } from '@aparte/core';\n\n/** One highlight per pane per this window. Shiki costs 50-100ms per call. */\nconst HIGHLIGHT_DEBOUNCE_MS = 400;\n/** Bound on the throttle bookkeeping, so a long session cannot grow it forever. */\nconst MAX_THROTTLE_ENTRIES = 256;\n\nconst _lastHighlightAt = new Map<string, number>();\n\n/**\n * Record `id`'s last-seen time in a bounded, insertion-ordered map.\n *\n * Shared with the artifact's event-dispatch throttle, which is why it is generic\n * over the map rather than closing over one.\n */\nexport function markThrottle(map: Map<string, number>, id: string, at: number): void {\n map.delete(id);\n if (map.size >= MAX_THROTTLE_ENTRIES) {\n const oldest = map.keys().next().value;\n if (oldest !== undefined) map.delete(oldest);\n }\n map.set(id, at);\n}\n\n/** Where the coloured prefix ends. Absent or unparseable means \"nothing coloured\". */\nfunction highlightedLen(pane: HTMLElement): number {\n const n = Number(pane.dataset.aparteHlLen);\n return Number.isFinite(n) && n > 0 ? n : 0;\n}\n\n/**\n * Paint `content` into `paneSelector` while it streams.\n *\n * Call it on every token: the cheap half runs every time, the highlight is\n * throttled internally. The caller still runs one final, whole-content highlight\n * when the segment settles — this function deliberately never colours the last\n * line, so the settle pass is what completes it.\n */\nexport function streamHighlight(\n element: HTMLElement,\n paneSelector: string,\n content: string,\n lang: string,\n segId: string,\n): void {\n const pane = element.querySelector<HTMLElement>(paneSelector);\n if (!pane) return;\n\n const tail = pane.querySelector<HTMLElement>('[data-aparte-tail]');\n const hlLen = highlightedLen(pane);\n\n // ── every token: move the tail, and nothing else ──────────────────────\n if (tail && hlLen <= content.length) {\n tail.textContent = content.slice(hlLen);\n } else {\n // No coloured prefix yet, or the pane was rebuilt under us (a settle\n // highlight replaces the whole thing). Plain text, and forget the\n // boundary — self-healing beats a reset call at three call sites.\n delete pane.dataset.aparteHlLen;\n const codeEl = pane.querySelector('code');\n if (codeEl) codeEl.textContent = content;\n else pane.innerHTML = `<pre><code class=\"language-${escapeHtml(lang || 'text')}\">${escapeHtml(content)}</code></pre>`;\n }\n\n // ── throttled: advance the coloured prefix by whole lines ─────────────\n const cut = content.lastIndexOf('\\n') + 1;\n if (cut <= hlLen) return;\n const now = Date.now();\n if (now - (_lastHighlightAt.get(segId) ?? 0) < HIGHLIGHT_DEBOUNCE_MS) return;\n markThrottle(_lastHighlightAt, segId, now);\n\n // Resolved from the element, not the ambient config: this lands late, and by\n // then the render-time config is gone.\n void contextConfig(element).highlightCode(content.slice(0, cut), lang).then(html => {\n const live = element.querySelector<HTMLElement>(paneSelector);\n if (!live) return;\n // Monotonic. Two highlights can be in flight; the older one carries the\n // SHORTER prefix, and letting it land would visibly rewind the pane.\n if (cut <= highlightedLen(live)) return;\n live.innerHTML = html;\n const codeEl = live.querySelector('code') ?? live;\n const span = document.createElement('span');\n span.dataset.aparteTail = '';\n // The captured tail may already be one token behind; the next token fixes\n // it, and there is always a next one or a settle pass.\n span.textContent = content.slice(cut);\n codeEl.appendChild(span);\n live.dataset.aparteHlLen = String(cut);\n }).catch(() => { /* best-effort: a failed highlight degrades silently */ });\n}\n","/**\n * What the app decided at `setupArtifacts()`, read back by the card at render time.\n *\n * The card is resolved through core's ambient config (`contextConfig()`), not through\n * a closure, so the settings live beside the config they were given for: one entry per\n * `AparteConfig`, the global one by default. A chat with its own config that never\n * called `setupArtifacts` reads the global settings, which is what its renderers do\n * too.\n */\nimport { aparteGlobalConfig, type AparteConfig } from '@aparte/core';\nimport type { ArtifactSegment } from './segment.js';\nimport type { ArtifactToolOptions } from './tool.js';\n\n/** Builds the `srcdoc` of the sandboxed preview frame for a previewable kind. */\nexport type ArtifactPreviewBuilder = (kind: string, body: string, title: string) => string;\n\n/** The bytes a binary artifact (pdf, xlsx, docx) resolved to. */\nexport interface ArtifactBinary {\n /** The file. */\n buffer: BlobPart;\n /** Its MIME type, for the download. */\n mime: string;\n /** The name the download gets. */\n filename: string;\n /**\n * Optional HTML rendering of the file (a spreadsheet as a table, a PDF's text),\n * shown in the card's preview pane after sanitisation. Absent: the pane says so.\n */\n previewHtml?: string | null;\n}\n\n/**\n * Turn a binary artifact's source (the JS the model wrote to produce a workbook, a\n * PDF, a document) into bytes. Core owns no sandbox and no file generator: this is the\n * app's, and without it a binary artifact shows its source with no download and no\n * preview — declared nowhere, offered nowhere (ratified decision #8).\n */\nexport type ArtifactBinaryResolver = (artifact: ArtifactSegment) => Promise<ArtifactBinary>;\n\nexport interface ArtifactRenderOptions {\n /**\n * The Preview tab for previewable kinds (html, react, svg, js, css). `true`\n * (default) mounts a sandboxed frame on a gesture with the built-in document\n * builder; a function replaces the builder; `false` offers no preview at all.\n */\n preview?: boolean | ArtifactPreviewBuilder;\n /** See {@link ArtifactBinaryResolver}. */\n onBinary?: ArtifactBinaryResolver;\n}\n\nconst settings = new WeakMap<AparteConfig, ArtifactRenderOptions>();\n\nexport function setRenderOptions(config: AparteConfig, options: ArtifactRenderOptions): void {\n settings.set(config, options);\n}\n\n/** The options for this config, else the global config's, else the defaults. */\nexport function renderOptions(config: AparteConfig): ArtifactRenderOptions {\n return settings.get(config) ?? settings.get(aparteGlobalConfig) ?? {};\n}\n\n/** For tests and a teardown: forget what a config was told. */\nexport function clearRenderOptions(config: AparteConfig): void {\n settings.delete(config);\n}\n\n/**\n * Everything `setupArtifacts()` accepts — the tool's options, the card's, and the tag.\n *\n * ONE declaration, here, because there are two `setupArtifacts()`: the browser barrel's\n * and the node one. Each used to declare its own `ArtifactsSetupOptions`, and they were\n * not the same shape — the node copy omitted `ArtifactRenderOptions`, so `preview` and\n * `onBinary` were type errors against the SSR entry while being valid against the\n * browser one. A consumer typing a shared setup object got a different contract\n * depending on which condition resolved, from a name that reads as one thing.\n *\n * The server ignores `preview` and `onBinary` (it registers no renderer), and that is\n * correct: the same options object is meant to be written once and passed on both\n * sides. An option nobody reads there is inert, whereas a type error there is a wall.\n */\nexport interface ArtifactsSetupOptions extends ArtifactToolOptions, ArtifactRenderOptions {\n /**\n * The tag recognised in the prose — `<artifact …>…</artifact>` by default. `false`\n * registers no grammar: only the tool produces artifacts then.\n */\n tag?: string | false;\n}\n","/**\n * The binary-artifact path: pdf, xlsx, docx.\n *\n * A different LIFETIME from the card, and that is the seam. The card renders content\n * that arrives in the stream and is complete when the stream ends. This path renders a\n * file that something ELSE produces from that content — a sandbox that runs the JS the\n * model wrote and hands back a workbook, a PDF, a document. That something is the\n * app's (`onBinary` in `setupArtifacts`): the plugin knows how to ask and how to show\n * the answer, not how to make it.\n *\n * This file used to be a protocol — three window events, a redownload/rehydrate\n * pair of host handlers, a debounce against a sandbox that was \"busy\", and the name of\n * a service that existed in no package. What is left is a function call and its two\n * outcomes: the bytes, or an error shown in the card.\n */\nimport { escapeHtml, escapeAttr, contextConfig, type AparteConfig } from '@aparte/core';\nimport type { ArtifactSegment } from './segment.js';\nimport { stripCodeFences, labelForKind } from './shared.js';\nimport { streamHighlight } from './highlight.js';\nimport { renderOptions, type ArtifactBinary } from './options.js';\n\nconst FILE_ICON_LABEL: Record<string, string> = {\n xlsx: 'XLS',\n pdf: 'PDF',\n docx: 'DOC',\n};\n\n/**\n * Bytes already produced, by segment id — so a re-mount (a branch switch, a\n * conversation toggle) shows the file again without asking the app to make it twice.\n * Bounded: each entry holds a whole file.\n */\nconst produced = new Map<string, ArtifactBinary>();\nconst MAX_PRODUCED = 24;\n/** One generation in flight per segment, whatever re-mounts meanwhile. */\nconst inFlight = new Map<string, Promise<ArtifactBinary>>();\n\nfunction remember(id: string, bin: ArtifactBinary): void {\n produced.delete(id);\n if (produced.size >= MAX_PRODUCED) {\n const oldest = produced.keys().next().value;\n if (oldest !== undefined) produced.delete(oldest);\n }\n produced.set(id, bin);\n}\n\n/** For tests: forget every produced file and every generation in flight. */\nexport function resetBinaryArtifacts(): void {\n produced.clear();\n inFlight.clear();\n}\n\nexport function renderBinaryFileArtifact(segment: ArtifactSegment, kind: string): string {\n const cfg = contextConfig();\n const title = segment.title?.trim() || labelForKind(kind);\n const iconLabel = FILE_ICON_LABEL[kind] ?? kind.toUpperCase();\n const isStreaming = !!segment.isStreaming;\n const canProduce = typeof renderOptions(cfg).onBinary === 'function';\n const downloadLabel = escapeHtml(cfg.t('download'));\n const done = produced.get(segment.id);\n\n // Already produced (a re-mount): the file, at once.\n if (done && !isStreaming) {\n const preview = previewMarkup(done, kind, cfg); // safe-text: the app's previewHtml through the config's sanitizer, or an escapeHtml'd sentence — markup on purpose, as swapToPreview() sets it\n return `\n <div class=\"aparte-segment aparte-card aparte-segment-artifact-file\"\n data-segment-id=\"${escapeHtml(segment.id)}\"\n data-artifact-type=\"${escapeHtml(kind)}\"\n data-state=\"ready\">\n <div class=\"aparte-art-file__card\">\n <div class=\"aparte-art-file__icon\" data-kind=\"${escapeHtml(kind)}\">${escapeHtml(iconLabel)}</div>\n <div class=\"aparte-art-file__meta\">\n <div class=\"aparte-art-file__meta-name\" data-role=\"file-name\">${escapeHtml(done.filename)}</div>\n <div class=\"aparte-art-file__meta-sub\" data-role=\"file-sub\">${escapeHtml(formatBytes(byteLength(done.buffer)))} · ${escapeHtml(kind.toUpperCase())}</div>\n </div>\n <div class=\"aparte-art-file__actions\">\n <button type=\"button\" class=\"aparte-btn aparte-btn--primary aparte-btn--solid aparte-art-file__btn aparte-art-file__btn--primary\" data-action=\"download\">${downloadLabel}</button>\n </div>\n </div>\n <div class=\"aparte-art-file__body\">\n <div class=\"aparte-art-file__code-pane\" data-role=\"code-pane\" hidden>\n <pre><code class=\"language-js\"></code></pre>\n </div>\n <div class=\"aparte-art-file__preview-pane\" data-role=\"preview-pane\">${preview}</div>\n </div>\n </div>\n `;\n }\n\n const cleanContent = stripCodeFences(segment.content || '');\n // Streaming: the model is still writing the source. Settled with a producer: the\n // file is being made. Settled without one: the source is all there is to show.\n const subText = isStreaming ? cfg.t('generating') : canProduce ? cfg.t('rebuildingPreview') : kind.toUpperCase();\n return `\n <div class=\"aparte-segment aparte-card aparte-segment-artifact-file\"\n data-segment-id=\"${escapeHtml(segment.id)}\"\n data-artifact-type=\"${escapeHtml(kind)}\"\n data-state=\"${escapeAttr(isStreaming ? 'streaming' : canProduce ? 'compiling' : 'source')}\">\n <div class=\"aparte-art-file__card\">\n <div class=\"aparte-art-file__icon\" data-kind=\"${escapeHtml(kind)}\">${escapeHtml(iconLabel)}</div>\n <div class=\"aparte-art-file__meta\">\n <div class=\"aparte-art-file__meta-name\" data-role=\"file-name\">${escapeHtml(title)}</div>\n <div class=\"aparte-art-file__meta-sub\" data-role=\"file-sub\">${escapeHtml(subText)}</div>\n </div>\n <div class=\"aparte-art-file__actions\">\n ${canProduce ? `<button type=\"button\" class=\"aparte-btn aparte-btn--primary aparte-btn--solid aparte-art-file__btn aparte-art-file__btn--primary\" data-action=\"download\" disabled>${downloadLabel}</button>` : ''}\n </div>\n </div>\n <div class=\"aparte-art-file__body\">\n <div class=\"aparte-art-file__code-pane\" data-role=\"code-pane\">\n <pre><code class=\"language-js\">${escapeHtml(cleanContent)}</code></pre>\n </div>\n <div class=\"aparte-art-file__preview-pane\" data-role=\"preview-pane\" hidden></div>\n </div>\n </div>\n `;\n}\n\nexport function setupBinaryFileArtifact(element: HTMLElement, segment: ArtifactSegment, kind: string): void {\n if (element.dataset['aparteInit'] !== 'true') {\n element.dataset['aparteInit'] = 'true';\n element.addEventListener('click', (ev) => {\n const target = ev.target as HTMLElement;\n const action = target.closest<HTMLElement>('[data-action]')?.getAttribute('data-action');\n if (action !== 'download') return;\n const bin = produced.get(segment.id);\n if (bin) downloadBinary(bin);\n });\n }\n if (segment.isStreaming) return;\n settle(element, segment, kind);\n}\n\nexport function updateBinaryFileArtifact(element: HTMLElement, segment: ArtifactSegment, isStreaming: boolean): void {\n const state = element.getAttribute('data-state');\n if (state === 'ready' || state === 'error') return;\n\n const cleanContent = stripCodeFences(segment.content || '');\n if (isStreaming) {\n streamHighlight(element, '[data-role=\"code-pane\"]', cleanContent, 'js', segment.id);\n return;\n }\n const codeEl = element.querySelector<HTMLElement>('[data-role=\"code-pane\"] code');\n if (codeEl) codeEl.textContent = cleanContent;\n if (state === 'streaming') settle(element, segment, kind(element));\n}\n\n/**\n * The source is final. Highlight it, and — when the app can — ask for the file.\n * One request per segment whatever re-mounts meanwhile; the answer is remembered so a\n * later mount shows the file without asking again.\n */\nfunction settle(element: HTMLElement, segment: ArtifactSegment, kind: string): void {\n const cfg = contextConfig(element);\n const cleanContent = stripCodeFences(segment.content || '');\n const wrapper = element.querySelector<HTMLElement>('[data-role=\"code-pane\"]');\n if (wrapper) {\n void cfg.highlightCode(cleanContent, 'js').then(html => { wrapper.innerHTML = html; })\n .catch(() => { /* best-effort: a failed highlight degrades silently */ });\n }\n\n const already = produced.get(segment.id);\n if (already) { swapToPreview(element, already, kind); return; }\n\n const resolve = renderOptions(cfg).onBinary;\n if (!resolve) { element.setAttribute('data-state', 'source'); return; }\n element.setAttribute('data-state', 'compiling');\n\n let job = inFlight.get(segment.id);\n if (!job) {\n job = resolve({ ...segment, content: cleanContent });\n inFlight.set(segment.id, job);\n // Both branches, or the bookkeeping chain itself rejects unhandled on failure.\n const done = (): void => { inFlight.delete(segment.id); };\n job.then(done, done);\n }\n void job.then((bin) => {\n remember(segment.id, bin);\n if (element.isConnected) swapToPreview(element, bin, kind);\n }).catch((err: unknown) => {\n if (element.isConnected) showError(element, err instanceof Error ? err.message : String(err));\n });\n}\n\nfunction kind(element: HTMLElement): string {\n return (element.getAttribute('data-artifact-type') || '').toLowerCase();\n}\n\n/**\n * The config is passed IN, never resolved here.\n *\n * This is the one sink in the file that puts app-supplied HTML on the page, and it\n * read the ambient config — which, from `swapToPreview`'s promise callback, is the\n * global one long after the render that set it. A chat with its own `AparteConfig`\n * therefore had its `setHtmlSanitizer` skipped at exactly this line, and its locale\n * skipped at the sentence beside it. Both callers already hold the right answer:\n * the render path is inside the bubble's `runWithConfig`, the swap path has the\n * element.\n */\nfunction previewMarkup(bin: ArtifactBinary, kind: string, cfg: AparteConfig): string {\n return bin.previewHtml\n ? cfg.sanitizeHtml(bin.previewHtml)\n : `<div class=\"aparte-art-file__preview-empty\">${escapeHtml(cfg.t('previewPending'))} ${escapeHtml(kind)}</div>`;\n}\n\nfunction swapToPreview(element: HTMLElement, bin: ArtifactBinary, kind: string): void {\n element.setAttribute('data-state', 'ready');\n const codePane = element.querySelector<HTMLElement>('[data-role=\"code-pane\"]');\n if (codePane) codePane.hidden = true;\n const preview = element.querySelector<HTMLElement>('[data-role=\"preview-pane\"]');\n if (preview) {\n // `previewHtml` comes from the app and is built from file bytes the model's code\n // produced, so it goes through the sanitizer like every other innerHTML here.\n preview.innerHTML = previewMarkup(bin, kind, contextConfig(element));\n preview.hidden = false;\n }\n const nameEl = element.querySelector<HTMLElement>('[data-role=\"file-name\"]');\n if (nameEl) nameEl.textContent = bin.filename;\n const sub = element.querySelector<HTMLElement>('[data-role=\"file-sub\"]');\n if (sub) sub.textContent = `${formatBytes(byteLength(bin.buffer))} · ${kind.toUpperCase()}`;\n const dlBtn = element.querySelector<HTMLButtonElement>('[data-action=\"download\"]');\n if (dlBtn) dlBtn.disabled = false;\n}\n\nfunction downloadBinary(bin: ArtifactBinary): void {\n const blob = new Blob([bin.buffer], { type: bin.mime });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = bin.filename;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n}\n\nfunction byteLength(part: BlobPart): number {\n if (typeof part === 'string') return new TextEncoder().encode(part).length;\n if (part instanceof Blob) return part.size;\n return (part as ArrayBufferView | ArrayBuffer).byteLength;\n}\n\nfunction formatBytes(n: number): string {\n if (n < 1024) return `${n} B`;\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;\n return `${(n / (1024 * 1024)).toFixed(2)} MB`;\n}\n\nfunction showError(element: HTMLElement, errorMsg: string): void {\n if (element.getAttribute('data-state') === 'ready') return;\n element.setAttribute('data-state', 'error');\n const cfg = contextConfig(element);\n const sub = element.querySelector<HTMLElement>('[data-role=\"file-sub\"]');\n if (sub) sub.textContent = cfg.t('sandboxError');\n const body = element.querySelector<HTMLElement>('.aparte-art-file__body');\n if (body) {\n // The first line of a stack is the actionable one.\n const short = (errorMsg.split('\\n')[0] ?? '').slice(0, 240);\n body.innerHTML = `\n <div class=\"aparte-art-file__error\">\n <div class=\"aparte-art-file__error-title\">${escapeHtml(cfg.t('sandboxError'))}</div>\n <div class=\"aparte-output aparte-art-file__error-msg\">${escapeHtml(short)}</div>\n <div class=\"aparte-art-file__error-hint\">${escapeHtml(cfg.t('sandboxErrorHint'))}</div>\n </div>\n `;\n }\n}\n","export default \"/*\\n * @aparte/plugin-artifacts — the artifact card and the artifact file preview.\\n *\\n * Injected once per document through the renderer's `getStyles()`, the seam core\\n * keeps for a renderer that is not core's. It reads core's tokens (`--aparte-space-*`,\\n * `--aparte-surface-*`, `--aparte-code-bg`…) and declares its own below, on the two\\n * roots it draws, so a consumer overrides them the way they override any other:\\n * `.aparte-segment-artifact-card { --aparte-art-paper-bg: … }`.\\n */\\n.aparte-segment-artifact-card,\\n.aparte-segment-artifact-file {\\n /* Paper. An artifact preview is a DOCUMENT shown inside the chat — a spreadsheet,\\n a rendered page — so it stays light whatever the app theme is, the way a PDF\\n viewer shows a white page in a dark editor. */\\n --aparte-art-paper-bg: #fff;\\n --aparte-art-paper-text: #1f2937;\\n --aparte-art-paper-row-alt: #f9fafb;\\n --aparte-art-paper-head-bg: #f3f4f6;\\n --aparte-art-paper-head-text: #111827;\\n --aparte-art-paper-border: rgba(0, 0, 0, 0.1);\\n /* The paper's OWN muted ink. The empty-preview line used to take\\n `--aparte-text-muted`, which is the THEME's — and the pane opts out of the\\n theme on purpose, so in the dark theme a #a89bb6 line landed on white paper at\\n 2.62:1. Mixed from the paper's own pair instead: 5.5:1, and it follows a\\n consumer who re-declares the paper. */\\n --aparte-art-paper-text-muted: color-mix(in srgb, var(--aparte-art-paper-text) 70%, var(--aparte-art-paper-bg));\\n /* File-type tiles. Brand colours, so they are literals on purpose — but named,\\n because an app with its own file-type palette has nowhere else to put it. The\\n lettering is fixed like the tiles: a themed ink would go near-black in dark\\n mode, on a dark green tile. */\\n --aparte-art-file-icon-bg: linear-gradient(135deg, #1d6f42, #0f5132);\\n --aparte-art-file-icon-color: #fff;\\n --aparte-art-file-icon-bg-pdf: linear-gradient(135deg, #c0392b, #7d1f17);\\n --aparte-art-file-icon-bg-docx: linear-gradient(135deg, #1e5288, #0f3060);\\n --aparte-art-file-error-msg-bg: rgba(0, 0, 0, 0.04);\\n /* Sizes. */\\n --aparte-art-card-header-min-height: 36px;\\n --aparte-art-card-pulse-size: 8px;\\n --aparte-art-card-btn-size: 28px;\\n --aparte-art-file-icon-size: 40px;\\n --aparte-art-file-preview-padding: 20px;\\n --aparte-art-file-error-padding-inline: 18px;\\n}\\n/* Not the paper — that stays light on purpose. This is a code block inside the\\n error panel, which does follow the theme.\\n\\n Twice, because CSS cannot share one block between an attribute selector and a\\n media query — the same duplication core's theme.css carries, for the same reason:\\n without the second one a system-dark reader who sets no attribute kept the LIGHT\\n wash (`rgba(0,0,0,0.04)`) on the already-dark `--aparte-error-bg`. Edit both. */\\n[data-aparte-theme=\\\"dark\\\"] .aparte-segment-artifact-file {\\n --aparte-art-file-error-msg-bg: rgba(255, 255, 255, 0.06);\\n}\\n@media (prefers-color-scheme: dark) {\\n :root:not([data-aparte-theme=\\\"light\\\"]) .aparte-segment-artifact-file {\\n --aparte-art-file-error-msg-bg: rgba(255, 255, 255, 0.06);\\n }\\n}\\n/* Artifact card */\\n/* The shell is `.aparte-card`; what stays here is what a card in a TRANSCRIPT needs\\n and a card in general does not — the vertical rhythm between messages, and `font:\\n inherit` so a segment never picks up a host's form-control font. */\\n.aparte-segment-artifact-card,\\n.aparte-segment-artifact-file {\\n margin: var(--aparte-space-4) 0;\\n font: inherit;\\n}\\n.aparte-art-card__header {\\n display: flex; align-items: center; justify-content: space-between;\\n padding: var(--aparte-space-4) var(--aparte-space-5);\\n border-bottom: var(--aparte-border-width) solid var(--aparte-border);\\n background: var(--aparte-surface-2);\\n min-height: var(--aparte-art-card-header-min-height);\\n}\\n.aparte-art-card__title-block { display: flex; align-items: center; gap: var(--aparte-space-4); min-width: 0; }\\n/* An outline `.aparte-badge`. What stays is the lettering: a language tag reads as a\\n code marker, so it is upper-cased and tracked out — which a badge in general is not. */\\n.aparte-art-card__kind {\\n text-transform: uppercase;\\n letter-spacing: 0.04em;\\n --aparte-badge-font-size: var(--aparte-font-size-xs);\\n --aparte-badge-radius: var(--aparte-radius-sm);\\n}\\n.aparte-art-card__title {\\n font-size: var(--aparte-font-size-base); font-weight: var(--aparte-font-weight-medium);\\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\\n}\\n/* The geometry is `.aparte-dot`; a streaming card breathes on the accent rather than\\n the status colour, and larger, because it marks a whole card and not a line. */\\n.aparte-art-card__pulse {\\n --aparte-status-dot-size: var(--aparte-art-card-pulse-size);\\n --aparte-status-color: var(--aparte-accent);\\n}\\n.aparte-art-card__actions { display: flex; gap: var(--aparte-space-2); }\\n.aparte-art-card__btn {\\n /* Same as .aparte-action-btn: declare the size, let the recipe draw. */\\n --aparte-btn-size: var(--aparte-art-card-btn-size);\\n}\\n.aparte-art-card__btn:hover:not(:disabled) {\\n background: var(--aparte-surface-hover);\\n border-color: var(--aparte-border);\\n color: var(--aparte-text);\\n}\\n.aparte-art-card__btn:disabled { opacity: var(--aparte-disabled-opacity); cursor: not-allowed; }\\n.aparte-art-card__tabs {\\n /* justify-content and the padding are DECLARED, not left to a default.\\n Core is light DOM on purpose - no shadow root, no ::part(), any selector\\n reaches in - and the corollary is that a component must state what its\\n layout depends on, because an undeclared property has nothing to override\\n a host rule with. A consuming page with a bare nav selector setting\\n justify-content: space-between and padding-top (this library's own docs\\n site had exactly that) otherwise pushes these two tabs to opposite ends of\\n the card and pads the row out.\\n\\n flex-end, and Code first in the DOM. The card OPENS on Code - mounting the\\n preview would execute model-authored code with no gesture (ratified\\n decision #8) - and a selected tab sitting second reads backwards. Right,\\n because the header above puts the artifact's identity on the left and its\\n copy/download buttons on the right, so this keeps every control in one\\n column. DOM order is also keyboard order, so the tab a reader lands on\\n first is the one already showing. */\\n display: flex; justify-content: flex-end; align-items: stretch; gap: var(--aparte-space-1);\\n padding: var(--aparte-space-2) var(--aparte-space-4) 0;\\n border-bottom: var(--aparte-border-width) solid var(--aparte-border);\\n background: var(--aparte-surface-2);\\n}\\n/* The tabs are `.aparte-tabs--underline`, like the elicitation panel's steps — one\\n tab in this library, not one per component. Only the row's own business stays. */\\n.aparte-art-card__tabs button { font-size: var(--aparte-font-size-sm); }\\n/* The card's heights, as variables with ONE owner each.\\n They were four hardcoded numbers in the only part of this card that did not\\n use a variable - everything else here already reads var(--aparte-code-bg) and\\n friends - and two of them had to agree while a third contradicted a fourth:\\n the code pane repeated the body's 600px, and the \\\"press Preview\\\" placeholder\\n was 120px tall inside a body whose min-height said 80, so that minimum applied\\n to nothing.\\n The frame stays a FIXED height rather than an aspect ratio, which is what\\n embeds of arbitrary HTML actually do - CodeSandbox documents 500px, StackBlitz\\n takes a height parameter - because a frame with an opaque origin cannot be\\n measured and a 16/10 ratio on a wide card is enormous. The vh cap is the part\\n that was missing: a fixed 480px should not own a phone screen.\\n Each default lives in its read, as var(--x, literal), the way every other\\n value in this file already does - not in a declaration block on top of the\\n fallbacks, which would be two owners of one number again. It also means the\\n docs' CSS-variable generator finds them: its sweep looks for reads that\\n carry a fallback, so a read without one is a public knob nobody documents. */\\n.aparte-art-card__body {\\n position: relative;\\n /* The placeholder is the tallest thing this box can hold while empty, so it\\n IS the minimum - one number instead of two that disagreed. */\\n min-height: var(--aparte-artifact-pending-height, 120px);\\n max-height: var(--aparte-artifact-body-max, 600px);\\n overflow: hidden;\\n}\\n.aparte-art-card__pane { display: none; height: 100%; }\\n.aparte-segment-artifact-card[data-tab=\\\"code\\\"] .aparte-art-card__pane[data-pane=\\\"code\\\"] { display: block; }\\n.aparte-segment-artifact-card[data-tab=\\\"preview\\\"] .aparte-art-card__pane[data-pane=\\\"preview\\\"] { display: block; }\\n.aparte-art-card__pane[data-pane=\\\"code\\\"] {\\n /* The body already caps this; repeating the number was the second owner. */\\n max-height: var(--aparte-artifact-body-max, 600px); overflow: auto;\\n}\\n.aparte-art-card__frame {\\n display: block;\\n width: 100%;\\n height: min(var(--aparte-artifact-frame-height, 480px), var(--aparte-artifact-frame-max, 70vh));\\n border: 0;\\n background: var(--aparte-art-paper-bg);\\n}\\n.aparte-art-card__pending {\\n display: flex; align-items: center; justify-content: center;\\n height: var(--aparte-artifact-pending-height, 120px);\\n color: var(--aparte-text-muted);\\n font-size: var(--aparte-font-size-md);\\n font-style: italic;\\n}\\n/* ── Binary file artifact (xlsx/pdf/docx) ──────────────────── */\\n.aparte-art-file__card {\\n display: flex; align-items: center; gap: var(--aparte-space-6);\\n padding: var(--aparte-space-6) var(--aparte-space-7);\\n background: var(--aparte-surface-2);\\n border-bottom: var(--aparte-border-width) solid var(--aparte-border);\\n}\\n.aparte-art-file__body {\\n position: relative;\\n}\\n.aparte-art-file__code-pane {\\n max-height: var(--aparte-artifact-file-code-max, 360px); overflow: auto;\\n background: var(--aparte-code-bg);\\n}\\n.aparte-art-file__preview-pane {\\n max-height: var(--aparte-artifact-file-preview-max, 460px);\\n overflow: auto;\\n background: var(--aparte-art-paper-bg);\\n /* Preview is a document view — force light scheme regardless of\\n the app theme, with a dark text colour so cells stay readable. */\\n color: var(--aparte-art-paper-text);\\n}\\n.aparte-art-file__icon {\\n width: var(--aparte-art-file-icon-size); height: var(--aparte-art-file-icon-size);\\n border-radius: var(--aparte-radius-lg);\\n display: flex; align-items: center; justify-content: center;\\n background: var(--aparte-art-file-icon-bg);\\n color: var(--aparte-art-file-icon-color);\\n font-weight: var(--aparte-font-weight-bold);\\n font-size: var(--aparte-font-size-sm);\\n letter-spacing: 0.04em;\\n flex-shrink: 0;\\n}\\n.aparte-art-file__icon[data-kind=\\\"pdf\\\"] { background: var(--aparte-art-file-icon-bg-pdf); }\\n.aparte-art-file__icon[data-kind=\\\"docx\\\"] { background: var(--aparte-art-file-icon-bg-docx); }\\n.aparte-art-file__meta { flex: 1 1 auto; min-width: 0; }\\n.aparte-art-file__meta-name {\\n font-weight: var(--aparte-font-weight-semibold); font-size: var(--aparte-font-size-base);\\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\\n}\\n.aparte-art-file__meta-sub {\\n font-size: var(--aparte-font-size-sm);\\n color: var(--aparte-text-muted);\\n}\\n.aparte-art-file__actions { display: flex; gap: var(--aparte-space-3); flex-shrink: 0; }\\n/* The chrome is `.aparte-btn`; every one of these is a `--primary --solid`, so the\\n fill comes from the recipe. Its own measurements stay: a labelled button in a card\\n is roomier than an icon in a row. */\\n.aparte-art-file__btn {\\n padding: var(--aparte-space-3) var(--aparte-space-5);\\n font-size: var(--aparte-font-size-md);\\n}\\n/* NOTHING here repaints the button, and the comment above is why. These seven lines\\n re-declared `background`, `color`, `border-color` and a hover the recipe already\\n paints — a migration leftover that contradicted the sentence directly above it.\\n\\n Five of them were inert duplicates. The sixth was not: `color: var(--aparte-text-inverse)`\\n overrode `--aparte-btn-on-intent`, which the recipe derives from the fill. Measured in a\\n browser on the built stylesheet: 3.54:1 in the light theme against the recipe's 5.27 —\\n an AA failure on a visible button label, in the default theme. It was also the last\\n place in `styles/` forcing `--aparte-text-inverse` as ink on a coloured fill; badge and\\n field had already stopped.\\n\\n The consumer cost was the sharper one: the one-attribute rebrand this library documents\\n re-derives the ink on every other solid-primary button and, here alone, kept a token\\n bound to core's own palette. `.aparte-art-file__btn:hover` went with them — at 0,2,0 it\\n was always beaten by `.aparte-btn--solid:hover:not(:disabled)` at 0,3,0, so it never\\n applied either. */\\n/* Hide the download button until the sandbox has produced the buffer.\\n Avoids confusing the user with a disabled-but-styled-primary button\\n during streaming / compiling. */\\n.aparte-segment-artifact-file:not([data-state=\\\"ready\\\"]) .aparte-art-file__btn[data-action=\\\"download\\\"] {\\n display: none;\\n}\\n.aparte-art-file__preview-pane th,\\n.aparte-art-file__preview-pane td {\\n border: var(--aparte-border-width) solid var(--aparte-art-paper-border);\\n padding: var(--aparte-space-3) var(--aparte-space-5);\\n text-align: start;\\n white-space: nowrap;\\n color: var(--aparte-art-paper-text);\\n background: var(--aparte-art-paper-bg);\\n}\\n.aparte-art-file__preview-pane tr:nth-child(odd) td { background: var(--aparte-art-paper-row-alt); }\\n.aparte-art-file__preview-pane tr:first-child td {\\n background: var(--aparte-art-paper-head-bg);\\n font-weight: var(--aparte-font-weight-semibold);\\n position: sticky; top: 0;\\n color: var(--aparte-art-paper-head-text);\\n}\\n.aparte-art-file__preview-empty {\\n padding: var(--aparte-art-file-preview-padding); text-align: center;\\n color: var(--aparte-art-paper-text-muted);\\n font-size: var(--aparte-font-size-md); font-style: italic;\\n}\\n.aparte-segment-artifact-file[data-state=\\\"error\\\"] .aparte-art-file__icon {\\n background: var(--aparte-art-file-icon-bg-pdf);\\n}\\n.aparte-art-file__error {\\n padding: var(--aparte-space-8) var(--aparte-art-file-error-padding-inline);\\n background: var(--aparte-error-bg);\\n border-top: var(--aparte-border-width) solid var(--aparte-error-border);\\n color: var(--aparte-text);\\n}\\n.aparte-art-file__error-title {\\n font-weight: var(--aparte-font-weight-semibold);\\n font-size: var(--aparte-font-size-base);\\n margin-bottom: var(--aparte-space-3);\\n /* `--aparte-error-text`, not `--aparte-error-title`: core removed the second one\\n with the error renderer's private classes and nothing here followed, so this\\n read was invalid at computed-value time and the heading quietly took the\\n panel's body ink instead of the error ink. */\\n color: var(--aparte-error-text);\\n}\\n/* A computed value shown back, which is `.aparte-output`. What stays is that this\\n one is an ERROR — tinted text on its own quiet ground — and that a raw message has\\n no spaces to wrap on, so it must be allowed to break mid-word. */\\n.aparte-art-file__error-msg {\\n display: block;\\n padding: var(--aparte-space-3) var(--aparte-space-5);\\n margin-bottom: var(--aparte-space-4);\\n background: var(--aparte-art-file-error-msg-bg);\\n color: var(--aparte-error-text);\\n word-break: break-word;\\n}\\n.aparte-art-file__error-hint {\\n font-size: var(--aparte-font-size-sm);\\n color: var(--aparte-text-muted);\\n font-style: italic;\\n}\\n/* ── The card's code and table panes ─────────────────────────\\n These three lived in core's prose sheet, styling this card's DOM from a sheet\\n that knew nothing else about it. */\\n.aparte-art-card__pane[data-pane=\\\"code\\\"] pre {\\n margin: 0; padding: var(--aparte-space-6);\\n font-size: var(--aparte-font-size-md);\\n background: var(--aparte-code-bg);\\n}\\n.aparte-art-file__code-pane pre {\\n margin: 0; padding: var(--aparte-space-6);\\n font-size: var(--aparte-font-size-md);\\n font-family: var(--aparte-code-font-family);\\n}\\n.aparte-art-file__preview-pane table {\\n border-collapse: collapse;\\n width: 100%;\\n font-size: var(--aparte-font-size-md);\\n font-family: inherit;\\n}\\n\"","/**\n * The artifact card: the inline Code/Preview panel a streamed artifact renders as.\n *\n * One thing, at length — and it used to be twice this size, half of it a `getStyles()`\n * block, which is why it kept `segment-renderers.ts` at 1900 lines while that file's own\n * banners described it as a renderer registry plus nine small renderers. It is that now,\n * and the CSS has since gone to `styles/segment/artifact.css` with every other built-in's.\n *\n * The card owns the previewable path. When a segment's kind is binary (pdf/xlsx/docx)\n * it DELEGATES to `./binary-file.ts` — which is why `BINARY_FILE_KINDS` lives\n * here despite the name: the card is what consults it to decide to hand over. A\n * call-site census settled that; grouping it as \"shared\" was the first draft's mistake.\n *\n * The preview frame is deliberately not built at render time. It is mounted only when\n * the user presses Preview (`mountPreviewFrame`), because a previewable artifact is\n * model-authored code and mounting it unasked executes it — ratified decision #8\n * applied to a tier-(c) affordance. The document it mounts comes from\n * `./preview-document.ts`, or from the consumer's own builder.\n */\nimport { escapeHtml, escapeAttr, copyText, contextConfig } from '@aparte/core';\nimport type { AparteSegmentRenderer } from '@aparte/core';\nimport { ARTIFACT_SEGMENT_TYPE, type ArtifactSegment } from './segment.js';\nimport { deriveArtifactKind } from './kinds.js';\nimport { stripCodeFences, labelForKind } from './shared.js';\nimport { streamHighlight } from './highlight.js';\nimport { PREVIEW_CSP, buildSafePreviewDocument } from './preview-document.js';\nimport { renderOptions } from './options.js';\nimport {\n renderBinaryFileArtifact,\n setupBinaryFileArtifact,\n updateBinaryFileArtifact,\n} from './binary-file.js';\nimport artifactStyles from './artifact.css?raw';\n\n/** The segment shape the card draws — exported for a consumer's own tests and renderers. */\nexport type AparteArtifactSegment = ArtifactSegment;\n\nconst PREVIEWABLE_KINDS: ReadonlySet<string> = new Set(['react', 'html', 'svg', 'js', 'css']);\n\n/**\n * One id space per rendered card, monotonic — the same shape `aparte-optgroup` uses\n * for its label ids, and for the same reason: the id has to be UNIQUE on the page,\n * and nothing about the content can promise that.\n */\nlet cardIdSeq = 0;\n\n/** Binary file kinds: their source is implementation noise; the app produces the file. */\nconst BINARY_FILE_KINDS: ReadonlySet<string> = new Set(['pdf', 'xlsx', 'docx']);\n\n/** Whether the Preview tab exists at all, per the app's `preview` option. */\nfunction previewEnabled(): boolean {\n return renderOptions(contextConfig()).preview !== false;\n}\n\nexport const artifactRenderer: AparteSegmentRenderer<ArtifactSegment> = {\n type: ARTIFACT_SEGMENT_TYPE,\n render: (segment: ArtifactSegment) => {\n const kind = (segment.artifactType || 'unknown').toLowerCase();\n // Binary file kinds (xlsx/pdf/docx) follow a separate track: the generated JS\n // is implementation noise, so the file card shows progress, then the file.\n if (BINARY_FILE_KINDS.has(kind)) {\n return renderBinaryFileArtifact(segment, kind);\n }\n const title = segment.title?.trim() || labelForKind(kind);\n const displayLang = languageForKind(kind);\n const isStreaming = !!segment.isStreaming;\n const previewable = PREVIEWABLE_KINDS.has(kind) && previewEnabled();\n // `t()`, not `getLocale().x ?? 'X'`: `t()` already falls back to\n // APARTE_DEFAULT_LOCALE, so the English lives in ONE place instead of being\n // re-typed at each call site — and `setLocale()` REPLACES rather than merges,\n // which makes a partial locale the normal case rather than an edge one.\n // `download` is title AND aria-label: they disagreed on the button next door,\n // whose title went through `t('copy')` while its aria-label said \"Copy\" in\n // every language.\n const cfg = contextConfig();\n const downloadLabel = cfg.t('download');\n const previewLabel = cfg.t('preview');\n const codeLabel = cfg.t('code');\n const isBinary = BINARY_FILE_KINDS.has(kind);\n const cleanContent = stripCodeFences(segment.content || '');\n /*\n * Ids, so the tabs can POINT at their panels.\n *\n * The card announced `role=\"tablist\"` with `role=\"tab\"` buttons and shipped none\n * of the pattern's obligations: no `aria-controls`, no `role=\"tabpanel\"`, no ids\n * and no arrow keys. A role is a promise about behaviour — declaring tablist and\n * then behaving like two ordinary buttons tells a screen-reader user to expect a\n * relationship and a keyboard model that are not there, which is worse than the\n * plain buttons it actually was.\n *\n * A COUNTER, not the segment's id. The segment's id is the model's — a tool-call\n * segment is `tool-${toolCallId}` straight off the wire — so two calls answering\n * to one id gave two cards the same `id` and the same `aria-controls`, and\n * `getElementById` returns whichever parsed first: exactly the collision this\n * scoping exists to prevent. An author-chosen `id` is also the DOM-clobbering\n * shape core's sanitizer refuses on model markup.\n *\n * Escaped HERE rather than inside an id-building helper: `check:attr-escaping`\n * follows a local produced by an escaper and cannot see through a function, and\n * teaching it to trust the helper's NAME is precisely the hole that guard was\n * just tightened to close. (The value is digits; the escaper is what the reader\n * and the guard are owed, since the position is what makes the rule.)\n */\n const cardId = escapeAttr(String(++cardIdSeq));\n // The card ALWAYS opens on the code tab, and the preview frame is not built\n // here at all — it is mounted only when the user presses Preview\n // (`mountPreviewFrame`, called from the tab handler).\n //\n // It used to open on Preview for any artifact that was not streaming, i.e.\n // every render of a completed one — so reloading a persisted conversation\n // executed the model's JS with no gesture. Defaulting the tab is not enough\n // on its own either: a `display:none` iframe still loads and still runs\n // scripts, so the frame has to be ABSENT, not hidden.\n //\n // Ratified decision #8, applied to a tier-(c) affordance: content the app\n // did not produce does not get to act on its own.\n\n return `\n <div class=\"aparte-segment aparte-card aparte-segment-artifact-card\"\n data-segment-id=\"${escapeHtml(segment.id)}\"\n data-artifact-type=\"${escapeHtml(kind)}\"\n data-streaming=\"${isStreaming ? 'true' : 'false'}\"\n data-tab=\"code\"\n data-previewable=\"${previewable ? 'true' : 'false'}\"\n data-binary=\"${isBinary ? 'true' : 'false'}\">\n <header class=\"aparte-art-card__header\">\n <div class=\"aparte-art-card__title-block\">\n <span class=\"aparte-badge aparte-badge--outline aparte-art-card__kind\" data-kind=\"${escapeHtml(kind)}\">${escapeHtml(displayLang)}</span>\n <span class=\"aparte-art-card__title\">${escapeHtml(title)}</span>\n ${isStreaming ? `<span class=\"aparte-dot aparte-art-card__pulse\" role=\"img\" aria-label=\"${escapeAttr(cfg.t('generating'))}\"></span>` : ''}\n </div>\n <div class=\"aparte-art-card__actions\">\n <button type=\"button\" class=\"aparte-btn aparte-btn--icon aparte-art-card__btn\" data-action=\"copy\" title=\"${escapeAttr(contextConfig().t('copy'))}\" aria-label=\"${escapeAttr(contextConfig().t('copy'))}\">\n ${contextConfig().getIcon('copy')}\n </button>\n <button type=\"button\" class=\"aparte-btn aparte-btn--icon aparte-art-card__btn\" data-action=\"download\" title=\"${escapeAttr(downloadLabel)}\" aria-label=\"${escapeAttr(downloadLabel)}\" ${isStreaming ? 'disabled' : ''}>\n ${contextConfig().getIcon('download')}\n </button>\n </div>\n </header>\n <nav class=\"aparte-tabs aparte-tabs--underline aparte-art-card__tabs\" role=\"tablist\">\n <button type=\"button\" class=\"aparte-tabs__tab\" role=\"tab\" id=\"aparte-art-${cardId}-tab-code\" aria-controls=\"aparte-art-${cardId}-pane-code\" aria-selected=\"true\" tabindex=\"0\" data-tab-target=\"code\">${escapeHtml(codeLabel)}</button>\n ${previewable ? `<button type=\"button\" class=\"aparte-tabs__tab\" role=\"tab\" id=\"aparte-art-${cardId}-tab-preview\" aria-controls=\"aparte-art-${cardId}-pane-preview\" aria-selected=\"false\" tabindex=\"-1\" data-tab-target=\"preview\" ${isStreaming ? 'disabled' : ''}>${escapeHtml(previewLabel)}</button>` : ''}\n </nav>\n <div class=\"aparte-art-card__body\">\n <div class=\"aparte-art-card__pane\" role=\"tabpanel\" id=\"aparte-art-${cardId}-pane-code\" aria-labelledby=\"aparte-art-${cardId}-tab-code\" tabindex=\"0\" data-pane=\"code\">\n <div class=\"aparte-code-content-wrapper\">\n <pre><code class=\"language-${escapeHtml(displayLang)}\">${escapeHtml(cleanContent)}</code></pre>\n </div>\n </div>\n ${previewable ? `\n <div class=\"aparte-art-card__pane\" role=\"tabpanel\" id=\"aparte-art-${cardId}-pane-preview\" aria-labelledby=\"aparte-art-${cardId}-tab-preview\" tabindex=\"0\" data-pane=\"preview\">\n <div class=\"aparte-art-card__pending\">${escapeHtml(cfg.t('previewPending'))}</div>\n </div>\n ` : ''}\n </div>\n </div>\n `;\n },\n /**\n * Every string this card shows: the copy button's tooltip, glyph and accessible\n * name, the download button's title and label, and the two tab names. They were\n * all hardcoded literals until they got locale keys — including the copy button's\n * `aria-label`, which said \"Copy\" while its own `title` one attribute away already\n * went through `t('copy')`, so a French reader got a French tooltip and an English\n * announcement.\n *\n * Nothing here touches the tab state or the preview pane: a mounted iframe is\n * running model-authored code, and re-rendering this card is exactly what the\n * hook exists to avoid.\n */\n relabel: (element: HTMLElement) => {\n const cfg = contextConfig();\n const copyBtn = element.querySelector('.aparte-art-card__btn[data-action=\"copy\"]');\n if (copyBtn) {\n copyBtn.setAttribute('title', cfg.t('copy'));\n copyBtn.setAttribute('aria-label', cfg.t('copy'));\n copyBtn.innerHTML = cfg.getIcon('copy');\n }\n const dl = element.querySelector('.aparte-art-card__btn[data-action=\"download\"]');\n if (dl) {\n const label = cfg.t('download');\n dl.setAttribute('title', label);\n dl.setAttribute('aria-label', label);\n }\n // Text only, and never `aria-selected` or `data-tab`: which pane is open is\n // the reader's state, not the locale's. A relabel that touched it would close\n // a preview somebody had opened.\n const previewTab = element.querySelector('[data-tab-target=\"preview\"]');\n if (previewTab) previewTab.textContent = cfg.t('preview');\n const codeTab = element.querySelector('[data-tab-target=\"code\"]');\n if (codeTab) codeTab.textContent = cfg.t('code');\n },\n setup: (element: HTMLElement, segment: AparteArtifactSegment) => {\n latestSegment.set(element, segment);\n const kind = (segment.artifactType || '').toLowerCase();\n if (BINARY_FILE_KINDS.has(kind)) {\n setupBinaryFileArtifact(element, segment, kind);\n return;\n }\n // Async highlight on the code pane\n const wrapper = element.querySelector('.aparte-code-content-wrapper');\n if (wrapper) {\n const displayLang = languageForKind(kind);\n const cleanContent = stripCodeFences(segment.content || '');\n void contextConfig().highlightCode(cleanContent, displayLang).then(html => {\n wrapper.innerHTML = html;\n }).catch(() => { /* best-effort: a failed highlight degrades silently */ });\n }\n\n /*\n * Tab switching — and, for Preview, the one place the frame is created.\n *\n * A `tablist` is a SINGLE tab stop with arrow keys inside it, not N tab stops.\n * This shipped as two ordinary buttons under a tablist role: Tab landed on each\n * in turn and the arrows did nothing, so the role promised a keyboard model the\n * card did not have. The roving `tabindex` and the arrow handling below are that\n * model; `aria-controls` / `role=\"tabpanel\"` in the markup are the other half.\n */\n const tabs = [...element.querySelectorAll<HTMLButtonElement>('[data-tab-target]')];\n const select = (btn: HTMLButtonElement, focus: boolean): void => {\n const target = btn.getAttribute('data-tab-target');\n if (!target || btn.disabled) return;\n if (target === 'preview') mountPreviewFrame(element, segment);\n element.setAttribute('data-tab', target);\n for (const b of tabs) {\n const on = b === btn;\n b.setAttribute('aria-selected', on ? 'true' : 'false');\n b.tabIndex = on ? 0 : -1;\n }\n if (focus) btn.focus();\n };\n for (const [i, btn] of tabs.entries()) {\n btn.addEventListener('click', () => select(btn, false));\n btn.addEventListener('keydown', (e) => {\n // Home/End as well as the arrows: both are in the pattern, and on a\n // two-tab list they are the fastest way to the one you are not on.\n const step = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;\n let next = -1;\n if (step) next = (i + step + tabs.length) % tabs.length;\n else if (e.key === 'Home') next = 0;\n else if (e.key === 'End') next = tabs.length - 1;\n if (next < 0) return;\n e.preventDefault();\n // Skip a disabled tab rather than trapping focus on it: Preview is\n // disabled while the artifact still streams.\n for (let n = 0; n < tabs.length; n++) {\n const candidate = tabs[(next + n * (step || 1) + tabs.length) % tabs.length];\n if (candidate && !candidate.disabled) { select(candidate, true); return; }\n }\n });\n }\n\n // Copy\n const copyBtn = element.querySelector<HTMLButtonElement>('[data-action=\"copy\"]');\n if (copyBtn) {\n copyBtn.addEventListener('click', () => {\n // Late execution (user click) — resolve from the element.\n const code = stripCodeFences(segment.content || '');\n void copyText(code).catch(() => { /* best-effort: a failed clipboard write degrades silently */ });\n const original = copyBtn.innerHTML;\n copyBtn.innerHTML = contextConfig(copyBtn).getIcon('check');\n copyBtn.setAttribute('title', contextConfig(copyBtn).t('copied'));\n setTimeout(() => {\n copyBtn.innerHTML = original;\n copyBtn.setAttribute('title', contextConfig(copyBtn).t('copy'));\n }, 1500);\n });\n }\n\n // Download — a text artifact downloads from its own content. (A binary one\n // never reaches here: its setup returned above, and its file card owns the\n // button.) The LATEST segment, not the one this closure captured.\n const dlBtn = element.querySelector<HTMLButtonElement>('[data-action=\"download\"]');\n if (dlBtn) {\n dlBtn.addEventListener('click', () => {\n if (dlBtn.disabled) return;\n downloadTextArtifact(latestSegment.get(element) ?? segment);\n });\n }\n },\n update: (element: HTMLElement, segment: AparteArtifactSegment) => {\n // Keep the click handler's view of the artifact current, and throw away a\n // frame that is now showing stale content so the next press rebuilds it.\n const previous = latestSegment.get(element);\n latestSegment.set(element, segment);\n if (previous && previous.content !== segment.content) {\n element.querySelector('.aparte-art-card__pane[data-pane=\"preview\"] iframe')?.remove();\n }\n const isStreaming = !!segment.isStreaming;\n const kind = (segment.artifactType || '').toLowerCase();\n if (BINARY_FILE_KINDS.has(kind)) {\n updateBinaryFileArtifact(element, segment, isStreaming);\n return;\n }\n const wasStreaming = element.getAttribute('data-streaming') === 'true';\n const cleanContent = stripCodeFences(segment.content || '');\n\n // 1. Live-update the code pane during streaming.\n //\n // One call owns both halves — the plain tail every token and the coloured\n // prefix on a throttle. Setting `textContent` here as well is what made the\n // pane flicker: it erased the highlighter's spans on every token.\n if (isStreaming) {\n const segId = element.getAttribute('data-segment-id') ?? segment.id;\n streamHighlight(element, '.aparte-code-content-wrapper', cleanContent, languageForKind(kind), segId);\n } else {\n const codeEl = element.querySelector('.aparte-code-content-wrapper code');\n if (codeEl) {\n codeEl.textContent = cleanContent;\n } else {\n const wrapper = element.querySelector('.aparte-code-content-wrapper');\n if (wrapper) {\n const displayLang = languageForKind(kind);\n wrapper.innerHTML = `<pre><code class=\"language-${escapeHtml(displayLang)}\">${escapeHtml(cleanContent)}</code></pre>`;\n }\n }\n }\n\n // 2. On stream-completion: highlight + build preview iframe + auto-switch\n if (wasStreaming && !isStreaming) {\n element.setAttribute('data-streaming', 'false');\n\n // The pulse says \"the model is still writing this\". `render()` painted it\n // and nothing ever took it away, so a finished document kept claiming to be\n // in flight — forever, at 1.2s a cycle. Unnoticed until the artifact demo\n // actually streamed: the indicator had never been exercised.\n element.querySelector('.aparte-art-card__pulse')?.remove();\n\n // Re-run syntax highlight now that content is final\n const wrapper = element.querySelector('.aparte-code-content-wrapper');\n if (wrapper) {\n const displayLang = languageForKind(kind);\n void contextConfig().highlightCode(cleanContent, displayLang).then(html => {\n wrapper.innerHTML = html;\n }).catch(() => { /* best-effort: a failed highlight degrades silently */ });\n }\n\n // Enable previously-disabled buttons (download, preview tab)\n element.querySelectorAll<HTMLButtonElement>('button[disabled]').forEach(b => {\n b.disabled = false;\n });\n\n // Nothing to build and nothing to switch: enabling the Preview button\n // above is the whole of it. The frame is mounted by the tab handler, on\n // a real user press — see the note at `initialTab`.\n }\n },\n // The card's sheet, injected once per document by core — the seam a renderer that\n // is not core's has onto the page. It is a real `.css` file, inlined at build.\n getStyles: () => artifactStyles,\n};\n\nfunction languageForKind(kind: string): string {\n if (kind === 'react') return 'jsx';\n if (kind === 'markdown') return 'md';\n if (kind === 'pdf' || kind === 'xlsx' || kind === 'docx') return 'js';\n return kind || 'text';\n}\n\nfunction downloadTextArtifact(segment: AparteArtifactSegment): void {\n const content = stripCodeFences(segment.content || '');\n const kind = (segment.artifactType || '').toLowerCase();\n const ext = ({\n react: 'jsx', html: 'html', svg: 'svg', js: 'js', css: 'css',\n json: 'json', markdown: 'md', csv: 'csv', text: 'txt',\n python: 'py', typescript: 'ts', bash: 'sh', sql: 'sql',\n } as Record<string, string>)[kind] ?? 'txt';\n const baseTitle = (segment.title ?? labelForKind(kind)).trim();\n const safeBase = slugifyForFilename(baseTitle) || 'artifact';\n const filename = `${safeBase}.${ext}`;\n const mime = segment.mimeType || 'text/plain';\n const blob = new Blob([content], { type: mime });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n}\n\nfunction slugifyForFilename(text: string): string {\n const lower = text.trim().toLowerCase();\n let out = '';\n let prevDash = false;\n for (let i = 0; i < lower.length && out.length < 40; i++) {\n const ch = lower[i]!;\n const isAlnum = (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9');\n if (isAlnum) { out += ch; prevDash = false; continue; }\n if (!prevDash && out.length > 0) { out += '-'; prevDash = true; }\n }\n if (out.endsWith('-')) out = out.slice(0, -1);\n return out;\n}\n\n/**\n * The LATEST segment for a mounted artifact card.\n *\n * `setup()` runs once and closes over the segment it was handed; the bubble builds a\n * fresh object on every `updateSegment`, so that closure freezes the artifact as it\n * was when it was ADDED — which for anything streamed is `content: ''`. Pressing\n * Preview then ran an empty document. Gesture-gating the frame moved the read from\n * `update()` (which always had the current segment) into a closure that did not, and\n * the test written with it never streamed, so it could not see this.\n */\nconst latestSegment = new WeakMap<HTMLElement, AparteArtifactSegment>();\n\nfunction mountPreviewFrame(element: HTMLElement, fallback: AparteArtifactSegment): void {\n const pane = element.querySelector('.aparte-art-card__pane[data-pane=\"preview\"]');\n if (!pane || pane.querySelector('iframe')) return;\n\n // The latest segment, not the one the closure captured — see `latestSegment`.\n const segment = latestSegment.get(element) ?? fallback;\n // Lower-cased like every other reader of the kind in this file: an app-built\n // segment with `artifactType: 'HTML'` rendered an enabled Preview tab whose press\n // did nothing, because `PREVIEWABLE_KINDS.has('HTML')` is false.\n const kind = (segment.artifactType || deriveArtifactKind(segment.mimeType ?? '', 'text')).toLowerCase();\n if (!PREVIEWABLE_KINDS.has(kind) || !previewEnabled()) return;\n\n const title = segment.title?.trim() || labelForKind(kind);\n // The app's builder when it gave one (`preview: (kind, body, title) => srcdoc`),\n // else the built-in document with its CSP.\n const option = renderOptions(contextConfig(element)).preview;\n const build = typeof option === 'function' ? option : buildSafePreviewDocument;\n const srcdoc = build(kind, stripCodeFences(segment.content || ''), title);\n\n /*\n * `sandbox=\"allow-scripts\"` and nothing else: no `allow-same-origin` (opaque\n * origin — the frame cannot read this page, its storage, or the key), no\n * `allow-forms` (it cannot POST out), no `allow-top-navigation` (it cannot move\n * the tab), no `allow-popups`.\n *\n * It CAN navigate itself, which no sandbox token and no CSP directive prevents —\n * see the note on PREVIEW_CSP. `referrerpolicy=\"no-referrer\"` at least keeps the\n * host URL out of that request.\n */\n pane.innerHTML = `<iframe class=\"aparte-art-card__frame\"`\n + ` sandbox=\"allow-scripts\"`\n + ` csp=\"${escapeAttr(PREVIEW_CSP)}\"`\n + ` referrerpolicy=\"no-referrer\" loading=\"lazy\"`\n + ` title=\"${escapeAttr(title)}\" srcdoc=\"${escapeAttr(srcdoc)}\"></iframe>`;\n}\n","/**\n * @aparte/plugin-artifacts\n *\n * An artifact is a document the model produces — a page, a component, a script, a\n * spreadsheet — and nothing a model does by nature: it is a convention an app teaches\n * it. This package is that convention, end to end, for aparté:\n *\n * - a real `create_artifact` **tool** the model calls (`./tool.ts`), whose structured\n * result is the document;\n * - a **tool renderer** that draws that result as the Code/Preview card;\n * - the `<artifact …>…</artifact>` **block grammar**, for a model that writes one in\n * its prose, registered on core's parser (`registerStreamBlock`);\n * - the **segment renderer** for the segment that grammar produces — the same card.\n *\n * One implementation, four registrations, all made by `setupArtifacts()`.\n *\n * Usage:\n * import { setupArtifacts } from '@aparte/plugin-artifacts';\n * setupArtifacts();\n */\nimport { aparteGlobalConfig, registerSegmentRenderer, unregisterSegmentRenderer, type AparteConfig, type AparteToolRenderer } from '@aparte/core';\nimport { createArtifactTool, artifactHandler } from './tool.js';\nimport { artifactRenderer } from './card.js';\nimport { artifactBlock, artifactFromToolCall, ARTIFACT_TAG, ARTIFACT_SEGMENT_TYPE } from './segment.js';\nimport { setRenderOptions, clearRenderOptions, type ArtifactsSetupOptions } from './options.js';\n\n/**\n * Register the tool, its renderer, the block grammar and the segment renderer on\n * `config` (the global config by default). Call once at application startup;\n * returns a function that unregisters all four.\n */\nexport function setupArtifacts(options: ArtifactsSetupOptions = {}, config: AparteConfig = aparteGlobalConfig): () => void {\n const tool = createArtifactTool(options);\n setRenderOptions(config, { preview: options.preview, onBinary: options.onBinary });\n\n config.registerTool(tool, artifactHandler);\n // The card, on the tool's result: the same renderer the segment gets, adapted to\n // the tool-call segment it is handed. `update` and `relabel` are forwarded, so a\n // preview a reader mounted survives the result landing.\n const toolRenderer: AparteToolRenderer = {\n render: (segment) => artifactRenderer.render(artifactFromToolCall(segment)),\n setup: (element, segment) => artifactRenderer.setup?.(element, artifactFromToolCall(segment)),\n update: (element, segment) => artifactRenderer.update?.(element, artifactFromToolCall(segment)),\n relabel: (element, segment) => artifactRenderer.relabel?.(element, artifactFromToolCall(segment)),\n getStyles: () => artifactRenderer.getStyles?.() ?? '',\n };\n config.registerToolRenderer(tool.name, toolRenderer);\n\n registerSegmentRenderer(artifactRenderer, config);\n const tag = options.tag === undefined ? ARTIFACT_TAG : options.tag;\n if (tag) config.registerStreamBlock(artifactBlock(tag));\n\n return () => {\n config.unregisterTool(tool.name);\n config.unregisterToolRenderer(tool.name);\n unregisterSegmentRenderer(ARTIFACT_SEGMENT_TYPE, config);\n if (tag) config.unregisterStreamBlock(tag);\n clearRenderOptions(config);\n };\n}\n\nexport { createArtifactTool, artifactHandler, ARTIFACT_SYSTEM_PROMPT } from './tool.js';\nexport type { ArtifactToolOptions } from './tool.js';\nexport { artifactRenderer } from './card.js';\nexport type { AparteArtifactSegment } from './card.js';\nexport { artifactBlock, artifactSegment, artifactFromToolCall, ARTIFACT_TAG, ARTIFACT_SEGMENT_TYPE } from './segment.js';\nexport type { ArtifactSegment, ArtifactInput } from './segment.js';\nexport { deriveArtifactKind } from './kinds.js';\nexport { buildSafePreviewDocument, PREVIEW_CSP } from './preview-document.js';\nexport type { ArtifactRenderOptions, ArtifactPreviewBuilder, ArtifactBinary, ArtifactBinaryResolver, ArtifactsSetupOptions } from './options.js';\nexport type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';\n"],"names":["kind","e"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/shared.ts","../src/highlight.ts","../src/options.ts","../src/binary-file.ts","../src/artifact.css?raw","../src/card.ts","../src/index.ts"],"sourcesContent":["/**\n * The leaves the artifact card and the binary-file path BOTH use.\n *\n * Scoped by a call-site census, not by intuition. The obvious-looking candidates are\n * not here: `BINARY_FILE_KINDS` reads as shared and is used only by the card (which\n * consults it to delegate), `PREVIEWABLE_KINDS`, `languageForKind` and\n * `downloadTextArtifact` are card-only, and `FILE_ICON_LABEL` and `formatBytes` are\n * binary-only. A first draft of this module took all of them and would have been a\n * grab-bag named for one of its three jobs.\n *\n * What is genuinely shared is small: fence stripping (which the plain code renderer\n * needs too), the kind→label map, and the throttle pair behind the debounced\n * re-highlight.\n */\n\n\n/**\n * Strip leading/trailing markdown code fences (``` or ~~~, optional lang tag).\n * Also strips any content that appears after the closing fence (small models\n * frequently duplicate lines after the closing ``` block).\n *\n * Char-based scanner — no regex.\n */\nexport function stripCodeFences(content: string): string {\n let s = content;\n\n // Opening fence: leading ``` or ~~~ (3+) optionally followed by language tag,\n // then a newline. Walk the start of the string only.\n if (s.startsWith('```') || s.startsWith('~~~')) {\n const fenceChar = s[0];\n let i = 0;\n while (i < s.length && s[i] === fenceChar) i++;\n // Skip language tag chars (anything until newline)\n while (i < s.length && s[i] !== '\\n') i++;\n // Skip the newline itself if present\n if (i < s.length && s[i] === '\\n') i++;\n s = s.slice(i);\n }\n\n // Closing fence: scan forward to find a line that is exclusively `````/`~~~`+\n // optionally followed by trailing whitespace; cut there + everything after.\n const closeAt = findClosingFence(s);\n if (closeAt !== -1) s = s.slice(0, closeAt);\n\n return s.trim();\n}\n\n/** Find the byte offset where a closing fence line begins, or -1 if none. */\nfunction findClosingFence(s: string): number {\n let i = 0;\n while (i < s.length) {\n // Find start of next line\n const lineStart = i;\n // Skip leading whitespace on this line (indentation)\n let k = lineStart;\n while (k < s.length && (s[k] === ' ' || s[k] === '\\t')) k++;\n if (k < s.length && (s[k] === '`' || s[k] === '~')) {\n const fenceChar = s[k];\n let runs = 0;\n while (k < s.length && s[k] === fenceChar) { runs++; k++; }\n if (runs >= 3) {\n // The rest of the line should be whitespace only — otherwise\n // it's not a closing fence (e.g. inline backtick text).\n let onlyWs = true;\n while (k < s.length && s[k] !== '\\n') {\n if (s[k] !== ' ' && s[k] !== '\\t' && s[k] !== '\\r') { onlyWs = false; break; }\n k++;\n }\n if (onlyWs) {\n // Cut at start of this line; if a `\\n` precedes, drop it too\n let cut = lineStart;\n if (cut > 0 && s[cut - 1] === '\\n') cut--;\n return cut;\n }\n }\n }\n // Advance to next line\n while (i < s.length && s[i] !== '\\n') i++;\n if (i < s.length) i++;\n }\n return -1;\n}\n\nexport function labelForKind(kind: string): string {\n switch (kind) {\n case 'react': return 'React component';\n case 'html': return 'HTML document';\n case 'svg': return 'SVG image';\n case 'js': return 'JavaScript snippet';\n case 'css': return 'CSS stylesheet';\n case 'json': return 'JSON document';\n case 'markdown': return 'Markdown document';\n case 'csv': return 'CSV table';\n case 'text': return 'Text file';\n case 'python': return 'Python script';\n case 'typescript': return 'TypeScript file';\n case 'bash': return 'Bash script';\n case 'sql': return 'SQL query';\n case 'pdf': return 'PDF generator';\n case 'xlsx': return 'Excel generator';\n case 'docx': return 'Word generator';\n default: return 'Artifact';\n }\n}\n\n/** Cap a segmentId→timestamp throttle map so a long session can't grow it without\n * bound (one entry per streamed segment). Values are timestamps (tiny), so the cap\n * is generous; delete-then-set refreshes recency, evict the oldest key when full.\n * Evicting a stale entry costs at most one extra highlight/dispatch — never\n * incorrect, since both debounce windows are far shorter than the cap horizon. */\n\n","/**\n * Progressive syntax highlighting for a code pane that is STILL STREAMING.\n *\n * The previous shape — `codeEl.textContent = content` on every token, plus a\n * debounced full re-highlight — made the pane flicker between plain and coloured:\n * assigning `textContent` destroys the highlighter's `<span>`s, so each token\n * erased the colours the last debounce had painted, and the reader saw plain text\n * most of the time with a coloured frame every 400ms. The debounce was not the\n * problem; rewriting the whole block on every token was.\n *\n * So the pane is split at the last newline:\n *\n * <pre><code> …highlighted COMPLETE lines… <span data-aparte-tail>partial line</span>\n *\n * A token only rewrites the tail, which costs one text assignment and leaves the\n * coloured prefix untouched. The prefix advances at most once per\n * `HIGHLIGHT_DEBOUNCE_MS`, and only when a new line has actually completed —\n * highlighting a half-written line is also what made the colours wrong as well as\n * flickering, because an unterminated string or brace re-tokenises everything\n * after it.\n *\n * `data-aparte-hl-len` on the pane is the boundary, and the DOM is deliberately\n * the source of truth for it rather than a module-level map: it makes the value\n * monotonic across a slow highlight that resolves out of order, and it cannot go\n * stale when the pane is rebuilt underneath us.\n */\n// Copied from core's `renderers/highlight-stream.ts` (the only other reader is the\n// code segment's own renderer): two readers is not yet a layer to export.\nimport { contextConfig, escapeHtml } from '@aparte/core';\n\n/** One highlight per pane per this window. Shiki costs 50-100ms per call. */\nconst HIGHLIGHT_DEBOUNCE_MS = 400;\n/** Bound on the throttle bookkeeping, so a long session cannot grow it forever. */\nconst MAX_THROTTLE_ENTRIES = 256;\n\nconst _lastHighlightAt = new Map<string, number>();\n\n/**\n * Record `id`'s last-seen time in a bounded, insertion-ordered map.\n *\n * Shared with the artifact's event-dispatch throttle, which is why it is generic\n * over the map rather than closing over one.\n */\nexport function markThrottle(map: Map<string, number>, id: string, at: number): void {\n map.delete(id);\n if (map.size >= MAX_THROTTLE_ENTRIES) {\n const oldest = map.keys().next().value;\n if (oldest !== undefined) map.delete(oldest);\n }\n map.set(id, at);\n}\n\n/** Where the coloured prefix ends. Absent or unparseable means \"nothing coloured\". */\nfunction highlightedLen(pane: HTMLElement): number {\n const n = Number(pane.dataset.aparteHlLen);\n return Number.isFinite(n) && n > 0 ? n : 0;\n}\n\n/**\n * Paint `content` into `paneSelector` while it streams.\n *\n * Call it on every token: the cheap half runs every time, the highlight is\n * throttled internally. The caller still runs one final, whole-content highlight\n * when the segment settles — this function deliberately never colours the last\n * line, so the settle pass is what completes it.\n */\nexport function streamHighlight(\n element: HTMLElement,\n paneSelector: string,\n content: string,\n lang: string,\n segId: string,\n): void {\n const pane = element.querySelector<HTMLElement>(paneSelector);\n if (!pane) return;\n\n const tail = pane.querySelector<HTMLElement>('[data-aparte-tail]');\n const hlLen = highlightedLen(pane);\n\n // ── every token: move the tail, and nothing else ──────────────────────\n if (tail && hlLen <= content.length) {\n tail.textContent = content.slice(hlLen);\n } else {\n // No coloured prefix yet, or the pane was rebuilt under us (a settle\n // highlight replaces the whole thing). Plain text, and forget the\n // boundary — self-healing beats a reset call at three call sites.\n delete pane.dataset.aparteHlLen;\n const codeEl = pane.querySelector('code');\n if (codeEl) codeEl.textContent = content;\n else pane.innerHTML = `<pre><code class=\"language-${escapeHtml(lang || 'text')}\">${escapeHtml(content)}</code></pre>`;\n }\n\n // ── throttled: advance the coloured prefix by whole lines ─────────────\n const cut = content.lastIndexOf('\\n') + 1;\n if (cut <= hlLen) return;\n const now = Date.now();\n if (now - (_lastHighlightAt.get(segId) ?? 0) < HIGHLIGHT_DEBOUNCE_MS) return;\n markThrottle(_lastHighlightAt, segId, now);\n\n // Resolved from the element, not the ambient config: this lands late, and by\n // then the render-time config is gone.\n void contextConfig(element).highlightCode(content.slice(0, cut), lang).then(html => {\n const live = element.querySelector<HTMLElement>(paneSelector);\n if (!live) return;\n // Monotonic. Two highlights can be in flight; the older one carries the\n // SHORTER prefix, and letting it land would visibly rewind the pane.\n if (cut <= highlightedLen(live)) return;\n live.innerHTML = html;\n const codeEl = live.querySelector('code') ?? live;\n const span = document.createElement('span');\n span.dataset.aparteTail = '';\n // The captured tail may already be one token behind; the next token fixes\n // it, and there is always a next one or a settle pass.\n span.textContent = content.slice(cut);\n codeEl.appendChild(span);\n live.dataset.aparteHlLen = String(cut);\n }).catch(() => { /* best-effort: a failed highlight degrades silently */ });\n}\n","/**\n * What the app decided at `setupArtifacts()`, read back by the card at render time.\n *\n * The card is resolved through core's ambient config (`contextConfig()`), not through\n * a closure, so the settings live beside the config they were given for: one entry per\n * `AparteConfig`, the global one by default. A chat with its own config that never\n * called `setupArtifacts` reads the global settings, which is what its renderers do\n * too.\n */\nimport { aparteGlobalConfig, type AparteConfig } from '@aparte/core';\nimport type { ArtifactSegment } from './segment.js';\nimport type { ArtifactToolOptions } from './tool.js';\n\n/** Builds the `srcdoc` of the sandboxed preview frame for a previewable kind. */\nexport type ArtifactPreviewBuilder = (kind: string, body: string, title: string) => string;\n\n/** The bytes a binary artifact (pdf, xlsx, docx) resolved to. */\nexport interface ArtifactBinary {\n /** The file. */\n buffer: BlobPart;\n /** Its MIME type, for the download. */\n mime: string;\n /** The name the download gets. */\n filename: string;\n /**\n * Optional HTML rendering of the file (a spreadsheet as a table, a PDF's text),\n * shown in the card's preview pane after sanitisation. Absent: the pane says so.\n */\n previewHtml?: string | null;\n}\n\n/**\n * Turn a binary artifact's source (the JS the model wrote to produce a workbook, a\n * PDF, a document) into bytes. Core owns no sandbox and no file generator: this is the\n * app's, and without it a binary artifact shows its source with no download and no\n * preview — declared nowhere, offered nowhere (ratified decision #8).\n */\nexport type ArtifactBinaryResolver = (artifact: ArtifactSegment) => Promise<ArtifactBinary>;\n\nexport interface ArtifactRenderOptions {\n /**\n * The Preview tab for previewable kinds (html, react, svg, js, css). `true`\n * (default) mounts a sandboxed frame on a gesture with the built-in document\n * builder; a function replaces the builder; `false` offers no preview at all.\n */\n preview?: boolean | ArtifactPreviewBuilder;\n /** See {@link ArtifactBinaryResolver}. */\n onBinary?: ArtifactBinaryResolver;\n /**\n * An Open button on the card, which dispatches `aparte-artifact-open` (bubbling, out of\n * shadow roots) with the artifact, for the app to show it where it wants: a split pane,\n * a dialog, a tab. Off by default — without a listener nothing would open (ratified\n * decision #8).\n */\n open?: boolean;\n}\n\n/**\n * The artifact card's Open button was pressed (`setupArtifacts({ open: true })`): show the\n * artifact where the app wants it. Bubbles out of the chat and out of shadow roots. The\n * detail is the artifact as the card shows it now.\n *\n * @event aparte-artifact-open\n */\nexport interface ArtifactOpenEventDetail {\n artifact: ArtifactSegment;\n}\n\n// Typed where it is dispatched: `addEventListener('aparte-artifact-open', (e) => e.detail.artifact)`\n// with no cast, on an element, the document or the window (it bubbles out of the chat),\n// wherever the plugin is imported. The type names the plugin's segment, so it cannot live in\n// core's event map.\ninterface ArtifactEventMap {\n 'aparte-artifact-open': CustomEvent<ArtifactOpenEventDetail>;\n}\ndeclare global {\n interface HTMLElementEventMap extends ArtifactEventMap {}\n interface DocumentEventMap extends ArtifactEventMap {}\n interface WindowEventMap extends ArtifactEventMap {}\n}\n\nconst settings = new WeakMap<AparteConfig, ArtifactRenderOptions>();\n\nexport function setRenderOptions(config: AparteConfig, options: ArtifactRenderOptions): void {\n settings.set(config, options);\n}\n\n/** The options for this config, else the global config's, else the defaults. */\nexport function renderOptions(config: AparteConfig): ArtifactRenderOptions {\n return settings.get(config) ?? settings.get(aparteGlobalConfig) ?? {};\n}\n\n/** For tests and a teardown: forget what a config was told. */\nexport function clearRenderOptions(config: AparteConfig): void {\n settings.delete(config);\n}\n\n/**\n * Everything `setupArtifacts()` accepts — the tool's options, the card's, and the tag.\n *\n * ONE declaration, here, because there are two `setupArtifacts()`: the browser barrel's\n * and the node one. Each used to declare its own `ArtifactsSetupOptions`, and they were\n * not the same shape — the node copy omitted `ArtifactRenderOptions`, so `preview` and\n * `onBinary` were type errors against the SSR entry while being valid against the\n * browser one. A consumer typing a shared setup object got a different contract\n * depending on which condition resolved, from a name that reads as one thing.\n *\n * The server ignores `preview` and `onBinary` (it registers no renderer), and that is\n * correct: the same options object is meant to be written once and passed on both\n * sides. An option nobody reads there is inert, whereas a type error there is a wall.\n */\nexport interface ArtifactsSetupOptions extends ArtifactToolOptions, ArtifactRenderOptions {\n /**\n * The tag recognised in the prose — `<artifact …>…</artifact>` by default. `false`\n * registers no grammar: only the tool produces artifacts then.\n */\n tag?: string | false;\n}\n","/**\n * The binary-artifact path: pdf, xlsx, docx.\n *\n * A different LIFETIME from the card, and that is the seam. The card renders content\n * that arrives in the stream and is complete when the stream ends. This path renders a\n * file that something ELSE produces from that content — a sandbox that runs the JS the\n * model wrote and hands back a workbook, a PDF, a document. That something is the\n * app's (`onBinary` in `setupArtifacts`): the plugin knows how to ask and how to show\n * the answer, not how to make it.\n *\n * This file used to be a protocol — three window events, a redownload/rehydrate\n * pair of host handlers, a debounce against a sandbox that was \"busy\", and the name of\n * a service that existed in no package. What is left is a function call and its two\n * outcomes: the bytes, or an error shown in the card.\n */\nimport { escapeHtml, escapeAttr, contextConfig, type AparteConfig } from '@aparte/core';\nimport type { ArtifactSegment } from './segment.js';\nimport { stripCodeFences, labelForKind } from './shared.js';\nimport { streamHighlight } from './highlight.js';\nimport { renderOptions, type ArtifactBinary } from './options.js';\n\nconst FILE_ICON_LABEL: Record<string, string> = {\n xlsx: 'XLS',\n pdf: 'PDF',\n docx: 'DOC',\n};\n\n/**\n * Bytes already produced, by segment id — so a re-mount (a branch switch, a\n * conversation toggle) shows the file again without asking the app to make it twice.\n * Bounded: each entry holds a whole file.\n */\nconst produced = new Map<string, ArtifactBinary>();\nconst MAX_PRODUCED = 24;\n/** One generation in flight per segment, whatever re-mounts meanwhile. */\nconst inFlight = new Map<string, Promise<ArtifactBinary>>();\n\nfunction remember(id: string, bin: ArtifactBinary): void {\n produced.delete(id);\n if (produced.size >= MAX_PRODUCED) {\n const oldest = produced.keys().next().value;\n if (oldest !== undefined) produced.delete(oldest);\n }\n produced.set(id, bin);\n}\n\n/** For tests: forget every produced file and every generation in flight. */\nexport function resetBinaryArtifacts(): void {\n produced.clear();\n inFlight.clear();\n}\n\nexport function renderBinaryFileArtifact(segment: ArtifactSegment, kind: string): string {\n const cfg = contextConfig();\n const title = segment.title?.trim() || labelForKind(kind);\n const iconLabel = FILE_ICON_LABEL[kind] ?? kind.toUpperCase();\n const isStreaming = !!segment.isStreaming;\n const canProduce = typeof renderOptions(cfg).onBinary === 'function';\n const downloadLabel = escapeHtml(cfg.t('download'));\n const done = produced.get(segment.id);\n\n // Already produced (a re-mount): the file, at once.\n if (done && !isStreaming) {\n const preview = previewMarkup(done, kind, cfg); // safe-text: the app's previewHtml through the config's sanitizer, or an escapeHtml'd sentence — markup on purpose, as swapToPreview() sets it\n return `\n <div class=\"aparte-segment aparte-card aparte-segment-artifact-file\"\n data-segment-id=\"${escapeHtml(segment.id)}\"\n data-artifact-type=\"${escapeHtml(kind)}\"\n data-state=\"ready\">\n <div class=\"aparte-art-file__card\">\n <div class=\"aparte-art-file__icon\" data-kind=\"${escapeHtml(kind)}\">${escapeHtml(iconLabel)}</div>\n <div class=\"aparte-art-file__meta\">\n <div class=\"aparte-art-file__meta-name\" data-role=\"file-name\">${escapeHtml(done.filename)}</div>\n <div class=\"aparte-art-file__meta-sub\" data-role=\"file-sub\">${escapeHtml(formatBytes(byteLength(done.buffer)))} · ${escapeHtml(kind.toUpperCase())}</div>\n </div>\n <div class=\"aparte-art-file__actions\">\n <button type=\"button\" class=\"aparte-btn aparte-btn--primary aparte-btn--solid aparte-art-file__btn aparte-art-file__btn--primary\" data-action=\"download\">${downloadLabel}</button>\n </div>\n </div>\n <div class=\"aparte-art-file__body\">\n <div class=\"aparte-art-file__code-pane\" data-role=\"code-pane\" hidden>\n <pre><code class=\"language-js\"></code></pre>\n </div>\n <div class=\"aparte-art-file__preview-pane\" data-role=\"preview-pane\">${preview}</div>\n </div>\n </div>\n `;\n }\n\n const cleanContent = stripCodeFences(segment.content || '');\n // Streaming: the model is still writing the source. Settled with a producer: the\n // file is being made. Settled without one: the source is all there is to show.\n const subText = isStreaming ? cfg.t('generating') : canProduce ? cfg.t('rebuildingPreview') : kind.toUpperCase();\n return `\n <div class=\"aparte-segment aparte-card aparte-segment-artifact-file\"\n data-segment-id=\"${escapeHtml(segment.id)}\"\n data-artifact-type=\"${escapeHtml(kind)}\"\n data-state=\"${escapeAttr(isStreaming ? 'streaming' : canProduce ? 'compiling' : 'source')}\">\n <div class=\"aparte-art-file__card\">\n <div class=\"aparte-art-file__icon\" data-kind=\"${escapeHtml(kind)}\">${escapeHtml(iconLabel)}</div>\n <div class=\"aparte-art-file__meta\">\n <div class=\"aparte-art-file__meta-name\" data-role=\"file-name\">${escapeHtml(title)}</div>\n <div class=\"aparte-art-file__meta-sub\" data-role=\"file-sub\">${escapeHtml(subText)}</div>\n </div>\n <div class=\"aparte-art-file__actions\">\n ${canProduce ? `<button type=\"button\" class=\"aparte-btn aparte-btn--primary aparte-btn--solid aparte-art-file__btn aparte-art-file__btn--primary\" data-action=\"download\" disabled>${downloadLabel}</button>` : ''}\n </div>\n </div>\n <div class=\"aparte-art-file__body\">\n <div class=\"aparte-art-file__code-pane\" data-role=\"code-pane\">\n <pre><code class=\"language-js\">${escapeHtml(cleanContent)}</code></pre>\n </div>\n <div class=\"aparte-art-file__preview-pane\" data-role=\"preview-pane\" hidden></div>\n </div>\n </div>\n `;\n}\n\nexport function setupBinaryFileArtifact(element: HTMLElement, segment: ArtifactSegment, kind: string): void {\n if (element.dataset['aparteInit'] !== 'true') {\n element.dataset['aparteInit'] = 'true';\n element.addEventListener('click', (ev) => {\n const target = ev.target as HTMLElement;\n const action = target.closest<HTMLElement>('[data-action]')?.getAttribute('data-action');\n if (action !== 'download') return;\n const bin = produced.get(segment.id);\n if (bin) downloadBinary(bin);\n });\n }\n if (segment.isStreaming) return;\n settle(element, segment, kind);\n}\n\nexport function updateBinaryFileArtifact(element: HTMLElement, segment: ArtifactSegment, isStreaming: boolean): void {\n const state = element.getAttribute('data-state');\n if (state === 'ready' || state === 'error') return;\n\n const cleanContent = stripCodeFences(segment.content || '');\n if (isStreaming) {\n streamHighlight(element, '[data-role=\"code-pane\"]', cleanContent, 'js', segment.id);\n return;\n }\n const codeEl = element.querySelector<HTMLElement>('[data-role=\"code-pane\"] code');\n if (codeEl) codeEl.textContent = cleanContent;\n if (state === 'streaming') settle(element, segment, kind(element));\n}\n\n/**\n * The source is final. Highlight it, and — when the app can — ask for the file.\n * One request per segment whatever re-mounts meanwhile; the answer is remembered so a\n * later mount shows the file without asking again.\n */\nfunction settle(element: HTMLElement, segment: ArtifactSegment, kind: string): void {\n const cfg = contextConfig(element);\n const cleanContent = stripCodeFences(segment.content || '');\n const wrapper = element.querySelector<HTMLElement>('[data-role=\"code-pane\"]');\n if (wrapper) {\n void cfg.highlightCode(cleanContent, 'js').then(html => { wrapper.innerHTML = html; })\n .catch(() => { /* best-effort: a failed highlight degrades silently */ });\n }\n\n const already = produced.get(segment.id);\n if (already) { swapToPreview(element, already, kind); return; }\n\n const resolve = renderOptions(cfg).onBinary;\n if (!resolve) { element.setAttribute('data-state', 'source'); return; }\n element.setAttribute('data-state', 'compiling');\n\n let job = inFlight.get(segment.id);\n if (!job) {\n job = resolve({ ...segment, content: cleanContent });\n inFlight.set(segment.id, job);\n // Both branches, or the bookkeeping chain itself rejects unhandled on failure.\n const done = (): void => { inFlight.delete(segment.id); };\n job.then(done, done);\n }\n void job.then((bin) => {\n remember(segment.id, bin);\n if (element.isConnected) swapToPreview(element, bin, kind);\n }).catch((err: unknown) => {\n if (element.isConnected) showError(element, err instanceof Error ? err.message : String(err));\n });\n}\n\nfunction kind(element: HTMLElement): string {\n return (element.getAttribute('data-artifact-type') || '').toLowerCase();\n}\n\n/**\n * The config is passed IN, never resolved here.\n *\n * This is the one sink in the file that puts app-supplied HTML on the page, and it\n * read the ambient config — which, from `swapToPreview`'s promise callback, is the\n * global one long after the render that set it. A chat with its own `AparteConfig`\n * therefore had its `setHtmlSanitizer` skipped at exactly this line, and its locale\n * skipped at the sentence beside it. Both callers already hold the right answer:\n * the render path is inside the bubble's `runWithConfig`, the swap path has the\n * element.\n */\nfunction previewMarkup(bin: ArtifactBinary, kind: string, cfg: AparteConfig): string {\n return bin.previewHtml\n ? cfg.sanitizeHtml(bin.previewHtml)\n : `<div class=\"aparte-art-file__preview-empty\">${escapeHtml(cfg.t('previewPending'))} ${escapeHtml(kind)}</div>`;\n}\n\nfunction swapToPreview(element: HTMLElement, bin: ArtifactBinary, kind: string): void {\n element.setAttribute('data-state', 'ready');\n const codePane = element.querySelector<HTMLElement>('[data-role=\"code-pane\"]');\n if (codePane) codePane.hidden = true;\n const preview = element.querySelector<HTMLElement>('[data-role=\"preview-pane\"]');\n if (preview) {\n // `previewHtml` comes from the app and is built from file bytes the model's code\n // produced, so it goes through the sanitizer like every other innerHTML here.\n preview.innerHTML = previewMarkup(bin, kind, contextConfig(element));\n preview.hidden = false;\n }\n const nameEl = element.querySelector<HTMLElement>('[data-role=\"file-name\"]');\n if (nameEl) nameEl.textContent = bin.filename;\n const sub = element.querySelector<HTMLElement>('[data-role=\"file-sub\"]');\n if (sub) sub.textContent = `${formatBytes(byteLength(bin.buffer))} · ${kind.toUpperCase()}`;\n const dlBtn = element.querySelector<HTMLButtonElement>('[data-action=\"download\"]');\n if (dlBtn) dlBtn.disabled = false;\n}\n\nfunction downloadBinary(bin: ArtifactBinary): void {\n const blob = new Blob([bin.buffer], { type: bin.mime });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = bin.filename;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n}\n\nfunction byteLength(part: BlobPart): number {\n if (typeof part === 'string') return new TextEncoder().encode(part).length;\n if (part instanceof Blob) return part.size;\n return (part as ArrayBufferView | ArrayBuffer).byteLength;\n}\n\nfunction formatBytes(n: number): string {\n if (n < 1024) return `${n} B`;\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;\n return `${(n / (1024 * 1024)).toFixed(2)} MB`;\n}\n\nfunction showError(element: HTMLElement, errorMsg: string): void {\n if (element.getAttribute('data-state') === 'ready') return;\n element.setAttribute('data-state', 'error');\n const cfg = contextConfig(element);\n const sub = element.querySelector<HTMLElement>('[data-role=\"file-sub\"]');\n if (sub) sub.textContent = cfg.t('sandboxError');\n const body = element.querySelector<HTMLElement>('.aparte-art-file__body');\n if (body) {\n // The first line of a stack is the actionable one.\n const short = (errorMsg.split('\\n')[0] ?? '').slice(0, 240);\n body.innerHTML = `\n <div class=\"aparte-art-file__error\">\n <div class=\"aparte-art-file__error-title\">${escapeHtml(cfg.t('sandboxError'))}</div>\n <div class=\"aparte-output aparte-art-file__error-msg\">${escapeHtml(short)}</div>\n <div class=\"aparte-art-file__error-hint\">${escapeHtml(cfg.t('sandboxErrorHint'))}</div>\n </div>\n `;\n }\n}\n","export default \"/*\\n * @aparte/plugin-artifacts — the artifact card and the artifact file preview.\\n *\\n * Injected once per document through the renderer's `getStyles()`, the seam core\\n * keeps for a renderer that is not core's. It reads core's tokens (`--aparte-space-*`,\\n * `--aparte-surface-*`, `--aparte-code-bg`…) and declares its own below, on the two\\n * roots it draws, so a consumer overrides them the way they override any other:\\n * `.aparte-segment-artifact-card { --aparte-art-paper-bg: … }`.\\n */\\n.aparte-segment-artifact-card,\\n.aparte-segment-artifact-file {\\n /* Paper. An artifact preview is a DOCUMENT shown inside the chat — a spreadsheet,\\n a rendered page — so it stays light whatever the app theme is, the way a PDF\\n viewer shows a white page in a dark editor. */\\n --aparte-art-paper-bg: #fff;\\n --aparte-art-paper-text: #1f2937;\\n --aparte-art-paper-row-alt: #f9fafb;\\n --aparte-art-paper-head-bg: #f3f4f6;\\n --aparte-art-paper-head-text: #111827;\\n --aparte-art-paper-border: rgba(0, 0, 0, 0.1);\\n /* The paper's OWN muted ink. The empty-preview line used to take\\n `--aparte-text-muted`, which is the THEME's — and the pane opts out of the\\n theme on purpose, so in the dark theme a #a89bb6 line landed on white paper at\\n 2.62:1. Mixed from the paper's own pair instead: 5.5:1, and it follows a\\n consumer who re-declares the paper. */\\n --aparte-art-paper-text-muted: color-mix(in srgb, var(--aparte-art-paper-text) 70%, var(--aparte-art-paper-bg));\\n /* File-type tiles. Brand colours, so they are literals on purpose — but named,\\n because an app with its own file-type palette has nowhere else to put it. The\\n lettering is fixed like the tiles: a themed ink would go near-black in dark\\n mode, on a dark green tile. */\\n --aparte-art-file-icon-bg: linear-gradient(135deg, #1d6f42, #0f5132);\\n --aparte-art-file-icon-color: #fff;\\n --aparte-art-file-icon-bg-pdf: linear-gradient(135deg, #c0392b, #7d1f17);\\n --aparte-art-file-icon-bg-docx: linear-gradient(135deg, #1e5288, #0f3060);\\n --aparte-art-file-error-msg-bg: rgba(0, 0, 0, 0.04);\\n /* Sizes. */\\n --aparte-art-card-header-min-height: 36px;\\n --aparte-art-card-pulse-size: 8px;\\n --aparte-art-card-btn-size: 28px;\\n --aparte-art-file-icon-size: 40px;\\n --aparte-art-file-preview-padding: 20px;\\n --aparte-art-file-error-padding-inline: 18px;\\n}\\n/* Not the paper — that stays light on purpose. This is a code block inside the\\n error panel, which does follow the theme.\\n\\n Twice, because CSS cannot share one block between an attribute selector and a\\n media query — the same duplication core's theme.css carries, for the same reason:\\n without the second one a system-dark reader who sets no attribute kept the LIGHT\\n wash (`rgba(0,0,0,0.04)`) on the already-dark `--aparte-error-bg`. Edit both. */\\n[data-aparte-theme=\\\"dark\\\"] .aparte-segment-artifact-file {\\n --aparte-art-file-error-msg-bg: rgba(255, 255, 255, 0.06);\\n}\\n@media (prefers-color-scheme: dark) {\\n :root:not([data-aparte-theme=\\\"light\\\"]) .aparte-segment-artifact-file {\\n --aparte-art-file-error-msg-bg: rgba(255, 255, 255, 0.06);\\n }\\n}\\n/* Artifact card */\\n/* The shell is `.aparte-card`; what stays here is what a card in a TRANSCRIPT needs\\n and a card in general does not — the vertical rhythm between messages, and `font:\\n inherit` so a segment never picks up a host's form-control font. */\\n.aparte-segment-artifact-card,\\n.aparte-segment-artifact-file {\\n margin: var(--aparte-space-4) 0;\\n font: inherit;\\n}\\n.aparte-art-card__header {\\n display: flex; align-items: center; justify-content: space-between;\\n padding: var(--aparte-space-4) var(--aparte-space-5);\\n border-bottom: var(--aparte-border-width) solid var(--aparte-border);\\n background: var(--aparte-surface-2);\\n min-height: var(--aparte-art-card-header-min-height);\\n}\\n.aparte-art-card__title-block { display: flex; align-items: center; gap: var(--aparte-space-4); min-width: 0; }\\n/* An outline `.aparte-badge`. What stays is the lettering: a language tag reads as a\\n code marker, so it is upper-cased and tracked out — which a badge in general is not. */\\n.aparte-art-card__kind {\\n text-transform: uppercase;\\n letter-spacing: 0.04em;\\n --aparte-badge-font-size: var(--aparte-font-size-xs);\\n --aparte-badge-radius: var(--aparte-radius-sm);\\n}\\n.aparte-art-card__title {\\n font-size: var(--aparte-font-size-base); font-weight: var(--aparte-font-weight-medium);\\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\\n}\\n/* The geometry is `.aparte-dot`; a streaming card breathes on the accent rather than\\n the status colour, and larger, because it marks a whole card and not a line. */\\n.aparte-art-card__pulse {\\n --aparte-status-dot-size: var(--aparte-art-card-pulse-size);\\n --aparte-status-color: var(--aparte-accent);\\n}\\n.aparte-art-card__actions { display: flex; gap: var(--aparte-space-2); }\\n.aparte-art-card__btn {\\n /* Same as .aparte-action-btn: declare the size, let the recipe draw. */\\n --aparte-btn-size: var(--aparte-art-card-btn-size);\\n}\\n.aparte-art-card__btn:hover:not(:disabled) {\\n background: var(--aparte-surface-hover);\\n border-color: var(--aparte-border);\\n color: var(--aparte-text);\\n}\\n.aparte-art-card__btn:disabled { opacity: var(--aparte-disabled-opacity); cursor: not-allowed; }\\n.aparte-art-card__tabs {\\n /* justify-content and the padding are DECLARED, not left to a default.\\n Core is light DOM on purpose - no shadow root, no ::part(), any selector\\n reaches in - and the corollary is that a component must state what its\\n layout depends on, because an undeclared property has nothing to override\\n a host rule with. A consuming page with a bare nav selector setting\\n justify-content: space-between and padding-top (this library's own docs\\n site had exactly that) otherwise pushes these two tabs to opposite ends of\\n the card and pads the row out.\\n\\n flex-end, and Code first in the DOM. The card OPENS on Code - mounting the\\n preview would execute model-authored code with no gesture (ratified\\n decision #8) - and a selected tab sitting second reads backwards. Right,\\n because the header above puts the artifact's identity on the left and its\\n copy/download buttons on the right, so this keeps every control in one\\n column. DOM order is also keyboard order, so the tab a reader lands on\\n first is the one already showing. */\\n display: flex; justify-content: flex-end; align-items: stretch; gap: var(--aparte-space-1);\\n padding: var(--aparte-space-2) var(--aparte-space-4) 0;\\n border-bottom: var(--aparte-border-width) solid var(--aparte-border);\\n background: var(--aparte-surface-2);\\n}\\n/* The tabs are `.aparte-tabs--underline`, like the elicitation panel's steps — one\\n tab in this library, not one per component. Only the row's own business stays. */\\n.aparte-art-card__tabs button { font-size: var(--aparte-font-size-sm); }\\n/* The card's heights, as variables with ONE owner each.\\n They were four hardcoded numbers in the only part of this card that did not\\n use a variable - everything else here already reads var(--aparte-code-bg) and\\n friends - and two of them had to agree while a third contradicted a fourth:\\n the code pane repeated the body's 600px, and the \\\"press Preview\\\" placeholder\\n was 120px tall inside a body whose min-height said 80, so that minimum applied\\n to nothing.\\n The frame stays a FIXED height rather than an aspect ratio, which is what\\n embeds of arbitrary HTML actually do - CodeSandbox documents 500px, StackBlitz\\n takes a height parameter - because a frame with an opaque origin cannot be\\n measured and a 16/10 ratio on a wide card is enormous. The vh cap is the part\\n that was missing: a fixed 480px should not own a phone screen.\\n Each default lives in its read, as var(--x, literal), the way every other\\n value in this file already does - not in a declaration block on top of the\\n fallbacks, which would be two owners of one number again. It also means the\\n docs' CSS-variable generator finds them: its sweep looks for reads that\\n carry a fallback, so a read without one is a public knob nobody documents. */\\n.aparte-art-card__body {\\n position: relative;\\n /* The placeholder is the tallest thing this box can hold while empty, so it\\n IS the minimum - one number instead of two that disagreed. */\\n min-height: var(--aparte-artifact-pending-height, 120px);\\n max-height: var(--aparte-artifact-body-max, 600px);\\n overflow: hidden;\\n}\\n.aparte-art-card__pane { display: none; height: 100%; }\\n.aparte-segment-artifact-card[data-tab=\\\"code\\\"] .aparte-art-card__pane[data-pane=\\\"code\\\"] { display: block; }\\n.aparte-segment-artifact-card[data-tab=\\\"preview\\\"] .aparte-art-card__pane[data-pane=\\\"preview\\\"] { display: block; }\\n.aparte-art-card__pane[data-pane=\\\"code\\\"] {\\n /* The body already caps this; repeating the number was the second owner. */\\n max-height: var(--aparte-artifact-body-max, 600px); overflow: auto;\\n}\\n.aparte-art-card__frame {\\n display: block;\\n width: 100%;\\n height: min(var(--aparte-artifact-frame-height, 480px), var(--aparte-artifact-frame-max, 70vh));\\n border: 0;\\n background: var(--aparte-art-paper-bg);\\n}\\n.aparte-art-card__pending {\\n display: flex; align-items: center; justify-content: center;\\n height: var(--aparte-artifact-pending-height, 120px);\\n color: var(--aparte-text-muted);\\n font-size: var(--aparte-font-size-md);\\n font-style: italic;\\n}\\n/* ── Binary file artifact (xlsx/pdf/docx) ──────────────────── */\\n.aparte-art-file__card {\\n display: flex; align-items: center; gap: var(--aparte-space-6);\\n padding: var(--aparte-space-6) var(--aparte-space-7);\\n background: var(--aparte-surface-2);\\n border-bottom: var(--aparte-border-width) solid var(--aparte-border);\\n}\\n.aparte-art-file__body {\\n position: relative;\\n}\\n.aparte-art-file__code-pane {\\n max-height: var(--aparte-artifact-file-code-max, 360px); overflow: auto;\\n background: var(--aparte-code-bg);\\n}\\n.aparte-art-file__preview-pane {\\n max-height: var(--aparte-artifact-file-preview-max, 460px);\\n overflow: auto;\\n background: var(--aparte-art-paper-bg);\\n /* Preview is a document view — force light scheme regardless of\\n the app theme, with a dark text colour so cells stay readable. */\\n color: var(--aparte-art-paper-text);\\n}\\n.aparte-art-file__icon {\\n width: var(--aparte-art-file-icon-size); height: var(--aparte-art-file-icon-size);\\n border-radius: var(--aparte-radius-lg);\\n display: flex; align-items: center; justify-content: center;\\n background: var(--aparte-art-file-icon-bg);\\n color: var(--aparte-art-file-icon-color);\\n font-weight: var(--aparte-font-weight-bold);\\n font-size: var(--aparte-font-size-sm);\\n letter-spacing: 0.04em;\\n flex-shrink: 0;\\n}\\n.aparte-art-file__icon[data-kind=\\\"pdf\\\"] { background: var(--aparte-art-file-icon-bg-pdf); }\\n.aparte-art-file__icon[data-kind=\\\"docx\\\"] { background: var(--aparte-art-file-icon-bg-docx); }\\n.aparte-art-file__meta { flex: 1 1 auto; min-width: 0; }\\n.aparte-art-file__meta-name {\\n font-weight: var(--aparte-font-weight-semibold); font-size: var(--aparte-font-size-base);\\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\\n}\\n.aparte-art-file__meta-sub {\\n font-size: var(--aparte-font-size-sm);\\n color: var(--aparte-text-muted);\\n}\\n.aparte-art-file__actions { display: flex; gap: var(--aparte-space-3); flex-shrink: 0; }\\n/* The chrome is `.aparte-btn`; every one of these is a `--primary --solid`, so the\\n fill comes from the recipe. Its own measurements stay: a labelled button in a card\\n is roomier than an icon in a row. */\\n.aparte-art-file__btn {\\n padding: var(--aparte-space-3) var(--aparte-space-5);\\n font-size: var(--aparte-font-size-md);\\n}\\n/* NOTHING here repaints the button, and the comment above is why. These seven lines\\n re-declared `background`, `color`, `border-color` and a hover the recipe already\\n paints — a migration leftover that contradicted the sentence directly above it.\\n\\n Five of them were inert duplicates. The sixth was not: `color: var(--aparte-text-inverse)`\\n overrode `--aparte-btn-on-intent`, which the recipe derives from the fill. Measured in a\\n browser on the built stylesheet: 3.54:1 in the light theme against the recipe's 5.27 —\\n an AA failure on a visible button label, in the default theme. It was also the last\\n place in `styles/` forcing `--aparte-text-inverse` as ink on a coloured fill; badge and\\n field had already stopped.\\n\\n The consumer cost was the sharper one: the one-attribute rebrand this library documents\\n re-derives the ink on every other solid-primary button and, here alone, kept a token\\n bound to core's own palette. `.aparte-art-file__btn:hover` went with them — at 0,2,0 it\\n was always beaten by `.aparte-btn--solid:hover:not(:disabled)` at 0,3,0, so it never\\n applied either. */\\n/* Hide the download button until the sandbox has produced the buffer.\\n Avoids confusing the user with a disabled-but-styled-primary button\\n during streaming / compiling. */\\n.aparte-segment-artifact-file:not([data-state=\\\"ready\\\"]) .aparte-art-file__btn[data-action=\\\"download\\\"] {\\n display: none;\\n}\\n.aparte-art-file__preview-pane th,\\n.aparte-art-file__preview-pane td {\\n border: var(--aparte-border-width) solid var(--aparte-art-paper-border);\\n padding: var(--aparte-space-3) var(--aparte-space-5);\\n text-align: start;\\n white-space: nowrap;\\n color: var(--aparte-art-paper-text);\\n background: var(--aparte-art-paper-bg);\\n}\\n.aparte-art-file__preview-pane tr:nth-child(odd) td { background: var(--aparte-art-paper-row-alt); }\\n.aparte-art-file__preview-pane tr:first-child td {\\n background: var(--aparte-art-paper-head-bg);\\n font-weight: var(--aparte-font-weight-semibold);\\n position: sticky; top: 0;\\n color: var(--aparte-art-paper-head-text);\\n}\\n.aparte-art-file__preview-empty {\\n padding: var(--aparte-art-file-preview-padding); text-align: center;\\n color: var(--aparte-art-paper-text-muted);\\n font-size: var(--aparte-font-size-md); font-style: italic;\\n}\\n.aparte-segment-artifact-file[data-state=\\\"error\\\"] .aparte-art-file__icon {\\n background: var(--aparte-art-file-icon-bg-pdf);\\n}\\n.aparte-art-file__error {\\n padding: var(--aparte-space-8) var(--aparte-art-file-error-padding-inline);\\n background: var(--aparte-error-bg);\\n border-top: var(--aparte-border-width) solid var(--aparte-error-border);\\n color: var(--aparte-text);\\n}\\n.aparte-art-file__error-title {\\n font-weight: var(--aparte-font-weight-semibold);\\n font-size: var(--aparte-font-size-base);\\n margin-bottom: var(--aparte-space-3);\\n /* `--aparte-error-text`, not `--aparte-error-title`: core removed the second one\\n with the error renderer's private classes and nothing here followed, so this\\n read was invalid at computed-value time and the heading quietly took the\\n panel's body ink instead of the error ink. */\\n color: var(--aparte-error-text);\\n}\\n/* A computed value shown back, which is `.aparte-output`. What stays is that this\\n one is an ERROR — tinted text on its own quiet ground — and that a raw message has\\n no spaces to wrap on, so it must be allowed to break mid-word. */\\n.aparte-art-file__error-msg {\\n display: block;\\n padding: var(--aparte-space-3) var(--aparte-space-5);\\n margin-bottom: var(--aparte-space-4);\\n background: var(--aparte-art-file-error-msg-bg);\\n color: var(--aparte-error-text);\\n word-break: break-word;\\n}\\n.aparte-art-file__error-hint {\\n font-size: var(--aparte-font-size-sm);\\n color: var(--aparte-text-muted);\\n font-style: italic;\\n}\\n/* ── The card's code and table panes ─────────────────────────\\n These three lived in core's prose sheet, styling this card's DOM from a sheet\\n that knew nothing else about it. */\\n.aparte-art-card__pane[data-pane=\\\"code\\\"] pre {\\n margin: 0; padding: var(--aparte-space-6);\\n font-size: var(--aparte-font-size-md);\\n background: var(--aparte-code-bg);\\n}\\n.aparte-art-file__code-pane pre {\\n margin: 0; padding: var(--aparte-space-6);\\n font-size: var(--aparte-font-size-md);\\n font-family: var(--aparte-code-font-family);\\n}\\n.aparte-art-file__preview-pane table {\\n border-collapse: collapse;\\n width: 100%;\\n font-size: var(--aparte-font-size-md);\\n font-family: inherit;\\n}\\n\"","/**\n * The artifact card: the inline Code/Preview panel a streamed artifact renders as.\n *\n * One thing, at length — and it used to be twice this size, half of it a `getStyles()`\n * block, which is why it kept `segment-renderers.ts` at 1900 lines while that file's own\n * banners described it as a renderer registry plus nine small renderers. It is that now,\n * and the CSS has since gone to `styles/segment/artifact.css` with every other built-in's.\n *\n * The card owns the previewable path. When a segment's kind is binary (pdf/xlsx/docx)\n * it DELEGATES to `./binary-file.ts` — which is why `BINARY_FILE_KINDS` lives\n * here despite the name: the card is what consults it to decide to hand over. A\n * call-site census settled that; grouping it as \"shared\" was the first draft's mistake.\n *\n * The preview frame is deliberately not built at render time. It is mounted only when\n * the user presses Preview (`mountPreviewFrame`), because a previewable artifact is\n * model-authored code and mounting it unasked executes it — ratified decision #8\n * applied to a tier-(c) affordance. The document it mounts comes from\n * `./preview-document.ts`, or from the consumer's own builder.\n */\nimport { escapeHtml, escapeAttr, copyText, contextConfig } from '@aparte/core';\nimport type { AparteSegmentRenderer } from '@aparte/core';\nimport { ARTIFACT_SEGMENT_TYPE, type ArtifactSegment } from './segment.js';\nimport { deriveArtifactKind } from './kinds.js';\nimport { stripCodeFences, labelForKind } from './shared.js';\nimport { streamHighlight } from './highlight.js';\nimport { PREVIEW_CSP, buildSafePreviewDocument } from './preview-document.js';\nimport { renderOptions, type ArtifactOpenEventDetail } from './options.js';\nimport {\n renderBinaryFileArtifact,\n setupBinaryFileArtifact,\n updateBinaryFileArtifact,\n} from './binary-file.js';\nimport artifactStyles from './artifact.css?raw';\n\n/** The segment shape the card draws — exported for a consumer's own tests and renderers. */\nexport type AparteArtifactSegment = ArtifactSegment;\n\nconst PREVIEWABLE_KINDS: ReadonlySet<string> = new Set(['react', 'html', 'svg', 'js', 'css']);\n\n/**\n * One id space per rendered card, monotonic — the same shape `aparte-optgroup` uses\n * for its label ids, and for the same reason: the id has to be UNIQUE on the page,\n * and nothing about the content can promise that.\n */\nlet cardIdSeq = 0;\n\n/** Binary file kinds: their source is implementation noise; the app produces the file. */\nconst BINARY_FILE_KINDS: ReadonlySet<string> = new Set(['pdf', 'xlsx', 'docx']);\n\n/** What each card's code pane holds, so an update that changes nothing does not rewrite it. */\nconst paneContent = new WeakMap<Element, string>();\n\n/** Whether the Preview tab exists at all, per the app's `preview` option. */\nfunction previewEnabled(): boolean {\n return renderOptions(contextConfig()).preview !== false;\n}\n\nexport const artifactRenderer: AparteSegmentRenderer<ArtifactSegment> = {\n type: ARTIFACT_SEGMENT_TYPE,\n render: (segment: ArtifactSegment) => {\n const kind = (segment.artifactType || 'unknown').toLowerCase();\n // Binary file kinds (xlsx/pdf/docx) follow a separate track: the generated JS\n // is implementation noise, so the file card shows progress, then the file.\n if (BINARY_FILE_KINDS.has(kind)) {\n return renderBinaryFileArtifact(segment, kind);\n }\n const title = segment.title?.trim() || labelForKind(kind);\n const displayLang = languageForKind(kind);\n const isStreaming = !!segment.isStreaming;\n const previewable = PREVIEWABLE_KINDS.has(kind) && previewEnabled();\n // `t()`, not `getLocale().x ?? 'X'`: `t()` already falls back to\n // APARTE_DEFAULT_LOCALE, so the English lives in ONE place instead of being\n // re-typed at each call site — and `setLocale()` REPLACES rather than merges,\n // which makes a partial locale the normal case rather than an edge one.\n // `download` is title AND aria-label: they disagreed on the button next door,\n // whose title went through `t('copy')` while its aria-label said \"Copy\" in\n // every language.\n const cfg = contextConfig();\n const downloadLabel = cfg.t('download');\n const openLabel = cfg.t('openArtifact');\n const openable = renderOptions(cfg).open === true;\n const previewLabel = cfg.t('preview');\n const codeLabel = cfg.t('code');\n const isBinary = BINARY_FILE_KINDS.has(kind);\n const cleanContent = stripCodeFences(segment.content || '');\n /*\n * Ids, so the tabs can POINT at their panels.\n *\n * The card announced `role=\"tablist\"` with `role=\"tab\"` buttons and shipped none\n * of the pattern's obligations: no `aria-controls`, no `role=\"tabpanel\"`, no ids\n * and no arrow keys. A role is a promise about behaviour — declaring tablist and\n * then behaving like two ordinary buttons tells a screen-reader user to expect a\n * relationship and a keyboard model that are not there, which is worse than the\n * plain buttons it actually was.\n *\n * A COUNTER, not the segment's id. The segment's id is the model's — a tool-call\n * segment is `tool-${toolCallId}` straight off the wire — so two calls answering\n * to one id gave two cards the same `id` and the same `aria-controls`, and\n * `getElementById` returns whichever parsed first: exactly the collision this\n * scoping exists to prevent. An author-chosen `id` is also the DOM-clobbering\n * shape core's sanitizer refuses on model markup.\n *\n * Escaped HERE rather than inside an id-building helper: `check:attr-escaping`\n * follows a local produced by an escaper and cannot see through a function, and\n * teaching it to trust the helper's NAME is precisely the hole that guard was\n * just tightened to close. (The value is digits; the escaper is what the reader\n * and the guard are owed, since the position is what makes the rule.)\n */\n const cardId = escapeAttr(String(++cardIdSeq));\n // The card ALWAYS opens on the code tab, and the preview frame is not built\n // here at all — it is mounted only when the user presses Preview\n // (`mountPreviewFrame`, called from the tab handler).\n //\n // It used to open on Preview for any artifact that was not streaming, i.e.\n // every render of a completed one — so reloading a persisted conversation\n // executed the model's JS with no gesture. Defaulting the tab is not enough\n // on its own either: a `display:none` iframe still loads and still runs\n // scripts, so the frame has to be ABSENT, not hidden.\n //\n // Ratified decision #8, applied to a tier-(c) affordance: content the app\n // did not produce does not get to act on its own.\n\n return `\n <div class=\"aparte-segment aparte-card aparte-segment-artifact-card\"\n data-segment-id=\"${escapeHtml(segment.id)}\"\n data-artifact-type=\"${escapeHtml(kind)}\"\n data-streaming=\"${isStreaming ? 'true' : 'false'}\"\n data-tab=\"code\"\n data-previewable=\"${previewable ? 'true' : 'false'}\"\n data-binary=\"${isBinary ? 'true' : 'false'}\">\n <header class=\"aparte-art-card__header\">\n <div class=\"aparte-art-card__title-block\">\n <span class=\"aparte-badge aparte-badge--outline aparte-art-card__kind\" data-kind=\"${escapeHtml(kind)}\">${escapeHtml(displayLang)}</span>\n <span class=\"aparte-art-card__title\">${escapeHtml(title)}</span>\n ${isStreaming ? `<span class=\"aparte-dot aparte-art-card__pulse\" role=\"img\" aria-label=\"${escapeAttr(cfg.t('generating'))}\"></span>` : ''}\n </div>\n <div class=\"aparte-art-card__actions\">\n <button type=\"button\" class=\"aparte-btn aparte-btn--icon aparte-art-card__btn\" data-action=\"copy\" title=\"${escapeAttr(contextConfig().t('copy'))}\" aria-label=\"${escapeAttr(contextConfig().t('copy'))}\">\n ${contextConfig().getIcon('copy')}\n </button>\n <button type=\"button\" class=\"aparte-btn aparte-btn--icon aparte-art-card__btn\" data-action=\"download\" title=\"${escapeAttr(downloadLabel)}\" aria-label=\"${escapeAttr(downloadLabel)}\" ${isStreaming ? 'disabled' : ''}>\n ${contextConfig().getIcon('download')}\n </button>\n ${openable ? `<button type=\"button\" class=\"aparte-btn aparte-btn--icon aparte-art-card__btn\" data-action=\"open\" title=\"${escapeAttr(openLabel)}\" aria-label=\"${escapeAttr(openLabel)}\" ${isStreaming ? 'disabled' : ''}>${contextConfig().getIcon('expand')}</button>` : ''}\n </div>\n </header>\n <nav class=\"aparte-tabs aparte-tabs--underline aparte-art-card__tabs\" role=\"tablist\">\n <button type=\"button\" class=\"aparte-tabs__tab\" role=\"tab\" id=\"aparte-art-${cardId}-tab-code\" aria-controls=\"aparte-art-${cardId}-pane-code\" aria-selected=\"true\" tabindex=\"0\" data-tab-target=\"code\">${escapeHtml(codeLabel)}</button>\n ${previewable ? `<button type=\"button\" class=\"aparte-tabs__tab\" role=\"tab\" id=\"aparte-art-${cardId}-tab-preview\" aria-controls=\"aparte-art-${cardId}-pane-preview\" aria-selected=\"false\" tabindex=\"-1\" data-tab-target=\"preview\" ${isStreaming ? 'disabled' : ''}>${escapeHtml(previewLabel)}</button>` : ''}\n </nav>\n <div class=\"aparte-art-card__body\">\n <div class=\"aparte-art-card__pane\" role=\"tabpanel\" id=\"aparte-art-${cardId}-pane-code\" aria-labelledby=\"aparte-art-${cardId}-tab-code\" tabindex=\"0\" data-pane=\"code\">\n <div class=\"aparte-code-content-wrapper\">\n <pre><code class=\"language-${escapeHtml(displayLang)}\">${escapeHtml(cleanContent)}</code></pre>\n </div>\n </div>\n ${previewable ? `\n <div class=\"aparte-art-card__pane\" role=\"tabpanel\" id=\"aparte-art-${cardId}-pane-preview\" aria-labelledby=\"aparte-art-${cardId}-tab-preview\" tabindex=\"0\" data-pane=\"preview\">\n <div class=\"aparte-art-card__pending\">${escapeHtml(cfg.t('previewPending'))}</div>\n </div>\n ` : ''}\n </div>\n </div>\n `;\n },\n /**\n * Every string this card shows: the copy button's tooltip, glyph and accessible\n * name, the download button's title and label, and the two tab names. They were\n * all hardcoded literals until they got locale keys — including the copy button's\n * `aria-label`, which said \"Copy\" while its own `title` one attribute away already\n * went through `t('copy')`, so a French reader got a French tooltip and an English\n * announcement.\n *\n * Nothing here touches the tab state or the preview pane: a mounted iframe is\n * running model-authored code, and re-rendering this card is exactly what the\n * hook exists to avoid.\n */\n relabel: (element: HTMLElement) => {\n const cfg = contextConfig();\n const copyBtn = element.querySelector('.aparte-art-card__btn[data-action=\"copy\"]');\n if (copyBtn) {\n copyBtn.setAttribute('title', cfg.t('copy'));\n copyBtn.setAttribute('aria-label', cfg.t('copy'));\n copyBtn.innerHTML = cfg.getIcon('copy');\n }\n const dl = element.querySelector('.aparte-art-card__btn[data-action=\"download\"]');\n if (dl) {\n const label = cfg.t('download');\n dl.setAttribute('title', label);\n dl.setAttribute('aria-label', label);\n }\n const open = element.querySelector('.aparte-art-card__btn[data-action=\"open\"]');\n if (open) {\n const label = cfg.t('openArtifact');\n open.setAttribute('title', label);\n open.setAttribute('aria-label', label);\n }\n // Text only, and never `aria-selected` or `data-tab`: which pane is open is\n // the reader's state, not the locale's. A relabel that touched it would close\n // a preview somebody had opened.\n const previewTab = element.querySelector('[data-tab-target=\"preview\"]');\n if (previewTab) previewTab.textContent = cfg.t('preview');\n const codeTab = element.querySelector('[data-tab-target=\"code\"]');\n if (codeTab) codeTab.textContent = cfg.t('code');\n },\n setup: (element: HTMLElement, segment: AparteArtifactSegment) => {\n latestSegment.set(element, segment);\n const kind = (segment.artifactType || '').toLowerCase();\n if (BINARY_FILE_KINDS.has(kind)) {\n setupBinaryFileArtifact(element, segment, kind);\n return;\n }\n // Async highlight on the code pane\n const wrapper = element.querySelector('.aparte-code-content-wrapper');\n if (wrapper) {\n const displayLang = languageForKind(kind);\n const cleanContent = stripCodeFences(segment.content || '');\n paneContent.set(element, cleanContent);\n void contextConfig().highlightCode(cleanContent, displayLang).then(html => {\n wrapper.innerHTML = html;\n }).catch(() => { /* best-effort: a failed highlight degrades silently */ });\n }\n\n /*\n * Tab switching — and, for Preview, the one place the frame is created.\n *\n * A `tablist` is a SINGLE tab stop with arrow keys inside it, not N tab stops.\n * This shipped as two ordinary buttons under a tablist role: Tab landed on each\n * in turn and the arrows did nothing, so the role promised a keyboard model the\n * card did not have. The roving `tabindex` and the arrow handling below are that\n * model; `aria-controls` / `role=\"tabpanel\"` in the markup are the other half.\n */\n const tabs = [...element.querySelectorAll<HTMLButtonElement>('[data-tab-target]')];\n const select = (btn: HTMLButtonElement, focus: boolean): void => {\n const target = btn.getAttribute('data-tab-target');\n if (!target || btn.disabled) return;\n if (target === 'preview') mountPreviewFrame(element, segment);\n element.setAttribute('data-tab', target);\n for (const b of tabs) {\n const on = b === btn;\n b.setAttribute('aria-selected', on ? 'true' : 'false');\n b.tabIndex = on ? 0 : -1;\n }\n if (focus) btn.focus();\n };\n for (const [i, btn] of tabs.entries()) {\n btn.addEventListener('click', () => select(btn, false));\n btn.addEventListener('keydown', (e) => {\n // Home/End as well as the arrows: both are in the pattern, and on a\n // two-tab list they are the fastest way to the one you are not on.\n const step = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;\n let next = -1;\n if (step) next = (i + step + tabs.length) % tabs.length;\n else if (e.key === 'Home') next = 0;\n else if (e.key === 'End') next = tabs.length - 1;\n if (next < 0) return;\n e.preventDefault();\n // Skip a disabled tab rather than trapping focus on it: Preview is\n // disabled while the artifact still streams.\n for (let n = 0; n < tabs.length; n++) {\n const candidate = tabs[(next + n * (step || 1) + tabs.length) % tabs.length];\n if (candidate && !candidate.disabled) { select(candidate, true); return; }\n }\n });\n }\n\n // Open — handed to the app, which shows it where it wants. The LATEST segment.\n const openBtn = element.querySelector<HTMLButtonElement>('[data-action=\"open\"]');\n if (openBtn) {\n openBtn.addEventListener('click', () => {\n // Not while it streams, like Download: the app would get a half-written document.\n if (openBtn.disabled) return;\n const latest = latestSegment.get(element) ?? segment;\n // As the card shows it, copies it and downloads it: without the code fences a\n // model may wrap the content in.\n const artifact = { ...latest, content: stripCodeFences(latest.content || '') };\n element.dispatchEvent(new CustomEvent<ArtifactOpenEventDetail>('aparte-artifact-open', {\n bubbles: true,\n composed: true,\n detail: { artifact },\n }));\n });\n }\n\n // Copy\n const copyBtn = element.querySelector<HTMLButtonElement>('[data-action=\"copy\"]');\n if (copyBtn) {\n copyBtn.addEventListener('click', () => {\n // Late execution (user click) — resolve from the element, and copy the\n // latest segment, not the one this closure captured: see `latestSegment`.\n const code = stripCodeFences((latestSegment.get(element) ?? segment).content || '');\n void copyText(code).catch(() => { /* best-effort: a failed clipboard write degrades silently */ });\n const original = copyBtn.innerHTML;\n copyBtn.innerHTML = contextConfig(copyBtn).getIcon('check');\n copyBtn.setAttribute('title', contextConfig(copyBtn).t('copied'));\n setTimeout(() => {\n copyBtn.innerHTML = original;\n copyBtn.setAttribute('title', contextConfig(copyBtn).t('copy'));\n }, 1500);\n });\n }\n\n // Download — a text artifact downloads from its own content. (A binary one\n // never reaches here: its setup returned above, and its file card owns the\n // button.) The LATEST segment, not the one this closure captured.\n const dlBtn = element.querySelector<HTMLButtonElement>('[data-action=\"download\"]');\n if (dlBtn) {\n dlBtn.addEventListener('click', () => {\n if (dlBtn.disabled) return;\n downloadTextArtifact(latestSegment.get(element) ?? segment);\n });\n }\n },\n update: (element: HTMLElement, segment: AparteArtifactSegment) => {\n // Keep the click handler's view of the artifact current, and throw away a\n // frame that is now showing stale content so the next press rebuilds it.\n const previous = latestSegment.get(element);\n latestSegment.set(element, segment);\n if (previous && previous.content !== segment.content) {\n element.querySelector('.aparte-art-card__pane[data-pane=\"preview\"] iframe')?.remove();\n }\n const isStreaming = !!segment.isStreaming;\n const kind = (segment.artifactType || '').toLowerCase();\n if (BINARY_FILE_KINDS.has(kind)) {\n updateBinaryFileArtifact(element, segment, isStreaming);\n return;\n }\n const wasStreaming = element.getAttribute('data-streaming') === 'true';\n const cleanContent = stripCodeFences(segment.content || '');\n\n // 1. Live-update the code pane during streaming.\n //\n // One call owns both halves — the plain tail every token and the coloured\n // prefix on a throttle. Setting `textContent` here as well is what made the\n // pane flicker: it erased the highlighter's spans on every token.\n if (isStreaming) {\n const segId = element.getAttribute('data-segment-id') ?? segment.id;\n streamHighlight(element, '.aparte-code-content-wrapper', cleanContent, languageForKind(kind), segId);\n } else if (paneContent.get(element) !== cleanContent) {\n // Only a CHANGED content is written. A card built from a tool result is settled\n // from its first paint and still receives every update the tool segment gets;\n // writing `textContent` each time wiped the token spans `setup` had painted, and\n // with no stream ending there was no second highlight — a plain block in a Shiki\n // frame. A changed one is written and coloured again.\n paneContent.set(element, cleanContent);\n const displayLang = languageForKind(kind);\n const wrapper = element.querySelector('.aparte-code-content-wrapper');\n const codeEl = wrapper?.querySelector('code');\n if (codeEl) {\n codeEl.textContent = cleanContent;\n } else if (wrapper) {\n wrapper.innerHTML = `<pre><code class=\"language-${escapeHtml(displayLang)}\">${escapeHtml(cleanContent)}</code></pre>`;\n }\n if (wrapper && !wasStreaming) {\n void contextConfig().highlightCode(cleanContent, displayLang).then(html => {\n if (paneContent.get(element) === cleanContent) wrapper.innerHTML = html;\n }).catch(() => { /* best-effort: a failed highlight degrades silently */ });\n }\n }\n\n // 2. On stream-completion: highlight + build preview iframe + auto-switch\n if (wasStreaming && !isStreaming) {\n element.setAttribute('data-streaming', 'false');\n\n // The pulse says \"the model is still writing this\". `render()` painted it\n // and nothing ever took it away, so a finished document kept claiming to be\n // in flight — forever, at 1.2s a cycle. Unnoticed until the artifact demo\n // actually streamed: the indicator had never been exercised.\n element.querySelector('.aparte-art-card__pulse')?.remove();\n\n // Re-run syntax highlight now that content is final\n const wrapper = element.querySelector('.aparte-code-content-wrapper');\n if (wrapper) {\n const displayLang = languageForKind(kind);\n void contextConfig().highlightCode(cleanContent, displayLang).then(html => {\n wrapper.innerHTML = html;\n }).catch(() => { /* best-effort: a failed highlight degrades silently */ });\n }\n\n // Enable previously-disabled buttons (download, preview tab)\n element.querySelectorAll<HTMLButtonElement>('button[disabled]').forEach(b => {\n b.disabled = false;\n });\n\n // Nothing to build and nothing to switch: enabling the Preview button\n // above is the whole of it. The frame is mounted by the tab handler, on\n // a real user press — see the note at `initialTab`.\n }\n },\n // The card's sheet, injected once per document by core — the seam a renderer that\n // is not core's has onto the page. It is a real `.css` file, inlined at build.\n getStyles: () => artifactStyles,\n};\n\nfunction languageForKind(kind: string): string {\n if (kind === 'react') return 'jsx';\n if (kind === 'markdown') return 'md';\n if (kind === 'pdf' || kind === 'xlsx' || kind === 'docx') return 'js';\n return kind || 'text';\n}\n\nfunction downloadTextArtifact(segment: AparteArtifactSegment): void {\n const content = stripCodeFences(segment.content || '');\n const kind = (segment.artifactType || '').toLowerCase();\n const ext = ({\n react: 'jsx', html: 'html', svg: 'svg', js: 'js', css: 'css',\n json: 'json', markdown: 'md', csv: 'csv', text: 'txt',\n python: 'py', typescript: 'ts', bash: 'sh', sql: 'sql',\n } as Record<string, string>)[kind] ?? 'txt';\n const baseTitle = (segment.title ?? labelForKind(kind)).trim();\n const safeBase = slugifyForFilename(baseTitle) || 'artifact';\n const filename = `${safeBase}.${ext}`;\n const mime = segment.mimeType || 'text/plain';\n const blob = new Blob([content], { type: mime });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n}\n\nfunction slugifyForFilename(text: string): string {\n const lower = text.trim().toLowerCase();\n let out = '';\n let prevDash = false;\n for (let i = 0; i < lower.length && out.length < 40; i++) {\n const ch = lower[i]!;\n const isAlnum = (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9');\n if (isAlnum) { out += ch; prevDash = false; continue; }\n if (!prevDash && out.length > 0) { out += '-'; prevDash = true; }\n }\n if (out.endsWith('-')) out = out.slice(0, -1);\n return out;\n}\n\n/**\n * The LATEST segment for a mounted artifact card.\n *\n * `setup()` runs once and closes over the segment it was handed; the bubble builds a\n * fresh object on every `updateSegment`, so that closure freezes the artifact as it\n * was when it was ADDED — which for anything streamed is `content: ''`. Pressing\n * Preview then ran an empty document. Gesture-gating the frame moved the read from\n * `update()` (which always had the current segment) into a closure that did not, and\n * the test written with it never streamed, so it could not see this.\n */\nconst latestSegment = new WeakMap<HTMLElement, AparteArtifactSegment>();\n\nfunction mountPreviewFrame(element: HTMLElement, fallback: AparteArtifactSegment): void {\n const pane = element.querySelector('.aparte-art-card__pane[data-pane=\"preview\"]');\n if (!pane || pane.querySelector('iframe')) return;\n\n // The latest segment, not the one the closure captured — see `latestSegment`.\n const segment = latestSegment.get(element) ?? fallback;\n // Lower-cased like every other reader of the kind in this file: an app-built\n // segment with `artifactType: 'HTML'` rendered an enabled Preview tab whose press\n // did nothing, because `PREVIEWABLE_KINDS.has('HTML')` is false.\n const kind = (segment.artifactType || deriveArtifactKind(segment.mimeType ?? '', 'text')).toLowerCase();\n if (!PREVIEWABLE_KINDS.has(kind) || !previewEnabled()) return;\n\n const title = segment.title?.trim() || labelForKind(kind);\n // The app's builder when it gave one (`preview: (kind, body, title) => srcdoc`),\n // else the built-in document with its CSP.\n const option = renderOptions(contextConfig(element)).preview;\n const build = typeof option === 'function' ? option : buildSafePreviewDocument;\n const srcdoc = build(kind, stripCodeFences(segment.content || ''), title);\n\n /*\n * `sandbox=\"allow-scripts\"` and nothing else: no `allow-same-origin` (opaque\n * origin — the frame cannot read this page, its storage, or the key), no\n * `allow-forms` (it cannot POST out), no `allow-top-navigation` (it cannot move\n * the tab), no `allow-popups`.\n *\n * It CAN navigate itself, which no sandbox token and no CSP directive prevents —\n * see the note on PREVIEW_CSP. `referrerpolicy=\"no-referrer\"` at least keeps the\n * host URL out of that request.\n */\n pane.innerHTML = `<iframe class=\"aparte-art-card__frame\"`\n + ` sandbox=\"allow-scripts\"`\n + ` csp=\"${escapeAttr(PREVIEW_CSP)}\"`\n + ` referrerpolicy=\"no-referrer\" loading=\"lazy\"`\n + ` title=\"${escapeAttr(title)}\" srcdoc=\"${escapeAttr(srcdoc)}\"></iframe>`;\n}\n","/**\n * @aparte/plugin-artifacts\n *\n * An artifact is a document the model produces — a page, a component, a script, a\n * spreadsheet — and nothing a model does by nature: it is a convention an app teaches\n * it. This package is that convention, end to end, for aparté:\n *\n * - a real `create_artifact` **tool** the model calls (`./tool.ts`), whose structured\n * result is the document;\n * - a **tool renderer** that draws that result as the Code/Preview card;\n * - the `<artifact …>…</artifact>` **block grammar**, for a model that writes one in\n * its prose, registered on core's parser (`registerStreamBlock`);\n * - the **segment renderer** for the segment that grammar produces — the same card.\n *\n * One implementation, four registrations, all made by `setupArtifacts()`.\n *\n * Usage:\n * import { setupArtifacts } from '@aparte/plugin-artifacts';\n * setupArtifacts();\n */\nimport { aparteGlobalConfig, registerSegmentRenderer, unregisterSegmentRenderer, type AparteConfig, type AparteToolRenderer } from '@aparte/core';\nimport { createArtifactTool, artifactHandler } from './tool.js';\nimport { artifactRenderer } from './card.js';\nimport { artifactBlock, artifactFromToolCall, ARTIFACT_TAG, ARTIFACT_SEGMENT_TYPE } from './segment.js';\nimport { setRenderOptions, clearRenderOptions, type ArtifactsSetupOptions } from './options.js';\n\n/**\n * Register the tool, its renderer, the block grammar and the segment renderer on\n * `config` (the global config by default). Call once at application startup;\n * returns a function that unregisters all four.\n */\nexport function setupArtifacts(options: ArtifactsSetupOptions = {}, config: AparteConfig = aparteGlobalConfig): () => void {\n const tool = createArtifactTool(options);\n setRenderOptions(config, { preview: options.preview, onBinary: options.onBinary, open: options.open });\n\n config.registerTool(tool, artifactHandler);\n // The card, on the tool's result: the same renderer the segment gets, adapted to\n // the tool-call segment it is handed. `update` and `relabel` are forwarded, so a\n // preview a reader mounted survives the result landing.\n const toolRenderer: AparteToolRenderer = {\n render: (segment) => artifactRenderer.render(artifactFromToolCall(segment)),\n setup: (element, segment) => artifactRenderer.setup?.(element, artifactFromToolCall(segment)),\n update: (element, segment) => artifactRenderer.update?.(element, artifactFromToolCall(segment)),\n relabel: (element, segment) => artifactRenderer.relabel?.(element, artifactFromToolCall(segment)),\n getStyles: () => artifactRenderer.getStyles?.() ?? '',\n };\n config.registerToolRenderer(tool.name, toolRenderer);\n\n registerSegmentRenderer(artifactRenderer, config);\n const tag = options.tag === undefined ? ARTIFACT_TAG : options.tag;\n if (tag) config.registerStreamBlock(artifactBlock(tag));\n\n return () => {\n config.unregisterTool(tool.name);\n config.unregisterToolRenderer(tool.name);\n unregisterSegmentRenderer(ARTIFACT_SEGMENT_TYPE, config);\n if (tag) config.unregisterStreamBlock(tag);\n clearRenderOptions(config);\n };\n}\n\nexport { createArtifactTool, artifactHandler, ARTIFACT_SYSTEM_PROMPT } from './tool.js';\nexport type { ArtifactToolOptions } from './tool.js';\nexport { artifactRenderer } from './card.js';\nexport type { AparteArtifactSegment } from './card.js';\nexport { artifactBlock, artifactSegment, artifactFromToolCall, ARTIFACT_TAG, ARTIFACT_SEGMENT_TYPE } from './segment.js';\nexport type { ArtifactSegment, ArtifactInput } from './segment.js';\nexport { deriveArtifactKind } from './kinds.js';\nexport { buildSafePreviewDocument, PREVIEW_CSP } from './preview-document.js';\nexport type { ArtifactRenderOptions, ArtifactPreviewBuilder, ArtifactBinary, ArtifactBinaryResolver, ArtifactsSetupOptions, ArtifactOpenEventDetail } from './options.js';\nexport type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';\n"],"names":["kind","e"],"mappings":"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"}
|
package/dist/index.node.d.ts
CHANGED
|
@@ -27,6 +27,6 @@ export { artifactBlock, artifactSegment, artifactFromToolCall, ARTIFACT_TAG, ART
|
|
|
27
27
|
export type { ArtifactSegment, ArtifactInput } from './segment.js';
|
|
28
28
|
export { deriveArtifactKind } from './kinds.js';
|
|
29
29
|
export { buildSafePreviewDocument, PREVIEW_CSP } from './preview-document.js';
|
|
30
|
-
export type { ArtifactRenderOptions, ArtifactPreviewBuilder, ArtifactBinary, ArtifactBinaryResolver, ArtifactsSetupOptions } from './options.js';
|
|
30
|
+
export type { ArtifactRenderOptions, ArtifactPreviewBuilder, ArtifactBinary, ArtifactBinaryResolver, ArtifactsSetupOptions, ArtifactOpenEventDetail } from './options.js';
|
|
31
31
|
export type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';
|
|
32
32
|
//# sourceMappingURL=index.node.d.ts.map
|
package/dist/index.node.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.node.d.ts","sourceRoot":"","sources":["../src/index.node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAGrE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1D,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,EAAE,MAAM,GAAE,YAAiC,GAAG,MAAM,IAAI,CAWzH;AAED,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACxF,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACzH,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC9E,YAAY,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,cAAc,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.node.d.ts","sourceRoot":"","sources":["../src/index.node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAGrE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1D,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,EAAE,MAAM,GAAE,YAAiC,GAAG,MAAM,IAAI,CAWzH;AAED,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACxF,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACzH,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC9E,YAAY,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,cAAc,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAC1K,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.node.js","sources":["../src/index.node.ts"],"sourcesContent":["/**\n * `@aparte/plugin-artifacts` — the DOM-free entry, for Node and SSR.\n *\n * The browser barrel carries the card, which builds DOM and reads a stylesheet; an\n * SSR build that evaluates the import on the server needs neither. What is here is\n * what a server can legitimately use: the tool, its handler, the block grammar (the\n * parser runs anywhere), the preview document builder and the types. Calling\n * `setupArtifacts()` here registers the tool and the grammar without a renderer, which\n * is the correct outcome: nothing is being rendered there.\n *\n * `buildSafePreviewDocument` and `PREVIEW_CSP` are pure string work over `escapeHtml` /\n * `escapeAttr`, both of which core's own node barrel carries — they were missing here\n * for no reason but the omission, and the consequence was not a missing feature but a\n * hard `SyntaxError: does not provide an export named 'buildSafePreviewDocument'` the\n * moment an SSR build evaluated the import. `ArtifactsSetupOptions` is imported from\n * `./options.js` rather than declared again: this file used to declare a SECOND\n * interface of that name, without the render half, so the same name meant two shapes\n * depending on which export condition resolved.\n */\nimport { aparteGlobalConfig, type AparteConfig } from '@aparte/core';\nimport { createArtifactTool, artifactHandler } from './tool.js';\nimport { artifactBlock, ARTIFACT_TAG } from './segment.js';\nimport type { ArtifactsSetupOptions } from './options.js';\n\n/** Register the tool, its handler and the grammar on the server. No renderer: it builds DOM. */\nexport function setupArtifacts(options: ArtifactsSetupOptions = {}, config: AparteConfig = aparteGlobalConfig): () => void {\n const tool = createArtifactTool(options);\n config.registerTool(tool, artifactHandler);\n config.registerToolRenderer(tool.name, { render: () => '' });\n const tag = options.tag === undefined ? ARTIFACT_TAG : options.tag;\n if (tag) config.registerStreamBlock(artifactBlock(tag));\n return () => {\n config.unregisterTool(tool.name);\n config.unregisterToolRenderer(tool.name);\n if (tag) config.unregisterStreamBlock(tag);\n };\n}\n\nexport { createArtifactTool, artifactHandler, ARTIFACT_SYSTEM_PROMPT } from './tool.js';\nexport type { ArtifactToolOptions } from './tool.js';\nexport { artifactBlock, artifactSegment, artifactFromToolCall, ARTIFACT_TAG, ARTIFACT_SEGMENT_TYPE } from './segment.js';\nexport type { ArtifactSegment, ArtifactInput } from './segment.js';\nexport { deriveArtifactKind } from './kinds.js';\nexport { buildSafePreviewDocument, PREVIEW_CSP } from './preview-document.js';\nexport type { ArtifactRenderOptions, ArtifactPreviewBuilder, ArtifactBinary, ArtifactBinaryResolver, ArtifactsSetupOptions } from './options.js';\nexport type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';\n"],"names":[],"mappings":";;;AAyBO,SAAS,eAAe,UAAiC,IAAI,SAAuB,oBAAgC;AACvH,QAAM,OAAO,mBAAmB,OAAO;AACvC,SAAO,aAAa,MAAM,eAAe;AACzC,SAAO,qBAAqB,KAAK,MAAM,EAAE,QAAQ,MAAM,IAAI;AAC3D,QAAM,MAAM,QAAQ,QAAQ,SAAY,eAAe,QAAQ;AAC/D,MAAI,IAAK,QAAO,oBAAoB,cAAc,GAAG,CAAC;AACtD,SAAO,MAAM;AACT,WAAO,eAAe,KAAK,IAAI;AAC/B,WAAO,uBAAuB,KAAK,IAAI;AACvC,QAAI,IAAK,QAAO,sBAAsB,GAAG;AAAA,EAC7C;AACJ;"}
|
|
1
|
+
{"version":3,"file":"index.node.js","sources":["../src/index.node.ts"],"sourcesContent":["/**\n * `@aparte/plugin-artifacts` — the DOM-free entry, for Node and SSR.\n *\n * The browser barrel carries the card, which builds DOM and reads a stylesheet; an\n * SSR build that evaluates the import on the server needs neither. What is here is\n * what a server can legitimately use: the tool, its handler, the block grammar (the\n * parser runs anywhere), the preview document builder and the types. Calling\n * `setupArtifacts()` here registers the tool and the grammar without a renderer, which\n * is the correct outcome: nothing is being rendered there.\n *\n * `buildSafePreviewDocument` and `PREVIEW_CSP` are pure string work over `escapeHtml` /\n * `escapeAttr`, both of which core's own node barrel carries — they were missing here\n * for no reason but the omission, and the consequence was not a missing feature but a\n * hard `SyntaxError: does not provide an export named 'buildSafePreviewDocument'` the\n * moment an SSR build evaluated the import. `ArtifactsSetupOptions` is imported from\n * `./options.js` rather than declared again: this file used to declare a SECOND\n * interface of that name, without the render half, so the same name meant two shapes\n * depending on which export condition resolved.\n */\nimport { aparteGlobalConfig, type AparteConfig } from '@aparte/core';\nimport { createArtifactTool, artifactHandler } from './tool.js';\nimport { artifactBlock, ARTIFACT_TAG } from './segment.js';\nimport type { ArtifactsSetupOptions } from './options.js';\n\n/** Register the tool, its handler and the grammar on the server. No renderer: it builds DOM. */\nexport function setupArtifacts(options: ArtifactsSetupOptions = {}, config: AparteConfig = aparteGlobalConfig): () => void {\n const tool = createArtifactTool(options);\n config.registerTool(tool, artifactHandler);\n config.registerToolRenderer(tool.name, { render: () => '' });\n const tag = options.tag === undefined ? ARTIFACT_TAG : options.tag;\n if (tag) config.registerStreamBlock(artifactBlock(tag));\n return () => {\n config.unregisterTool(tool.name);\n config.unregisterToolRenderer(tool.name);\n if (tag) config.unregisterStreamBlock(tag);\n };\n}\n\nexport { createArtifactTool, artifactHandler, ARTIFACT_SYSTEM_PROMPT } from './tool.js';\nexport type { ArtifactToolOptions } from './tool.js';\nexport { artifactBlock, artifactSegment, artifactFromToolCall, ARTIFACT_TAG, ARTIFACT_SEGMENT_TYPE } from './segment.js';\nexport type { ArtifactSegment, ArtifactInput } from './segment.js';\nexport { deriveArtifactKind } from './kinds.js';\nexport { buildSafePreviewDocument, PREVIEW_CSP } from './preview-document.js';\nexport type { ArtifactRenderOptions, ArtifactPreviewBuilder, ArtifactBinary, ArtifactBinaryResolver, ArtifactsSetupOptions, ArtifactOpenEventDetail } from './options.js';\nexport type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';\n"],"names":[],"mappings":";;;AAyBO,SAAS,eAAe,UAAiC,IAAI,SAAuB,oBAAgC;AACvH,QAAM,OAAO,mBAAmB,OAAO;AACvC,SAAO,aAAa,MAAM,eAAe;AACzC,SAAO,qBAAqB,KAAK,MAAM,EAAE,QAAQ,MAAM,IAAI;AAC3D,QAAM,MAAM,QAAQ,QAAQ,SAAY,eAAe,QAAQ;AAC/D,MAAI,IAAK,QAAO,oBAAoB,cAAc,GAAG,CAAC;AACtD,SAAO,MAAM;AACT,WAAO,eAAe,KAAK,IAAI;AAC/B,WAAO,uBAAuB,KAAK,IAAI;AACvC,QAAI,IAAK,QAAO,sBAAsB,GAAG;AAAA,EAC7C;AACJ;"}
|
package/dist/options.d.ts
CHANGED
|
@@ -42,6 +42,34 @@ export interface ArtifactRenderOptions {
|
|
|
42
42
|
preview?: boolean | ArtifactPreviewBuilder;
|
|
43
43
|
/** See {@link ArtifactBinaryResolver}. */
|
|
44
44
|
onBinary?: ArtifactBinaryResolver;
|
|
45
|
+
/**
|
|
46
|
+
* An Open button on the card, which dispatches `aparte-artifact-open` (bubbling, out of
|
|
47
|
+
* shadow roots) with the artifact, for the app to show it where it wants: a split pane,
|
|
48
|
+
* a dialog, a tab. Off by default — without a listener nothing would open (ratified
|
|
49
|
+
* decision #8).
|
|
50
|
+
*/
|
|
51
|
+
open?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The artifact card's Open button was pressed (`setupArtifacts({ open: true })`): show the
|
|
55
|
+
* artifact where the app wants it. Bubbles out of the chat and out of shadow roots. The
|
|
56
|
+
* detail is the artifact as the card shows it now.
|
|
57
|
+
*
|
|
58
|
+
* @event aparte-artifact-open
|
|
59
|
+
*/
|
|
60
|
+
export interface ArtifactOpenEventDetail {
|
|
61
|
+
artifact: ArtifactSegment;
|
|
62
|
+
}
|
|
63
|
+
interface ArtifactEventMap {
|
|
64
|
+
'aparte-artifact-open': CustomEvent<ArtifactOpenEventDetail>;
|
|
65
|
+
}
|
|
66
|
+
declare global {
|
|
67
|
+
interface HTMLElementEventMap extends ArtifactEventMap {
|
|
68
|
+
}
|
|
69
|
+
interface DocumentEventMap extends ArtifactEventMap {
|
|
70
|
+
}
|
|
71
|
+
interface WindowEventMap extends ArtifactEventMap {
|
|
72
|
+
}
|
|
45
73
|
}
|
|
46
74
|
export declare function setRenderOptions(config: AparteConfig, options: ArtifactRenderOptions): void;
|
|
47
75
|
/** The options for this config, else the global config's, else the defaults. */
|
|
@@ -69,4 +97,5 @@ export interface ArtifactsSetupOptions extends ArtifactToolOptions, ArtifactRend
|
|
|
69
97
|
*/
|
|
70
98
|
tag?: string | false;
|
|
71
99
|
}
|
|
100
|
+
export {};
|
|
72
101
|
//# sourceMappingURL=options.d.ts.map
|
package/dist/options.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAErD,iFAAiF;AACjF,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;AAE3F,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC3B,gBAAgB;IAChB,MAAM,EAAE,QAAQ,CAAC;IACjB,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,QAAQ,EAAE,eAAe,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;AAE5F,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC3C,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAErD,iFAAiF;AACjF,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;AAE3F,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC3B,gBAAgB;IAChB,MAAM,EAAE,QAAQ,CAAC;IACjB,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,QAAQ,EAAE,eAAe,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;AAE5F,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC3C,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACpC,QAAQ,EAAE,eAAe,CAAC;CAC7B;AAMD,UAAU,gBAAgB;IACtB,sBAAsB,EAAE,WAAW,CAAC,uBAAuB,CAAC,CAAC;CAChE;AACD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,mBAAoB,SAAQ,gBAAgB;KAAG;IACzD,UAAU,gBAAiB,SAAQ,gBAAgB;KAAG;IACtD,UAAU,cAAe,SAAQ,gBAAgB;KAAG;CACvD;AAID,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAE3F;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,qBAAqB,CAEzE;AAED,+DAA+D;AAC/D,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAE7D;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,qBAAsB,SAAQ,mBAAmB,EAAE,qBAAqB;IACrF;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACxB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preview-document-CUKDtx2T.js","sources":["../src/tool.ts","../src/kinds.ts","../src/segment.ts","../src/preview-document.ts"],"sourcesContent":["/**\n * The `create_artifact` tool: a real `AparteTool` the app registers, with a handler that\n * returns the document as its structured result.\n *\n * It used to be a string compared in the engine (`event.name === 'create_artifact'`)\n * with no tool behind it: no app declared it to the model, no approval policy could\n * rule on it, and its result skipped the generic path. As a tool it goes through\n * everything a tool goes through — the schema the model sees, the approval gate (a\n * policy may well class writing a document as a `write`), the handler, the envelope —\n * and the card is a tool renderer on its result.\n */\nimport type { AparteTool, AparteToolCall, AparteToolResult } from '@aparte/core';\nimport type { ArtifactInput } from './segment.js';\n\n/** What the model is told, when the app does not say otherwise. */\nexport const ARTIFACT_SYSTEM_PROMPT =\n 'When the user asks for a document, a page, a component, a diagram or a file — anything '\n + 'they will keep, edit or run rather than read once — produce it with the create_artifact '\n + 'tool, with a MIME type that names what it is, rather than pasting it into your reply. '\n + 'Keep your reply short: say what you made, not what it contains.';\n\nexport interface ArtifactToolOptions {\n /** The tool's name as the model sees it. Default `create_artifact`. */\n name?: string;\n /**\n * The system prompt the tool carries — how the model learns when to produce a\n * document. A string of your own replaces the default; `false` sends none.\n */\n systemPrompt?: string | false;\n}\n\n/** The tool definition — name, description, input schema, system prompt. */\nexport function createArtifactTool(options: ArtifactToolOptions = {}): AparteTool {\n const tool: AparteTool = {\n name: options.name ?? 'create_artifact',\n description: 'Create a self-contained document the user can keep, edit, run or download: a page, a '\n + 'component, a script, a stylesheet, an SVG, a JSON document, a Markdown note, a CSV. '\n + 'Give it a MIME type (text/html, application/vnd.ant.react, image/svg+xml, text/markdown, '\n + 'application/json, text/csv, application/javascript, text/css) and a short title.',\n inputSchema: {\n type: 'object',\n properties: {\n mimeType: { type: 'string', description: 'The MIME type of the document, e.g. text/html or application/vnd.ant.react.' },\n title: { type: 'string', description: 'A short human title for the document.' },\n content: { type: 'string', description: 'The complete document.' },\n },\n required: ['mimeType', 'content'],\n },\n };\n if (options.systemPrompt !== false) tool.systemPrompt = options.systemPrompt ?? ARTIFACT_SYSTEM_PROMPT;\n return tool;\n}\n\n/**\n * The handler: the document IS the result. The prose tells the model what happened;\n * the structured twin carries the artifact for the renderer, verbatim.\n */\nexport async function artifactHandler(call: AparteToolCall): Promise<AparteToolResult> {\n const input = (call.input ?? {}) as Partial<ArtifactInput>;\n const mimeType = typeof input.mimeType === 'string' && input.mimeType.trim() ? input.mimeType.trim() : 'text/plain';\n const title = typeof input.title === 'string' ? input.title.trim() : '';\n const content = typeof input.content === 'string' ? input.content : '';\n const structured: ArtifactInput = { mimeType, content, ...(title ? { title } : {}) };\n return {\n toolCallId: call.id,\n content: `Artifact created${title ? `: ${title}` : ''} (${mimeType}, ${content.length} characters). It is shown to the user; do not repeat its content.`,\n structuredContent: structured,\n };\n}\n","/**\n * Map an artifact MIME type to a renderer kind — `'react'`, `'html'`, `'js'`, `'css'`,\n * `'svg'`, `'json'`, `'markdown'`, `'csv'`, `'text'` — or `fallback` (default\n * `'unknown'`) for anything else. Anthropic's `application/vnd.ant.*` namespace maps to\n * its suffix; exact standard MIMEs are matched first, then a substring rescue for\n * parameterised or vendor variants (`text/html; charset=utf-8`, `application/ld+json`).\n *\n * THE implementation, and the plugin's: it used to live in `@aparte/engine` and be\n * re-exported by `@aparte/core`, because the engine's built-in `create_artifact` and\n * core's parser both needed it. Neither does any more — an artifact is a plugin's\n * convention, so the function that names its kinds is the plugin's too.\n *\n * @example\n * deriveArtifactKind('application/vnd.ant.react') // 'react'\n * deriveArtifactKind('text/html; charset=utf-8') // 'html'\n * deriveArtifactKind('font/woff2', 'text') // 'text' (fallback)\n */\nexport function deriveArtifactKind(mimeType: string, fallback = 'unknown'): string {\n const m = (mimeType || '').toLowerCase().trim();\n const ant = m.match(/^application\\/vnd\\.ant\\.([a-z0-9-]+)/);\n if (ant) return ant[1]!;\n if (m === 'text/html' || m === 'application/xhtml+xml') return 'html';\n if (m === 'application/javascript' || m === 'text/javascript') return 'js';\n if (m === 'text/css') return 'css';\n if (m === 'image/svg+xml') return 'svg';\n if (m === 'application/json') return 'json';\n if (m === 'text/markdown') return 'markdown';\n if (m === 'text/csv') return 'csv';\n if (m === 'text/plain') return 'text';\n // The binary kinds the file card knows, by their standard names — a model calling\n // the tool with a real MIME type must land on the same path as `vnd.ant.pdf`.\n if (m === 'application/pdf') return 'pdf';\n if (m === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') return 'xlsx';\n if (m === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') return 'docx';\n if (m.includes('react')) return 'react';\n if (m.includes('html')) return 'html';\n if (m.includes('javascript')) return 'js';\n if (m.includes('css')) return 'css';\n if (m.includes('svg')) return 'svg';\n if (m.includes('json')) return 'json';\n if (m.includes('csv')) return 'csv';\n if (m.includes('markdown')) return 'markdown';\n return fallback;\n}\n","/**\n * The artifact segment, and the two ways one is produced.\n *\n * An artifact is not something a model does by nature — it is a convention an app\n * teaches it, and there are exactly two ways the convention arrives in a stream:\n *\n * 1. **A tool call.** The app registers `create_artifact` (`./tool.ts`); the model\n * calls it with `{ mimeType, title, content }`; the call's structured result is\n * the artifact, and the tool renderer draws the card from it.\n * 2. **A tag in the prose.** A model with no tools, or one prompted that way, writes\n * `<artifact type=\"…\" title=\"…\">…</artifact>` inline; the block grammar below turns\n * it into a segment of this type, streamed delta by delta by core's parser.\n *\n * Both end in the same card, which is what makes them one feature and not two.\n */\nimport type { AparteSegmentBase, AparteStreamBlock, AparteToolCallSegment } from '@aparte/core';\nimport { deriveArtifactKind } from './kinds.js';\n\n/** Segment type discriminator. Registered by `setupArtifacts()`, not built into core. */\nexport const ARTIFACT_SEGMENT_TYPE = 'artifact' as const;\n\n/** The default tag the grammar recognises: `<artifact …>…</artifact>`. */\nexport const ARTIFACT_TAG = 'artifact';\n\n/**\n * An artifact — a document the model produced, embedded in its reply.\n *\n * `mimeType` follows standard MIME conventions, with Anthropic's vendor namespace for\n * framework-specific kinds (`application/vnd.ant.react`); `artifactType` is the short\n * kind derived from it (`'react'`, `'html'`, `'svg'`, `'pdf'`…), which is what the card\n * switches on. `content` accumulates while a tagged block streams and is complete at\n * once for a tool call.\n */\nexport interface ArtifactSegment extends AparteSegmentBase {\n type: typeof ARTIFACT_SEGMENT_TYPE;\n /** Standard MIME type, verbatim from the tool input or the tag's `type` / `mimeType` attribute. */\n mimeType: string;\n /** Short kind derived from `mimeType` — see {@link deriveArtifactKind}. */\n artifactType: string;\n /** Optional human title. */\n title?: string;\n /** The document. Accumulates while a tagged block streams. */\n content: string;\n}\n\n/**\n * What the model passes to the tool — and what the tool returns as its structured\n * result, so the card reads one shape on both paths.\n */\nexport interface ArtifactInput {\n mimeType: string;\n title?: string;\n content: string;\n}\n\n/** Build the segment for a document, from either path. */\nexport function artifactSegment(id: string, input: Partial<ArtifactInput>): ArtifactSegment {\n const mimeType = typeof input.mimeType === 'string' && input.mimeType.trim() ? input.mimeType.trim() : 'text/plain';\n return {\n id,\n type: ARTIFACT_SEGMENT_TYPE,\n mimeType,\n artifactType: deriveArtifactKind(mimeType),\n title: typeof input.title === 'string' && input.title.trim() ? input.title.trim() : undefined,\n content: typeof input.content === 'string' ? input.content : '',\n };\n}\n\n/**\n * The block grammar for `<artifact …>…</artifact>` in the prose.\n *\n * Both attribute spellings are read — `type=` (Anthropic's) and `mimeType=` — and\n * `mimeType` wins when both are present; `title=` is optional. Everything the parser\n * does with it (chunk boundaries, the partial closing tag, `finalize()`) is core's.\n */\nexport function artifactBlock(tag: string = ARTIFACT_TAG): AparteStreamBlock {\n return {\n tag,\n toSegment: ({ attrs, id }) => artifactSegment(id, {\n mimeType: attrs['mimeType'] ?? attrs['mimetype'] ?? attrs['type'],\n title: attrs['title'],\n content: '',\n }),\n };\n}\n\n/**\n * The artifact a tool-call segment carries: its structured result once the tool ran,\n * else the model's input while the call is pending — so the card can be drawn before\n * the handler returns, which is instantaneous anyway.\n */\nexport function artifactFromToolCall(segment: AparteToolCallSegment): ArtifactSegment {\n const structured = segment.structuredResult as Partial<ArtifactInput> | undefined;\n const source = structured && typeof structured === 'object' ? structured : (segment.toolCall?.input ?? {}) as Partial<ArtifactInput>;\n const art = artifactSegment(segment.id, source);\n // A call whose input is still arriving would stream; a tool call's input arrives\n // whole, so the card is never in its streaming state here.\n art.isStreaming = false;\n return art;\n}\n","/**\n * The artifact preview DOCUMENT — the security boundary of the artifact card.\n *\n * A previewable artifact is model-authored code rendered in a sandboxed iframe. What\n * that iframe is allowed to reach, and how the model's body is wrapped before it gets\n * there, is decided entirely here. Until now it decided it from line 1806 of a\n * 1900-line file, next to nine unrelated renderers.\n *\n * It is also a documented public override point:\n * `contextConfig().getArtifactPreviewBuilder() ?? buildSafePreviewDocument` means a\n * consumer can replace this whole document — so `buildSafePreviewDocument` is the\n * reference implementation their replacement is measured against, and it deserves to\n * be findable.\n *\n * Everything here is pure: `(kind, body, title) => string`. No config read, no DOM,\n * no state — which is why it can be tested directly and why moving it changes nothing.\n *\n * Two things travelled deliberately and must not be separated again:\n * • `PREVIEW_CSP` is used twice — as the iframe's `csp` attribute and as the\n * document's own `<meta http-equiv>`. Belt and braces: the attribute is not\n * honoured by Firefox or Safari, so the meta tag is the one that actually applies\n * there.\n * • `escapeClosingScriptTag` is listed by NAME in `scripts/escaping-names.mjs`, so\n * both escaping guards keep recognising it from its new home. Its `<style>`\n * sibling is not — a positional escaper is only ever right in one position, and\n * that one is declared at its use site instead.\n */\nimport { escapeHtml, escapeAttr } from '@aparte/core';\n\n/**\n * What a previewed artifact is allowed to FETCH. Inline script and style are\n * permitted (that IS the preview); no fetch, no XHR, no websocket, no remote image,\n * no font. Without this the sandbox still lets injected code beacon out to any\n * origin, so the policy does real work — measured blocked in Chromium, Firefox and\n * WebKit.\n *\n * WHAT IT DOES NOT STOP, stated because the sentence here used to claim otherwise:\n * the frame NAVIGATING ITSELF. Assigning `location.href` is a navigation, not a\n * fetch, and no directive in this policy governs it — CSP's `navigate-to` was\n * removed from the spec and never shipped, and the parent page's `frame-src` does\n * not apply to a navigation the frame initiates. Reproduced in all three engines;\n * in Firefox and WebKit the frame then RENDERS the attacker's page inside the card,\n * which is a phishing surface as well as an exfiltration channel.\n *\n * What still holds, and is why this is serious rather than fatal: the sandbox has\n * no `allow-same-origin`, so the frame's origin is opaque — it cannot read the host\n * page, its storage, or the API key — and no `allow-forms` or\n * `allow-top-navigation`, so it can neither POST nor move the tab.\n *\n * The honest summary for a consumer: a previewed artifact cannot reach your page or\n * your data, and can phone home once. Previewing model-authored HTML is running\n * untrusted content in a box, not running nothing.\n */\nexport const PREVIEW_CSP = \"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:\";\n\n/**\n * The portable half of the preview containment.\n *\n * The iframe's `csp` attribute is Chromium-only, so the same policy also goes\n * INSIDE the document as a `<meta http-equiv>`, which every engine honours. Both\n * are declared; whichever the browser understands applies.\n *\n * A model-authored full document gets one too, which it previously did not.\n *\n * This used to insert the meta only after an existing `<head>` and skip the\n * document otherwise, justified as \"a `<meta>` cannot be prepended without\n * breaking the doctype\". That is not true — `<html>` may be followed by a `<head>`\n * we open ourselves — and it left the ONE case that needs the policy most (a\n * `<!doctype html>` document written entirely by the model, which is passed\n * through verbatim) with no `<meta>` at all. On Firefox and Safari, where the\n * `csp` attribute does nothing, that frame ran `allow-scripts` under no policy: it\n * could beacon out, and load remote script. So a `<head>` is now created when the\n * document has none.\n *\n * Containment that held regardless: `sandbox=\"allow-scripts\"` without\n * `allow-same-origin` gives an opaque origin, so the frame never reached the host\n * DOM, storage, or the API key.\n */\nfunction withMetaCsp(doc: string): string { // safe-text: doc is the model-authored artifact HTML this function INJECTS a CSP into — escaping it would destroy the document it is protecting\n const meta = `<meta http-equiv=\"Content-Security-Policy\" content=\"${escapeAttr(PREVIEW_CSP)}\">`;\n\n // FIRST, always — after the doctype when there is one, otherwise at the very\n // front. Not \"after the model's `<head>`\", which is what this used to do.\n //\n // A `<meta http-equiv>` policy governs only what FOLLOWS it, and the model\n // writes this document: it can put a `<script>` before its own `<head>`, or\n // before `<html>`. The parser hoists that script into an implicitly-opened\n // head, where it sits EARLIER in document order than a meta inserted after a\n // tag the model declared later — so the script ran outside the policy.\n // Reproduced end to end in Firefox, WebKit, and Chromium with the `csp`\n // attribute absent: the beacon fired. And since that attribute is\n // Chromium-only, on Firefox and Safari this meta is the frame's ONLY policy.\n //\n // Prepending is safe in every shape, which the previously-correct third branch\n // already demonstrated across all three engines: content before `<html>` is\n // hoisted into an implicit head, the model's own `<html>`/`<head>` then merge\n // into the one already open. One branch replaces three, and it is the branch\n // that was right.\n const doctype = doc.match(/^\\s*<!doctype[^>]*>/i);\n const at = doctype ? doctype[0].length : 0;\n return `${doc.slice(0, at)}<head>${meta}</head>${doc.slice(at)}`;\n}\n\nexport function buildSafePreviewDocument(kind: string, body: string, title: string): string {\n return withMetaCsp(buildPreviewBody(kind, body, title));\n}\n\nfunction buildPreviewBody(\n kind: string,\n body: string, // safe-text: the model-authored artifact SOURCE, wrapped for a sandboxed CSP-constrained iframe — escaping it would render the code as text, which is the bug the sandbox exists to avoid\n title: string,\n): string {\n switch (kind) {\n case 'html': {\n if (startsWithDoctype(body)) return body;\n return `<!doctype html><html><head><meta charset=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/><title>${escapeAttr(title)}</title></head><body>${body}</body></html>`;\n }\n // An SVG with only a `viewBox` — the recommended, responsive form, and the one\n // a model writes most often — has NO intrinsic size. As a flex item its cross\n // size then collapses to zero and the frame is blank: the preview worked for an\n // SVG that declared `width`/`height` and silently showed nothing for the\n // idiomatic one. Reported from the landing the moment its demo payload became a\n // real file instead of a hand-written chart with dimensions on it.\n //\n // The rule is narrowed by attribute selector rather than applied to every SVG,\n // so an SVG that DOES state its size keeps the size it asked for.\n case 'svg':\n return `<!doctype html><html><head><meta charset=\"utf-8\"/><title>${escapeAttr(title)}</title>\n<style>html,body{margin:0;height:100%;display:flex;align-items:center;justify-content:center;background:#fff}svg{max-width:90%;max-height:90%}svg:not([width]):not([height]){width:90%;height:90%}</style>\n</head><body>${body}</body></html>`;\n case 'js': {\n const safeBody = escapeClosingScriptTag(body);\n return `<!doctype html><html><head><meta charset=\"utf-8\"/><title>${escapeAttr(title)}</title>\n<style>body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;padding:1rem;background:#fff;color:#0f172a}</style>\n</head><body><div id=\"root\"></div><script>\ntry { ${safeBody}\n} catch (e) { document.getElementById('root').innerHTML = '<pre style=\"color:#b91c1c\">' + (e && e.stack || e) + '</pre>'; }\n</script></body></html>`;\n }\n case 'css': {\n const safeSheet = escapeClosingStyleTag(body); // safe-text: the model's stylesheet with its `</style>` neutralised — HTML-escaping CSS would render the sheet as text, which is the bug the wrapper exists to avoid\n return `<!doctype html><html><head><meta charset=\"utf-8\"/><title>${escapeAttr(title)}</title>\n<style>${safeSheet}</style></head><body>\n<div class=\"demo\">\n <h1>Heading</h1>\n <p>Paragraph with a <a href=\"#\">link</a> and <strong>strong</strong> text.</p>\n <button>Button</button>\n <input placeholder=\"Input\"/>\n <ul><li>One</li><li>Two</li><li>Three</li></ul>\n</div></body></html>`;\n }\n default:\n // react / unknown — no live preview offline; show the code read-only.\n return `<!doctype html><html><head><meta charset=\"utf-8\"/><title>${escapeAttr(title)}</title>\n<style>body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;padding:1rem;background:#fff;color:#0f172a}pre{white-space:pre-wrap;word-break:break-word;margin:0}</style>\n</head><body><pre>${escapeHtml(body)}</pre></body></html>`;\n }\n}\n\n/** Char-based check for `<!doctype` (case-insensitive) at start of string,\n * ignoring leading whitespace. */\nfunction startsWithDoctype(s: string): boolean {\n let i = 0;\n while (i < s.length && (s[i] === ' ' || s[i] === '\\t' || s[i] === '\\n' || s[i] === '\\r')) i++;\n const probe = s.slice(i, i + 9).toLowerCase();\n return probe === '<!doctype';\n}\n\n/**\n * Escape every literal `</script` inside the body so it cannot terminate the outer\n * `<script>` we wrap the model's code in. Listed by NAME in\n * `scripts/escaping-names.mjs`, so both escaping guards recognise it.\n */\nfunction escapeClosingScriptTag(body: string): string {\n // `<\\/script` — neutralise the `<` so the browser sees no tag, and the sequence\n // is still a valid JS escape inside a string or a regex.\n return escapeClosingTag(body, 'script', '<\\\\/script');\n}\n\n/**\n * The same, for the `<style>` the `css` kind wraps a stylesheet in. NOT listed in\n * `scripts/escaping-names.mjs` — a positional escaper is blessed by name only where\n * it is right in EVERY position, and this one is right in exactly one; its single\n * use site carries a `// safe-text:` marker instead. That marker blesses an\n * identifier name for the whole file, which is why the local it names is\n * `safeSheet` and not the `safeBody` of the `js` case above.\n *\n * `<style>` is raw text exactly like `<script>`: the PARSER ends it on `</style` and\n * never asks the CSS tokenizer, so a stylesheet artifact could close its own wrapper\n * and open a `<script>` — on the one kind whose whole promise is that it only\n * styles. `\\3c` is CSS's own escape for `<`, so a `</style>` written inside a string\n * (`content: \"</style>\"`) still reads as one; anywhere else it was a parse error\n * before this ran and stays one after.\n */\nfunction escapeClosingStyleTag(body: string): string {\n return escapeClosingTag(body, 'style', '\\\\3c /style');\n}\n\n/**\n * Replace every `</tag` that would END a raw-text element with `escaped`.\n *\n * The HTML spec closes one on `</tag` followed by whitespace, `/`, `>` or\n * end-of-input — not only on the exact `</tag>`, which is the spelling a naive\n * `replaceAll('</style>')` would miss. Char-based, so no regex has to be built from\n * a tag name.\n */\nfunction escapeClosingTag(body: string, tag: string, escaped: string): string {\n const close = `</${tag}`;\n let out = '';\n let i = 0;\n while (i < body.length) {\n if (body[i] === '<' && body.slice(i, i + close.length).toLowerCase() === close) {\n const next = body[i + close.length];\n if (next === undefined || next === '/' || next === '>' || /\\s/.test(next)) {\n out += escaped;\n i += close.length;\n continue;\n }\n }\n out += body[i];\n i++;\n }\n return out;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"preview-document-CUKDtx2T.js","sources":["../src/tool.ts","../src/kinds.ts","../src/segment.ts","../src/preview-document.ts"],"sourcesContent":["/**\n * The `create_artifact` tool: a real `AparteTool` the app registers, with a handler that\n * returns the document as its structured result.\n *\n * It used to be a string compared in the engine (`event.name === 'create_artifact'`)\n * with no tool behind it: no app declared it to the model, no approval policy could\n * rule on it, and its result skipped the generic path. As a tool it goes through\n * everything a tool goes through — the schema the model sees, the approval gate (a\n * policy may well class writing a document as a `write`), the handler, the envelope —\n * and the card is a tool renderer on its result.\n */\nimport type { AparteTool, AparteToolCall, AparteToolResult } from '@aparte/core';\nimport type { ArtifactInput } from './segment.js';\n\n/** What the model is told, when the app does not say otherwise. */\nexport const ARTIFACT_SYSTEM_PROMPT =\n 'When the user asks for a document, a page, a component, a diagram or a file — anything '\n + 'they will keep, edit or run rather than read once — produce it with the create_artifact '\n + 'tool, with a MIME type that names what it is, rather than pasting it into your reply. '\n + 'Keep your reply short: say what you made, not what it contains.';\n\nexport interface ArtifactToolOptions {\n /** The tool's name as the model sees it. Default `create_artifact`. */\n name?: string;\n /**\n * The system prompt the tool carries — how the model learns when to produce a\n * document. A string of your own replaces the default; `false` sends none.\n */\n systemPrompt?: string | false;\n}\n\n/** The tool definition — name, description, input schema, system prompt. */\nexport function createArtifactTool(options: ArtifactToolOptions = {}): AparteTool {\n const tool: AparteTool = {\n name: options.name ?? 'create_artifact',\n description: 'Create a self-contained document the user can keep, edit, run or download: a page, a '\n + 'component, a script, a stylesheet, an SVG, a JSON document, a Markdown note, a CSV. '\n + 'Give it a MIME type (text/html, application/vnd.ant.react, image/svg+xml, text/markdown, '\n + 'application/json, text/csv, application/javascript, text/css) and a short title.',\n inputSchema: {\n type: 'object',\n properties: {\n mimeType: { type: 'string', description: 'The MIME type of the document, e.g. text/html or application/vnd.ant.react.' },\n title: { type: 'string', description: 'A short human title for the document.' },\n content: { type: 'string', description: 'The complete document.' },\n },\n required: ['mimeType', 'content'],\n },\n };\n if (options.systemPrompt !== false) tool.systemPrompt = options.systemPrompt ?? ARTIFACT_SYSTEM_PROMPT;\n return tool;\n}\n\n/**\n * The handler: the document IS the result. The prose tells the model what happened;\n * the structured twin carries the artifact for the renderer, verbatim.\n */\nexport async function artifactHandler(call: AparteToolCall): Promise<AparteToolResult> {\n const input = (call.input ?? {}) as Partial<ArtifactInput>;\n const mimeType = typeof input.mimeType === 'string' && input.mimeType.trim() ? input.mimeType.trim() : 'text/plain';\n const title = typeof input.title === 'string' ? input.title.trim() : '';\n const content = typeof input.content === 'string' ? input.content : '';\n const structured: ArtifactInput = { mimeType, content, ...(title ? { title } : {}) };\n return {\n toolCallId: call.id,\n content: `Artifact created${title ? `: ${title}` : ''} (${mimeType}, ${content.length} characters). It is shown to the user; do not repeat its content.`,\n structuredContent: structured,\n };\n}\n","/**\n * Map an artifact MIME type to a renderer kind — `'react'`, `'html'`, `'js'`, `'css'`,\n * `'svg'`, `'json'`, `'markdown'`, `'csv'`, `'text'` — or `fallback` (default\n * `'unknown'`) for anything else. Anthropic's `application/vnd.ant.*` namespace maps to\n * its suffix; exact standard MIMEs are matched first, then a substring rescue for\n * parameterised or vendor variants (`text/html; charset=utf-8`, `application/ld+json`).\n *\n * THE implementation, and the plugin's: it used to live in `@aparte/engine` and be\n * re-exported by `@aparte/core`, because the engine's built-in `create_artifact` and\n * core's parser both needed it. Neither does any more — an artifact is a plugin's\n * convention, so the function that names its kinds is the plugin's too.\n *\n * @example\n * deriveArtifactKind('application/vnd.ant.react') // 'react'\n * deriveArtifactKind('text/html; charset=utf-8') // 'html'\n * deriveArtifactKind('font/woff2', 'text') // 'text' (fallback)\n */\nexport function deriveArtifactKind(mimeType: string, fallback = 'unknown'): string {\n const m = (mimeType || '').toLowerCase().trim();\n const ant = m.match(/^application\\/vnd\\.ant\\.([a-z0-9-]+)/);\n if (ant) return ant[1]!;\n if (m === 'text/html' || m === 'application/xhtml+xml') return 'html';\n if (m === 'application/javascript' || m === 'text/javascript') return 'js';\n if (m === 'text/css') return 'css';\n if (m === 'image/svg+xml') return 'svg';\n if (m === 'application/json') return 'json';\n if (m === 'text/markdown') return 'markdown';\n if (m === 'text/csv') return 'csv';\n if (m === 'text/plain') return 'text';\n // The binary kinds the file card knows, by their standard names — a model calling\n // the tool with a real MIME type must land on the same path as `vnd.ant.pdf`.\n if (m === 'application/pdf') return 'pdf';\n if (m === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') return 'xlsx';\n if (m === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') return 'docx';\n if (m.includes('react')) return 'react';\n if (m.includes('html')) return 'html';\n if (m.includes('javascript')) return 'js';\n if (m.includes('css')) return 'css';\n if (m.includes('svg')) return 'svg';\n if (m.includes('json')) return 'json';\n if (m.includes('csv')) return 'csv';\n if (m.includes('markdown')) return 'markdown';\n return fallback;\n}\n","/**\n * The artifact segment, and the two ways one is produced.\n *\n * An artifact is not something a model does by nature — it is a convention an app\n * teaches it, and there are exactly two ways the convention arrives in a stream:\n *\n * 1. **A tool call.** The app registers `create_artifact` (`./tool.ts`); the model\n * calls it with `{ mimeType, title, content }`; the call's structured result is\n * the artifact, and the tool renderer draws the card from it.\n * 2. **A tag in the prose.** A model with no tools, or one prompted that way, writes\n * `<artifact type=\"…\" title=\"…\">…</artifact>` inline; the block grammar below turns\n * it into a segment of this type, streamed delta by delta by core's parser.\n *\n * Both end in the same card, which is what makes them one feature and not two.\n */\nimport type { AparteSegmentBase, AparteStreamBlock, AparteToolCallSegment } from '@aparte/core';\nimport { deriveArtifactKind } from './kinds.js';\n\n/** Segment type discriminator. Registered by `setupArtifacts()`, not built into core. */\nexport const ARTIFACT_SEGMENT_TYPE = 'artifact' as const;\n\n/** The default tag the grammar recognises: `<artifact …>…</artifact>`. */\nexport const ARTIFACT_TAG = 'artifact';\n\n/**\n * An artifact — a document the model produced, embedded in its reply.\n *\n * `mimeType` follows standard MIME conventions, with Anthropic's vendor namespace for\n * framework-specific kinds (`application/vnd.ant.react`); `artifactType` is the short\n * kind derived from it (`'react'`, `'html'`, `'svg'`, `'pdf'`…), which is what the card\n * switches on. `content` accumulates while a tagged block streams and is complete at\n * once for a tool call.\n */\nexport interface ArtifactSegment extends AparteSegmentBase {\n type: typeof ARTIFACT_SEGMENT_TYPE;\n /** Standard MIME type, verbatim from the tool input or the tag's `type` / `mimeType` attribute. */\n mimeType: string;\n /** Short kind derived from `mimeType` — see {@link deriveArtifactKind}. */\n artifactType: string;\n /** Optional human title. */\n title?: string;\n /** The document. Accumulates while a tagged block streams. */\n content: string;\n}\n\n/** The artifact joins core's segment union, so `addSegment(id, artifactSegment(…))` takes it as it is. */\ndeclare module '@aparte/core' {\n interface AparteSegmentTypes {\n artifact: ArtifactSegment;\n }\n}\n\n/**\n * What the model passes to the tool — and what the tool returns as its structured\n * result, so the card reads one shape on both paths.\n */\nexport interface ArtifactInput {\n mimeType: string;\n title?: string;\n content: string;\n}\n\n/** Build the segment for a document, from either path. */\nexport function artifactSegment(id: string, input: Partial<ArtifactInput>): ArtifactSegment {\n const mimeType = typeof input.mimeType === 'string' && input.mimeType.trim() ? input.mimeType.trim() : 'text/plain';\n return {\n id,\n type: ARTIFACT_SEGMENT_TYPE,\n mimeType,\n artifactType: deriveArtifactKind(mimeType),\n title: typeof input.title === 'string' && input.title.trim() ? input.title.trim() : undefined,\n content: typeof input.content === 'string' ? input.content : '',\n };\n}\n\n/**\n * The block grammar for `<artifact …>…</artifact>` in the prose.\n *\n * Both attribute spellings are read — `type=` (Anthropic's) and `mimeType=` — and\n * `mimeType` wins when both are present; `title=` is optional. Everything the parser\n * does with it (chunk boundaries, the partial closing tag, `finalize()`) is core's.\n */\nexport function artifactBlock(tag: string = ARTIFACT_TAG): AparteStreamBlock {\n return {\n tag,\n toSegment: ({ attrs, id }) => artifactSegment(id, {\n mimeType: attrs['mimeType'] ?? attrs['mimetype'] ?? attrs['type'],\n title: attrs['title'],\n content: '',\n }),\n };\n}\n\n/**\n * The artifact a tool-call segment carries: its structured result once the tool ran,\n * else the model's input while the call is pending — so the card can be drawn before\n * the handler returns, which is instantaneous anyway.\n */\nexport function artifactFromToolCall(segment: AparteToolCallSegment): ArtifactSegment {\n const structured = segment.structuredResult as Partial<ArtifactInput> | undefined;\n const source = structured && typeof structured === 'object' ? structured : (segment.toolCall?.input ?? {}) as Partial<ArtifactInput>;\n const art = artifactSegment(segment.id, source);\n // A call whose input is still arriving would stream; a tool call's input arrives\n // whole, so the card is never in its streaming state here.\n art.isStreaming = false;\n return art;\n}\n","/**\n * The artifact preview DOCUMENT — the security boundary of the artifact card.\n *\n * A previewable artifact is model-authored code rendered in a sandboxed iframe. What\n * that iframe is allowed to reach, and how the model's body is wrapped before it gets\n * there, is decided entirely here. Until now it decided it from line 1806 of a\n * 1900-line file, next to nine unrelated renderers.\n *\n * It is also a documented public override point:\n * `contextConfig().getArtifactPreviewBuilder() ?? buildSafePreviewDocument` means a\n * consumer can replace this whole document — so `buildSafePreviewDocument` is the\n * reference implementation their replacement is measured against, and it deserves to\n * be findable.\n *\n * Everything here is pure: `(kind, body, title) => string`. No config read, no DOM,\n * no state — which is why it can be tested directly and why moving it changes nothing.\n *\n * Two things travelled deliberately and must not be separated again:\n * • `PREVIEW_CSP` is used twice — as the iframe's `csp` attribute and as the\n * document's own `<meta http-equiv>`. Belt and braces: the attribute is not\n * honoured by Firefox or Safari, so the meta tag is the one that actually applies\n * there.\n * • `escapeClosingScriptTag` is listed by NAME in `scripts/escaping-names.mjs`, so\n * both escaping guards keep recognising it from its new home. Its `<style>`\n * sibling is not — a positional escaper is only ever right in one position, and\n * that one is declared at its use site instead.\n */\nimport { escapeHtml, escapeAttr } from '@aparte/core';\n\n/**\n * What a previewed artifact is allowed to FETCH. Inline script and style are\n * permitted (that IS the preview); no fetch, no XHR, no websocket, no remote image,\n * no font. Without this the sandbox still lets injected code beacon out to any\n * origin, so the policy does real work — measured blocked in Chromium, Firefox and\n * WebKit.\n *\n * WHAT IT DOES NOT STOP, stated because the sentence here used to claim otherwise:\n * the frame NAVIGATING ITSELF. Assigning `location.href` is a navigation, not a\n * fetch, and no directive in this policy governs it — CSP's `navigate-to` was\n * removed from the spec and never shipped, and the parent page's `frame-src` does\n * not apply to a navigation the frame initiates. Reproduced in all three engines;\n * in Firefox and WebKit the frame then RENDERS the attacker's page inside the card,\n * which is a phishing surface as well as an exfiltration channel.\n *\n * What still holds, and is why this is serious rather than fatal: the sandbox has\n * no `allow-same-origin`, so the frame's origin is opaque — it cannot read the host\n * page, its storage, or the API key — and no `allow-forms` or\n * `allow-top-navigation`, so it can neither POST nor move the tab.\n *\n * The honest summary for a consumer: a previewed artifact cannot reach your page or\n * your data, and can phone home once. Previewing model-authored HTML is running\n * untrusted content in a box, not running nothing.\n */\nexport const PREVIEW_CSP = \"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:\";\n\n/**\n * The portable half of the preview containment.\n *\n * The iframe's `csp` attribute is Chromium-only, so the same policy also goes\n * INSIDE the document as a `<meta http-equiv>`, which every engine honours. Both\n * are declared; whichever the browser understands applies.\n *\n * A model-authored full document gets one too, which it previously did not.\n *\n * This used to insert the meta only after an existing `<head>` and skip the\n * document otherwise, justified as \"a `<meta>` cannot be prepended without\n * breaking the doctype\". That is not true — `<html>` may be followed by a `<head>`\n * we open ourselves — and it left the ONE case that needs the policy most (a\n * `<!doctype html>` document written entirely by the model, which is passed\n * through verbatim) with no `<meta>` at all. On Firefox and Safari, where the\n * `csp` attribute does nothing, that frame ran `allow-scripts` under no policy: it\n * could beacon out, and load remote script. So a `<head>` is now created when the\n * document has none.\n *\n * Containment that held regardless: `sandbox=\"allow-scripts\"` without\n * `allow-same-origin` gives an opaque origin, so the frame never reached the host\n * DOM, storage, or the API key.\n */\nfunction withMetaCsp(doc: string): string { // safe-text: doc is the model-authored artifact HTML this function INJECTS a CSP into — escaping it would destroy the document it is protecting\n const meta = `<meta http-equiv=\"Content-Security-Policy\" content=\"${escapeAttr(PREVIEW_CSP)}\">`;\n\n // FIRST, always — after the doctype when there is one, otherwise at the very\n // front. Not \"after the model's `<head>`\", which is what this used to do.\n //\n // A `<meta http-equiv>` policy governs only what FOLLOWS it, and the model\n // writes this document: it can put a `<script>` before its own `<head>`, or\n // before `<html>`. The parser hoists that script into an implicitly-opened\n // head, where it sits EARLIER in document order than a meta inserted after a\n // tag the model declared later — so the script ran outside the policy.\n // Reproduced end to end in Firefox, WebKit, and Chromium with the `csp`\n // attribute absent: the beacon fired. And since that attribute is\n // Chromium-only, on Firefox and Safari this meta is the frame's ONLY policy.\n //\n // Prepending is safe in every shape, which the previously-correct third branch\n // already demonstrated across all three engines: content before `<html>` is\n // hoisted into an implicit head, the model's own `<html>`/`<head>` then merge\n // into the one already open. One branch replaces three, and it is the branch\n // that was right.\n const doctype = doc.match(/^\\s*<!doctype[^>]*>/i);\n const at = doctype ? doctype[0].length : 0;\n return `${doc.slice(0, at)}<head>${meta}</head>${doc.slice(at)}`;\n}\n\nexport function buildSafePreviewDocument(kind: string, body: string, title: string): string {\n return withMetaCsp(buildPreviewBody(kind, body, title));\n}\n\nfunction buildPreviewBody(\n kind: string,\n body: string, // safe-text: the model-authored artifact SOURCE, wrapped for a sandboxed CSP-constrained iframe — escaping it would render the code as text, which is the bug the sandbox exists to avoid\n title: string,\n): string {\n switch (kind) {\n case 'html': {\n if (startsWithDoctype(body)) return body;\n return `<!doctype html><html><head><meta charset=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/><title>${escapeAttr(title)}</title></head><body>${body}</body></html>`;\n }\n // An SVG with only a `viewBox` — the recommended, responsive form, and the one\n // a model writes most often — has NO intrinsic size. As a flex item its cross\n // size then collapses to zero and the frame is blank: the preview worked for an\n // SVG that declared `width`/`height` and silently showed nothing for the\n // idiomatic one. Reported from the landing the moment its demo payload became a\n // real file instead of a hand-written chart with dimensions on it.\n //\n // The rule is narrowed by attribute selector rather than applied to every SVG,\n // so an SVG that DOES state its size keeps the size it asked for.\n case 'svg':\n return `<!doctype html><html><head><meta charset=\"utf-8\"/><title>${escapeAttr(title)}</title>\n<style>html,body{margin:0;height:100%;display:flex;align-items:center;justify-content:center;background:#fff}svg{max-width:90%;max-height:90%}svg:not([width]):not([height]){width:90%;height:90%}</style>\n</head><body>${body}</body></html>`;\n case 'js': {\n const safeBody = escapeClosingScriptTag(body);\n return `<!doctype html><html><head><meta charset=\"utf-8\"/><title>${escapeAttr(title)}</title>\n<style>body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;padding:1rem;background:#fff;color:#0f172a}</style>\n</head><body><div id=\"root\"></div><script>\ntry { ${safeBody}\n} catch (e) { document.getElementById('root').innerHTML = '<pre style=\"color:#b91c1c\">' + (e && e.stack || e) + '</pre>'; }\n</script></body></html>`;\n }\n case 'css': {\n const safeSheet = escapeClosingStyleTag(body); // safe-text: the model's stylesheet with its `</style>` neutralised — HTML-escaping CSS would render the sheet as text, which is the bug the wrapper exists to avoid\n return `<!doctype html><html><head><meta charset=\"utf-8\"/><title>${escapeAttr(title)}</title>\n<style>${safeSheet}</style></head><body>\n<div class=\"demo\">\n <h1>Heading</h1>\n <p>Paragraph with a <a href=\"#\">link</a> and <strong>strong</strong> text.</p>\n <button>Button</button>\n <input placeholder=\"Input\"/>\n <ul><li>One</li><li>Two</li><li>Three</li></ul>\n</div></body></html>`;\n }\n default:\n // react / unknown — no live preview offline; show the code read-only.\n return `<!doctype html><html><head><meta charset=\"utf-8\"/><title>${escapeAttr(title)}</title>\n<style>body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;padding:1rem;background:#fff;color:#0f172a}pre{white-space:pre-wrap;word-break:break-word;margin:0}</style>\n</head><body><pre>${escapeHtml(body)}</pre></body></html>`;\n }\n}\n\n/** Char-based check for `<!doctype` (case-insensitive) at start of string,\n * ignoring leading whitespace. */\nfunction startsWithDoctype(s: string): boolean {\n let i = 0;\n while (i < s.length && (s[i] === ' ' || s[i] === '\\t' || s[i] === '\\n' || s[i] === '\\r')) i++;\n const probe = s.slice(i, i + 9).toLowerCase();\n return probe === '<!doctype';\n}\n\n/**\n * Escape every literal `</script` inside the body so it cannot terminate the outer\n * `<script>` we wrap the model's code in. Listed by NAME in\n * `scripts/escaping-names.mjs`, so both escaping guards recognise it.\n */\nfunction escapeClosingScriptTag(body: string): string {\n // `<\\/script` — neutralise the `<` so the browser sees no tag, and the sequence\n // is still a valid JS escape inside a string or a regex.\n return escapeClosingTag(body, 'script', '<\\\\/script');\n}\n\n/**\n * The same, for the `<style>` the `css` kind wraps a stylesheet in. NOT listed in\n * `scripts/escaping-names.mjs` — a positional escaper is blessed by name only where\n * it is right in EVERY position, and this one is right in exactly one; its single\n * use site carries a `// safe-text:` marker instead. That marker blesses an\n * identifier name for the whole file, which is why the local it names is\n * `safeSheet` and not the `safeBody` of the `js` case above.\n *\n * `<style>` is raw text exactly like `<script>`: the PARSER ends it on `</style` and\n * never asks the CSS tokenizer, so a stylesheet artifact could close its own wrapper\n * and open a `<script>` — on the one kind whose whole promise is that it only\n * styles. `\\3c` is CSS's own escape for `<`, so a `</style>` written inside a string\n * (`content: \"</style>\"`) still reads as one; anywhere else it was a parse error\n * before this ran and stays one after.\n */\nfunction escapeClosingStyleTag(body: string): string {\n return escapeClosingTag(body, 'style', '\\\\3c /style');\n}\n\n/**\n * Replace every `</tag` that would END a raw-text element with `escaped`.\n *\n * The HTML spec closes one on `</tag` followed by whitespace, `/`, `>` or\n * end-of-input — not only on the exact `</tag>`, which is the spelling a naive\n * `replaceAll('</style>')` would miss. Char-based, so no regex has to be built from\n * a tag name.\n */\nfunction escapeClosingTag(body: string, tag: string, escaped: string): string {\n const close = `</${tag}`;\n let out = '';\n let i = 0;\n while (i < body.length) {\n if (body[i] === '<' && body.slice(i, i + close.length).toLowerCase() === close) {\n const next = body[i + close.length];\n if (next === undefined || next === '/' || next === '>' || /\\s/.test(next)) {\n out += escaped;\n i += close.length;\n continue;\n }\n }\n out += body[i];\n i++;\n }\n return out;\n}\n"],"names":[],"mappings":"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"}
|
package/dist/segment.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export interface ArtifactSegment extends AparteSegmentBase {
|
|
|
38
38
|
/** The document. Accumulates while a tagged block streams. */
|
|
39
39
|
content: string;
|
|
40
40
|
}
|
|
41
|
+
/** The artifact joins core's segment union, so `addSegment(id, artifactSegment(…))` takes it as it is. */
|
|
42
|
+
declare module '@aparte/core' {
|
|
43
|
+
interface AparteSegmentTypes {
|
|
44
|
+
artifact: ArtifactSegment;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
41
47
|
/**
|
|
42
48
|
* What the model passes to the tool — and what the tool returns as its structured
|
|
43
49
|
* result, so the card reads one shape on both paths.
|
package/dist/segment.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segment.d.ts","sourceRoot":"","sources":["../src/segment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAGhG,yFAAyF;AACzF,eAAO,MAAM,qBAAqB,EAAG,UAAmB,CAAC;AAEzD,0EAA0E;AAC1E,eAAO,MAAM,YAAY,aAAa,CAAC;AAEvC;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACtD,IAAI,EAAE,OAAO,qBAAqB,CAAC;IACnC,mGAAmG;IACnG,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAC;IACrB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,eAAe,CAU1F;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,GAAE,MAAqB,GAAG,iBAAiB,CAS3E;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,eAAe,CAQpF"}
|
|
1
|
+
{"version":3,"file":"segment.d.ts","sourceRoot":"","sources":["../src/segment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAGhG,yFAAyF;AACzF,eAAO,MAAM,qBAAqB,EAAG,UAAmB,CAAC;AAEzD,0EAA0E;AAC1E,eAAO,MAAM,YAAY,aAAa,CAAC;AAEvC;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACtD,IAAI,EAAE,OAAO,qBAAqB,CAAC;IACnC,mGAAmG;IACnG,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAC;IACrB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,0GAA0G;AAC1G,OAAO,QAAQ,cAAc,CAAC;IAC1B,UAAU,kBAAkB;QACxB,QAAQ,EAAE,eAAe,CAAC;KAC7B;CACJ;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,eAAe,CAU1F;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,GAAE,MAAqB,GAAG,iBAAiB,CAS3E;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,eAAe,CAQpF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aparte/plugin-artifacts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"homepage": "https://apartejs.dev/plugins/artifacts/",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -19,6 +19,26 @@
|
|
|
19
19
|
"types": "./dist/index.node.d.ts",
|
|
20
20
|
"default": "./dist/index.node.js"
|
|
21
21
|
},
|
|
22
|
+
"worker": {
|
|
23
|
+
"types": "./dist/index.node.d.ts",
|
|
24
|
+
"default": "./dist/index.node.js"
|
|
25
|
+
},
|
|
26
|
+
"workerd": {
|
|
27
|
+
"types": "./dist/index.node.d.ts",
|
|
28
|
+
"default": "./dist/index.node.js"
|
|
29
|
+
},
|
|
30
|
+
"edge-light": {
|
|
31
|
+
"types": "./dist/index.node.d.ts",
|
|
32
|
+
"default": "./dist/index.node.js"
|
|
33
|
+
},
|
|
34
|
+
"deno": {
|
|
35
|
+
"types": "./dist/index.node.d.ts",
|
|
36
|
+
"default": "./dist/index.node.js"
|
|
37
|
+
},
|
|
38
|
+
"bun": {
|
|
39
|
+
"types": "./dist/index.node.d.ts",
|
|
40
|
+
"default": "./dist/index.node.js"
|
|
41
|
+
},
|
|
22
42
|
"types": "./dist/index.d.ts",
|
|
23
43
|
"import": "./dist/index.js",
|
|
24
44
|
"default": "./dist/index.js"
|
|
@@ -33,13 +53,13 @@
|
|
|
33
53
|
"node": ">=18"
|
|
34
54
|
},
|
|
35
55
|
"peerDependencies": {
|
|
36
|
-
"@aparte/core": ">=0.
|
|
56
|
+
"@aparte/core": ">=0.20.0 <1.0.0"
|
|
37
57
|
},
|
|
38
58
|
"devDependencies": {
|
|
39
59
|
"@custom-elements-manifest/analyzer": "^0.11.0",
|
|
40
60
|
"typescript": "^5.4.0",
|
|
41
61
|
"vite": "^6.0.0",
|
|
42
|
-
"@aparte/core": "0.
|
|
62
|
+
"@aparte/core": "0.20.0"
|
|
43
63
|
},
|
|
44
64
|
"keywords": [
|
|
45
65
|
"aparte",
|
|
@@ -61,7 +81,7 @@
|
|
|
61
81
|
},
|
|
62
82
|
"scripts": {
|
|
63
83
|
"dev": "vite",
|
|
64
|
-
"build": "vite build && tsc -b --emitDeclarationOnly --force && pnpm run analyze",
|
|
84
|
+
"build": "vite build && tsc -b --emitDeclarationOnly --force && pnpm run analyze && node ../../../scripts/stamp-dist.mjs",
|
|
65
85
|
"analyze": "cem analyze",
|
|
66
86
|
"preview": "vite preview",
|
|
67
87
|
"test": "vitest",
|