@aparte/core 0.9.0 → 0.10.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.
Files changed (70) hide show
  1. package/dist/components/bubble/aparte-chat-bubble.d.ts +24 -3
  2. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  3. package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
  4. package/dist/components/composer/aparte-composer-add-attachment.d.ts +11 -0
  5. package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
  6. package/dist/components/composer/aparte-composer-cancel.d.ts +8 -0
  7. package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
  8. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  9. package/dist/components/composer/aparte-composer-send.d.ts +32 -0
  10. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  11. package/dist/components/elicitation/aparte-elicitation.d.ts +10 -0
  12. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  13. package/dist/components/viewport/aparte-chat-viewport.d.ts +3 -1
  14. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  15. package/dist/config/aparte-config.d.ts +34 -1
  16. package/dist/config/aparte-config.d.ts.map +1 -1
  17. package/dist/config/config-subscribe.d.ts +4 -0
  18. package/dist/config/config-subscribe.d.ts.map +1 -0
  19. package/dist/config/index.d.ts +1 -0
  20. package/dist/config/index.d.ts.map +1 -1
  21. package/dist/config/locale.d.ts +98 -0
  22. package/dist/config/locale.d.ts.map +1 -1
  23. package/dist/config/sanitize.d.ts.map +1 -1
  24. package/dist/custom-elements.json +4588 -4293
  25. package/dist/elicitation/panel.d.ts +13 -0
  26. package/dist/elicitation/panel.d.ts.map +1 -1
  27. package/dist/host/aparte-chat-host.d.ts +3 -1
  28. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  29. package/dist/{index-hQQljwfX.js → index-BnKBciDG.js} +515 -232
  30. package/dist/index-BnKBciDG.js.map +1 -0
  31. package/dist/index.css +147 -310
  32. package/dist/index.d.ts +3 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +248 -87
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.node.d.ts +3 -2
  37. package/dist/index.node.d.ts.map +1 -1
  38. package/dist/index.node.js +60 -57
  39. package/dist/index.node.js.map +1 -1
  40. package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
  41. package/dist/renderers/highlight-stream.d.ts +17 -0
  42. package/dist/renderers/highlight-stream.d.ts.map +1 -0
  43. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  44. package/dist/renderers/segments/artifact/binary-file.d.ts.map +1 -1
  45. package/dist/renderers/segments/artifact/card.d.ts.map +1 -1
  46. package/dist/renderers/segments/artifact/shared.d.ts +19 -2
  47. package/dist/renderers/segments/artifact/shared.d.ts.map +1 -1
  48. package/dist/renderers/segments/code.d.ts.map +1 -1
  49. package/dist/renderers/segments/error.d.ts.map +1 -1
  50. package/dist/renderers/segments/pipeline-waiting.d.ts.map +1 -1
  51. package/dist/renderers/segments/thinking.d.ts.map +1 -1
  52. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  53. package/dist/types/event-map.d.ts +1 -2
  54. package/dist/types/event-map.d.ts.map +1 -1
  55. package/dist/types/events.d.ts +0 -22
  56. package/dist/types/events.d.ts.map +1 -1
  57. package/dist/types/index.d.ts +2 -2
  58. package/dist/types/index.d.ts.map +1 -1
  59. package/dist/types/models.d.ts +1 -6
  60. package/dist/types/models.d.ts.map +1 -1
  61. package/dist/types/segments.d.ts +87 -67
  62. package/dist/types/segments.d.ts.map +1 -1
  63. package/dist/utils/segments.d.ts +75 -5
  64. package/dist/utils/segments.d.ts.map +1 -1
  65. package/package.json +1 -1
  66. package/dist/index-hQQljwfX.js.map +0 -1
  67. package/dist/renderers/segments/file-tree.d.ts +0 -3
  68. package/dist/renderers/segments/file-tree.d.ts.map +0 -1
  69. package/dist/renderers/segments/terminal.d.ts +0 -3
  70. package/dist/renderers/segments/terminal.d.ts.map +0 -1
@@ -69,11 +69,21 @@ const APARTE_DEFAULT_LOCALE = {
69
69
  elicitationOtherPlaceholder: "Type your answer…",
70
70
  elicitationOtherLabel: "Custom answer",
71
71
  submitButton: "Submit",
72
+ stopButton: "Stop",
72
73
  elicitationNext: "Next",
73
74
  elicitationSkip: "Skip",
74
75
  elicitationYes: "Yes",
75
76
  elicitationNo: "No",
76
77
  elicitationAnswerLabel: "Your answer",
78
+ actionUpload: "Attach file",
79
+ download: "Download",
80
+ previewPending: "Press Preview to run this artifact.",
81
+ sandboxError: "The sandbox failed during generation.",
82
+ sandboxErrorHint: "Common cause: the model produced invalid code (undefined variable, wrong argument type). Retry the request — the model may produce different code.",
83
+ preview: "Preview",
84
+ code: "Code",
85
+ generating: "Generating…",
86
+ rebuildingPreview: "Rebuilding preview…",
77
87
  typing: "Typing...",
78
88
  error: "Error",
79
89
  running: "Running...",
@@ -551,7 +561,6 @@ class AparteStreamParser {
551
561
  id: this._options.autoGenerateIds ? this._generateId() : "",
552
562
  type: "thinking",
553
563
  content,
554
- collapsed: false,
555
564
  label: "Thinking"
556
565
  };
557
566
  }
@@ -939,6 +948,9 @@ const SAFE_STYLE_PROPS = /* @__PURE__ */ new Set([
939
948
  "text-decoration-style",
940
949
  "white-space"
941
950
  ]);
951
+ function isSafeCustomProperty(prop) {
952
+ return prop.startsWith("--") && !prop.startsWith("--aparte-");
953
+ }
942
954
  function scrubStyle(value) {
943
955
  const kept = [];
944
956
  for (const decl of value.split(";")) {
@@ -947,7 +959,8 @@ function scrubStyle(value) {
947
959
  if (idx === -1) continue;
948
960
  const prop = trimmed.slice(0, idx).trim().toLowerCase();
949
961
  const val = trimmed.slice(idx + 1);
950
- if (!val.trim() || !SAFE_STYLE_PROPS.has(prop)) continue;
962
+ if (!val.trim()) continue;
963
+ if (!SAFE_STYLE_PROPS.has(prop) && !isSafeCustomProperty(prop)) continue;
951
964
  if (val.includes("\\")) continue;
952
965
  if (/url\s*\(|expression\s*\(|javascript:|vbscript:|[<>]/i.test(val)) continue;
953
966
  kept.push(trimmed);
@@ -1034,7 +1047,6 @@ const APARTE_DEFAULT_BUBBLE_ACTIONS = {
1034
1047
  };
1035
1048
  const APARTE_DEFAULT_HOST_HANDLERS = {
1036
1049
  attachmentPreview: false,
1037
- terminalRun: false,
1038
1050
  artifactRedownload: false,
1039
1051
  artifactRehydrate: false
1040
1052
  };
@@ -1097,6 +1109,7 @@ class AparteConfig {
1097
1109
  // Tool Registry
1098
1110
  _tools = /* @__PURE__ */ new Map();
1099
1111
  _toolRenderers = /* @__PURE__ */ new Map();
1112
+ _segmentDefaults = /* @__PURE__ */ new Map();
1100
1113
  // Host handlers — what the app declares it can actually complete.
1101
1114
  _hostHandlers = { ...APARTE_DEFAULT_HOST_HANDLERS };
1102
1115
  // Bubble Actions — APARTE_DEFAULT_BUBBLE_ACTIONS is the single source of truth
@@ -1177,7 +1190,6 @@ class AparteConfig {
1177
1190
  getHostHandlers() {
1178
1191
  return {
1179
1192
  attachmentPreview: this._hostHandlers.attachmentPreview ?? APARTE_DEFAULT_HOST_HANDLERS.attachmentPreview,
1180
- terminalRun: this._hostHandlers.terminalRun ?? APARTE_DEFAULT_HOST_HANDLERS.terminalRun,
1181
1193
  artifactRedownload: this._hostHandlers.artifactRedownload ?? APARTE_DEFAULT_HOST_HANDLERS.artifactRedownload,
1182
1194
  artifactRehydrate: this._hostHandlers.artifactRehydrate ?? APARTE_DEFAULT_HOST_HANDLERS.artifactRehydrate
1183
1195
  };
@@ -1804,6 +1816,44 @@ class AparteConfig {
1804
1816
  registerToolRenderer(toolName, renderer) {
1805
1817
  this._toolRenderers.set(toolName, renderer);
1806
1818
  }
1819
+ /**
1820
+ * Field defaults for a segment TYPE, filled in when a segment enters a message.
1821
+ *
1822
+ * One function for every type rather than one per field: a `setThinkingOpen()`
1823
+ * would need a sibling the next time any type wanted a default, and the type key
1824
+ * is a string, so a consumer's own segment type is covered by the same call.
1825
+ *
1826
+ * The problem it solves: a consumer streaming a reply does not CONSTRUCT its
1827
+ * segments — the parser does — so there was no hook at all for "reasoning blocks
1828
+ * open in my app". Per-segment fields only reach segments you build yourself.
1829
+ *
1830
+ * Applied where identity is stamped (`utils/segments.ts`), so it covers every
1831
+ * path a segment can arrive by — the parser, `addSegment`, and the segments
1832
+ * seeded on an `appendMessage` — and no renderer has to look anything up.
1833
+ *
1834
+ * A field the producer set always wins, and identity is never defaulted. Read at
1835
+ * insertion and baked in: changing a default later does not reach segments already
1836
+ * on screen, because a block the reader opened has state the data does not.
1837
+ *
1838
+ * @example
1839
+ * // Reasoning blocks arrive open in this app, streaming or settled.
1840
+ * aparteGlobalConfig.setSegmentDefaults('thinking', { collapsed: false });
1841
+ *
1842
+ * @example
1843
+ * // A consumer's own type, same call.
1844
+ * aparteGlobalConfig.setSegmentDefaults('my-chart', { theme: 'dark' });
1845
+ */
1846
+ setSegmentDefaults(type, defaults) {
1847
+ this._segmentDefaults.set(type, { ...defaults });
1848
+ }
1849
+ /** The defaults registered for a type, or undefined. */
1850
+ getSegmentDefaults(type) {
1851
+ return this._segmentDefaults.get(type);
1852
+ }
1853
+ /** Drop the defaults for a type. */
1854
+ clearSegmentDefaults(type) {
1855
+ this._segmentDefaults.delete(type);
1856
+ }
1807
1857
  /** Unregister a per-tool renderer */
1808
1858
  unregisterToolRenderer(toolName) {
1809
1859
  this._toolRenderers.delete(toolName);
@@ -1888,6 +1938,7 @@ class AparteConfig {
1888
1938
  this._siblingNavRenderer = void 0;
1889
1939
  this._bubbleShellRenderer = void 0;
1890
1940
  this._iconProvider = void 0;
1941
+ this._segmentDefaults.clear();
1891
1942
  this._avatarProvider = void 0;
1892
1943
  this._artifactPreviewBuilder = void 0;
1893
1944
  this._keyProvider = void 0;
@@ -1983,6 +2034,16 @@ function runWithConfig(config, fn) {
1983
2034
  function contextConfig(el2) {
1984
2035
  return _renderConfig ?? resolveConfig(el2);
1985
2036
  }
2037
+ const APARTE_CONFIG_CHANGE = "aparte-config-change";
2038
+ function subscribeConfigChange(el2, handler) {
2039
+ const listener = (e) => {
2040
+ const detail = e.detail;
2041
+ if (detail?.config && detail.config !== resolveConfig(el2)) return;
2042
+ handler();
2043
+ };
2044
+ window.addEventListener(APARTE_CONFIG_CHANGE, listener);
2045
+ return () => window.removeEventListener(APARTE_CONFIG_CHANGE, listener);
2046
+ }
1986
2047
  const APARTE_DEFAULT_SKELETON_FALLBACKS = {
1987
2048
  message: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading...</div>',
1988
2049
  code: '<div class="aparte-skeleton-fallback" style="padding:16px;background:#1e293b;color:#64748b;border-radius:8px;">Loading code...</div>',
@@ -2063,7 +2124,15 @@ const thinkingRenderer = {
2063
2124
  * new class of risk. With no Markdown provider registered it degrades to the
2064
2125
  * zero-dependency default (escape + `<br>`), which is what it used to be.
2065
2126
  */
2066
- render: (segment) => `<details class="segment segment-thinking" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed ? "" : "open"}><summary class="thinking-header"><span class="thinking-label">${escapeHtml(segment.label || contextConfig().t("thinking"))}</span><span class="thinking-toggle"></span></summary><div class="thinking-content">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
2127
+ render: (segment) => `<details class="segment segment-thinking" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed === false ? "open" : ""}><summary class="thinking-header"><span class="thinking-label">${escapeHtml(segment.label || contextConfig().t("thinking"))}</span><span class="thinking-toggle"></span></summary><div class="thinking-content">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
2128
+ /**
2129
+ * The default label is the only config-derived text here — and `segment.label`
2130
+ * still wins, exactly as in `render`, because that string is the app's.
2131
+ */
2132
+ relabel: (el2, segment) => {
2133
+ const label = el2.querySelector(".thinking-label");
2134
+ if (label) label.textContent = segment.label || contextConfig().t("thinking");
2135
+ },
2067
2136
  update: (el2, segment) => {
2068
2137
  const contentEl = el2.querySelector(".thinking-content");
2069
2138
  if (!contentEl) return;
@@ -2075,6 +2144,53 @@ const thinkingRenderer = {
2075
2144
  },
2076
2145
  getStyles: () => ``
2077
2146
  };
2147
+ const HIGHLIGHT_DEBOUNCE_MS = 400;
2148
+ const MAX_THROTTLE_ENTRIES = 256;
2149
+ const _lastHighlightAt = /* @__PURE__ */ new Map();
2150
+ function markThrottle(map, id, at) {
2151
+ map.delete(id);
2152
+ if (map.size >= MAX_THROTTLE_ENTRIES) {
2153
+ const oldest = map.keys().next().value;
2154
+ if (oldest !== void 0) map.delete(oldest);
2155
+ }
2156
+ map.set(id, at);
2157
+ }
2158
+ function highlightedLen(pane) {
2159
+ const n = Number(pane.dataset.aparteHlLen);
2160
+ return Number.isFinite(n) && n > 0 ? n : 0;
2161
+ }
2162
+ function streamHighlight(element, paneSelector, content, lang, segId) {
2163
+ const pane = element.querySelector(paneSelector);
2164
+ if (!pane) return;
2165
+ const tail = pane.querySelector("[data-aparte-tail]");
2166
+ const hlLen = highlightedLen(pane);
2167
+ if (tail && hlLen <= content.length) {
2168
+ tail.textContent = content.slice(hlLen);
2169
+ } else {
2170
+ delete pane.dataset.aparteHlLen;
2171
+ const codeEl = pane.querySelector("code");
2172
+ if (codeEl) codeEl.textContent = content;
2173
+ else pane.innerHTML = `<pre><code class="language-${escapeHtml(lang || "text")}">${escapeHtml(content)}</code></pre>`;
2174
+ }
2175
+ const cut = content.lastIndexOf("\n") + 1;
2176
+ if (cut <= hlLen) return;
2177
+ const now = Date.now();
2178
+ if (now - (_lastHighlightAt.get(segId) ?? 0) < HIGHLIGHT_DEBOUNCE_MS) return;
2179
+ markThrottle(_lastHighlightAt, segId, now);
2180
+ void contextConfig(element).highlightCode(content.slice(0, cut), lang).then((html) => {
2181
+ const live = element.querySelector(paneSelector);
2182
+ if (!live) return;
2183
+ if (cut <= highlightedLen(live)) return;
2184
+ live.innerHTML = html;
2185
+ const codeEl = live.querySelector("code") ?? live;
2186
+ const span = document.createElement("span");
2187
+ span.dataset.aparteTail = "";
2188
+ span.textContent = content.slice(cut);
2189
+ codeEl.appendChild(span);
2190
+ live.dataset.aparteHlLen = String(cut);
2191
+ }).catch(() => {
2192
+ });
2193
+ }
2078
2194
  const codeRenderer = {
2079
2195
  type: "code",
2080
2196
  render: (segment) => `
@@ -2091,6 +2207,13 @@ const codeRenderer = {
2091
2207
  </div>
2092
2208
  </div>
2093
2209
  `,
2210
+ /** The copy button's icon and tooltip — nothing else here comes from config. */
2211
+ relabel: (element) => {
2212
+ const copyBtn = element.querySelector(".code-copy");
2213
+ if (!copyBtn || copyBtn.dataset.copied) return;
2214
+ copyBtn.setAttribute("title", contextConfig().t("copy"));
2215
+ copyBtn.innerHTML = contextConfig().getIcon("copy");
2216
+ },
2094
2217
  setup: (element, segment) => {
2095
2218
  const wrapper = element.querySelector(".code-content-wrapper");
2096
2219
  if (wrapper) {
@@ -2107,22 +2230,18 @@ const codeRenderer = {
2107
2230
  });
2108
2231
  copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
2109
2232
  copyBtn.setAttribute("title", contextConfig(copyBtn).t("copied"));
2233
+ copyBtn.dataset.copied = "1";
2110
2234
  setTimeout(() => {
2111
2235
  copyBtn.innerHTML = contextConfig(copyBtn).getIcon("copy");
2112
2236
  copyBtn.setAttribute("title", contextConfig(copyBtn).t("copy"));
2237
+ delete copyBtn.dataset.copied;
2113
2238
  }, 1500);
2114
2239
  });
2115
2240
  }
2116
2241
  },
2117
2242
  update: (element, segment) => {
2118
2243
  if (segment.isStreaming) {
2119
- const codeEl = element.querySelector(".code-content-wrapper code");
2120
- if (codeEl) {
2121
- codeEl.textContent = segment.content;
2122
- } else {
2123
- const wrapper = element.querySelector(".code-content-wrapper");
2124
- if (wrapper) wrapper.innerHTML = `<pre><code class="language-${escapeHtml(segment.language || "text")}">${escapeHtml(segment.content)}</code></pre>`;
2125
- }
2244
+ streamHighlight(element, ".code-content-wrapper", segment.content, segment.language || "", segment.id);
2126
2245
  } else {
2127
2246
  const wrapper = element.querySelector(".code-content-wrapper");
2128
2247
  if (wrapper) {
@@ -2135,58 +2254,6 @@ const codeRenderer = {
2135
2254
  },
2136
2255
  getStyles: () => ``
2137
2256
  };
2138
- const terminalRenderer = {
2139
- type: "terminal",
2140
- render: (segment) => `
2141
- <div class="segment segment-terminal" data-segment-id="${escapeHtml(segment.id)}">
2142
- <div class="terminal-command-block">
2143
- <div class="terminal-icon">
2144
- ${contextConfig().getIcon("terminal")}
2145
- </div>
2146
- <code class="terminal-command">${escapeHtml(segment.command || "")}</code>
2147
- <div class="terminal-actions">
2148
- ${segment.isRunning ? `<span class="terminal-running"><span class="spinner"></span>${contextConfig().t("running")}</span>` : contextConfig().getHostHandlers().terminalRun ? `<button class="terminal-run-btn" data-action="run" aria-label="${escapeAttr(contextConfig().t("run"))}" title="${escapeAttr(contextConfig().t("run"))}">${contextConfig().t("run")}</button>` : ""}
2149
- <button class="terminal-copy-btn" data-action="copy" aria-label="${escapeAttr(contextConfig().t("copy"))}" title="${escapeAttr(contextConfig().t("copy"))}">
2150
- ${contextConfig().getIcon("copy")}
2151
- </button>
2152
- </div>
2153
- </div>
2154
- ${segment.output ? `<div class="terminal-output">${escapeHtml(segment.output)}</div>` : ""}
2155
- ${segment.exitCode !== void 0 && segment.exitCode !== 0 ? `<div class="terminal-error">Command failed with exit code ${segment.exitCode}</div>` : ""}
2156
- </div>
2157
- `,
2158
- setup: (element, segment) => {
2159
- const copyBtn = element.querySelector(".terminal-copy-btn");
2160
- const command = element.querySelector(".terminal-command");
2161
- if (copyBtn && command) {
2162
- copyBtn.addEventListener("click", () => {
2163
- void navigator.clipboard.writeText(command.textContent || "").catch(() => {
2164
- });
2165
- copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
2166
- copyBtn.setAttribute("title", contextConfig(copyBtn).t("copied"));
2167
- setTimeout(() => {
2168
- copyBtn.innerHTML = contextConfig(copyBtn).getIcon("copy");
2169
- copyBtn.setAttribute("title", contextConfig(copyBtn).t("copy"));
2170
- }, 1500);
2171
- });
2172
- }
2173
- const runBtn = element.querySelector(".terminal-run-btn");
2174
- if (runBtn) {
2175
- runBtn.addEventListener("click", () => {
2176
- element.dispatchEvent(new CustomEvent("aparte-terminal-run", {
2177
- bubbles: true,
2178
- composed: true,
2179
- detail: {
2180
- segmentId: segment.id,
2181
- messageId: segment.messageId,
2182
- command: command?.textContent || ""
2183
- }
2184
- }));
2185
- });
2186
- }
2187
- },
2188
- getStyles: () => ``
2189
- };
2190
2257
  const errorRenderer = {
2191
2258
  type: "error",
2192
2259
  render: (segment) => {
@@ -2205,13 +2272,28 @@ const errorRenderer = {
2205
2272
  ${contextConfig().getIcon("error") || "⚠"}
2206
2273
  </div>
2207
2274
  <div class="error-content">
2208
- <div class="error-title">Error</div>
2275
+ <div class="error-title">${escapeHtml(contextConfig().t("error"))}</div>
2209
2276
  <div class="error-message">${escapeHtml(segment.content)}</div>
2210
2277
  ${segment.details ? `<div class="error-details">${escapeHtml(segment.details)}</div>` : ""}
2211
2278
  </div>
2212
2279
  </div>
2213
2280
  `;
2214
2281
  },
2282
+ /**
2283
+ * The icon and the heading. The heading used to be the hardcoded literal `Error`
2284
+ * while `locale.error` — a REQUIRED key, documented, and already translated to
2285
+ * "Erreur" — was read by nothing at all. A translated string with no consumer and
2286
+ * a literal with no translation, in the same card.
2287
+ *
2288
+ * Not the message: that is the model's or the transport's text, in whatever
2289
+ * language it arrived in. Relabelling it would be inventing content.
2290
+ */
2291
+ relabel: (element) => {
2292
+ const wrap = element.querySelector(".error-icon-wrapper");
2293
+ if (wrap) wrap.innerHTML = contextConfig().getIcon("error") || "⚠";
2294
+ const title = element.querySelector(".error-title");
2295
+ if (title) title.textContent = contextConfig().t("error");
2296
+ },
2215
2297
  getStyles: () => ``
2216
2298
  };
2217
2299
  const progressRenderer = {
@@ -2223,31 +2305,6 @@ const progressRenderer = {
2223
2305
  },
2224
2306
  getStyles: () => ``
2225
2307
  };
2226
- function renderFileNode(node, depth = 0) {
2227
- const indent = depth * 16;
2228
- const icon = node.type === "directory" ? "📁" : "📄";
2229
- const statusClass = node.status ? `file-status-${escapeHtml(node.status)}` : "";
2230
- let html = `<div class="file-node ${statusClass}" style="padding-left: ${indent}px"><span class="file-icon">${icon}</span><span class="file-name">${escapeHtml(node.name)}</span></div>`;
2231
- if (node.children) {
2232
- for (const child of node.children) {
2233
- html += renderFileNode(child, depth + 1);
2234
- }
2235
- }
2236
- return html;
2237
- }
2238
- const fileTreeRenderer = {
2239
- type: "file-tree",
2240
- render: (segment) => {
2241
- let filesHtml = "";
2242
- if (segment.files) {
2243
- for (const file of segment.files) {
2244
- filesHtml += renderFileNode(file, 0);
2245
- }
2246
- }
2247
- return `<div class="segment segment-file-tree" data-segment-id="${escapeHtml(segment.id)}">${segment.title ? `<div class="file-tree-title">${escapeHtml(segment.title)}</div>` : ""}<div class="file-tree-content">${filesHtml}</div></div>`;
2248
- },
2249
- getStyles: () => ``
2250
- };
2251
2308
  const toolCallRenderer = {
2252
2309
  type: "tool_call",
2253
2310
  render: (segment) => {
@@ -2289,6 +2346,39 @@ const toolCallRenderer = {
2289
2346
  </div>
2290
2347
  `;
2291
2348
  },
2349
+ /**
2350
+ * The pill's glyphs and — when the turn is paused for a person — the two labels
2351
+ * on the approval gate. Those are the highest-stakes strings in the library: a
2352
+ * reader deciding whether to let a tool run should not be reading them in a
2353
+ * language they did not choose.
2354
+ *
2355
+ * No child node is added or removed, so the buttons keep their listeners and,
2356
+ * critically, their FOCUS — this is the one control a keyboard user may be
2357
+ * sitting on when a config change fires.
2358
+ */
2359
+ relabel: (element, segment) => {
2360
+ const cfg = contextConfig();
2361
+ const icon = element.querySelector(".tool-pill-icon");
2362
+ if (icon) icon.innerHTML = cfg.getIcon("tool");
2363
+ const statusEl = element.querySelector(".tool-pill-status");
2364
+ if (statusEl) {
2365
+ const s = segment.status;
2366
+ statusEl.innerHTML = s === "resolved" ? cfg.getIcon("check") : s === "aborted" || s === "rejected" ? cfg.getIcon("close") : "";
2367
+ }
2368
+ const loc = cfg.getLocale();
2369
+ const approve = element.querySelector(".tool-approve-btn");
2370
+ if (approve) {
2371
+ const label = loc.approveTool ?? "Approve";
2372
+ approve.textContent = label;
2373
+ approve.setAttribute("aria-label", label);
2374
+ }
2375
+ const reject = element.querySelector(".tool-reject-btn");
2376
+ if (reject) {
2377
+ const label = loc.rejectTool ?? "Reject";
2378
+ reject.textContent = label;
2379
+ reject.setAttribute("aria-label", label);
2380
+ }
2381
+ },
2292
2382
  setup: (element, segment) => {
2293
2383
  if (segment.status === "awaiting-approval") {
2294
2384
  const toolCallId = segment.toolCall?.id;
@@ -2355,11 +2445,12 @@ const toolCallRenderer = {
2355
2445
  @keyframes tool-spin { to { transform: rotate(360deg); } }
2356
2446
  `
2357
2447
  };
2448
+ const waitingLabel = () => contextConfig().t("generating");
2358
2449
  const pipelineWaitingRenderer = {
2359
2450
  type: "pipeline-waiting",
2360
2451
  render: (segment) => {
2361
2452
  return `
2362
- <div class="segment segment-pipeline-waiting" data-segment-id="${escapeHtml(segment.id)}" aria-label="Generating…" role="status">
2453
+ <div class="segment segment-pipeline-waiting" data-segment-id="${escapeHtml(segment.id)}" aria-label="${escapeAttr(waitingLabel())}" role="status">
2363
2454
  <span class="pw-dot"></span>
2364
2455
  <span class="pw-dot"></span>
2365
2456
  <span class="pw-dot"></span>
@@ -2367,6 +2458,15 @@ const pipelineWaitingRenderer = {
2367
2458
  },
2368
2459
  update: () => {
2369
2460
  },
2461
+ /**
2462
+ * Three dots and an accessible name. The dots are CSS; the name is the whole
2463
+ * content as far as a screen reader is concerned, and it was hardcoded English in
2464
+ * every locale — the one string in this library where being untranslated is
2465
+ * invisible to everyone who can see the screen.
2466
+ */
2467
+ relabel: (element) => {
2468
+ element.setAttribute("aria-label", waitingLabel());
2469
+ },
2370
2470
  setup: (el2) => {
2371
2471
  const parent = el2.parentElement;
2372
2472
  if (!parent) return;
@@ -2451,26 +2551,6 @@ function findClosingFence(s) {
2451
2551
  }
2452
2552
  return -1;
2453
2553
  }
2454
- const _lastHighlightAt = /* @__PURE__ */ new Map();
2455
- function markThrottle(map, id, at) {
2456
- map.delete(id);
2457
- if (map.size >= MAX_THROTTLE_ENTRIES) {
2458
- const oldest = map.keys().next().value;
2459
- if (oldest !== void 0) map.delete(oldest);
2460
- }
2461
- map.set(id, at);
2462
- }
2463
- function debounceHighlight(element, paneSelector, content, lang, segId) {
2464
- const now = Date.now();
2465
- const last = _lastHighlightAt.get(segId) ?? 0;
2466
- if (now - last < HIGHLIGHT_DEBOUNCE_MS) return;
2467
- markThrottle(_lastHighlightAt, segId, now);
2468
- void contextConfig(element).highlightCode(content, lang).then((html) => {
2469
- const wrapper = element.querySelector(paneSelector);
2470
- if (wrapper) wrapper.innerHTML = html;
2471
- }).catch(() => {
2472
- });
2473
- }
2474
2554
  function labelForKind(kind) {
2475
2555
  switch (kind) {
2476
2556
  case "react":
@@ -2509,8 +2589,6 @@ function labelForKind(kind) {
2509
2589
  return "Artifact";
2510
2590
  }
2511
2591
  }
2512
- const MAX_THROTTLE_ENTRIES = 256;
2513
- const HIGHLIGHT_DEBOUNCE_MS = 400;
2514
2592
  const PREVIEW_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:";
2515
2593
  function withMetaCsp(doc) {
2516
2594
  const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttr(PREVIEW_CSP)}">`;
@@ -2527,9 +2605,18 @@ function buildPreviewBody(kind, body, title) {
2527
2605
  if (startsWithDoctype(body)) return body;
2528
2606
  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>`;
2529
2607
  }
2608
+ // An SVG with only a `viewBox` — the recommended, responsive form, and the one
2609
+ // a model writes most often — has NO intrinsic size. As a flex item its cross
2610
+ // size then collapses to zero and the frame is blank: the preview worked for an
2611
+ // SVG that declared `width`/`height` and silently showed nothing for the
2612
+ // idiomatic one. Reported from the landing the moment its demo payload became a
2613
+ // real file instead of a hand-written chart with dimensions on it.
2614
+ //
2615
+ // The rule is narrowed by attribute selector rather than applied to every SVG,
2616
+ // so an SVG that DOES state its size keeps the size it asked for.
2530
2617
  case "svg":
2531
2618
  return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
2532
- <style>html,body{margin:0;height:100%;display:flex;align-items:center;justify-content:center;background:#fff}svg{max-width:90%;max-height:90%}</style>
2619
+ <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>
2533
2620
  </head><body>${body}</body></html>`;
2534
2621
  case "js": {
2535
2622
  const safeBody = escapeClosingScriptTag(body);
@@ -2612,6 +2699,7 @@ function renderBinaryFileArtifact(segment, kind) {
2612
2699
  const iconLabel = FILE_ICON_LABEL[kind] ?? kind.toUpperCase();
2613
2700
  const isStreaming = !!segment.isStreaming;
2614
2701
  const canRedownload = contextConfig().getHostHandlers().artifactRedownload;
2702
+ const downloadLabel = escapeHtml(contextConfig().t("download"));
2615
2703
  const cached = _binaryArtifactCache.get(segment.id);
2616
2704
  if (cached && !isStreaming) {
2617
2705
  const previewBody = cached.previewHtml ? contextConfig().sanitizeHtml(cached.previewHtml) : `<div class="aparte-art-file__preview-empty">Preview not available for ${escapeHtml(kind)} yet</div>`;
@@ -2627,7 +2715,7 @@ function renderBinaryFileArtifact(segment, kind) {
2627
2715
  <div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(formatBytes(cached.bytes))} · ${escapeHtml(kind.toUpperCase())}</div>
2628
2716
  </div>
2629
2717
  <div class="aparte-art-file__actions">
2630
- ${canRedownload ? '<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download">Download</button>' : ""}
2718
+ ${canRedownload ? `<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download">${downloadLabel}</button>` : ""}
2631
2719
  </div>
2632
2720
  </div>
2633
2721
  <div class="aparte-art-file__body">
@@ -2640,7 +2728,7 @@ function renderBinaryFileArtifact(segment, kind) {
2640
2728
  `;
2641
2729
  }
2642
2730
  const cleanContent = stripCodeFences(segment.content || "");
2643
- const subText = isStreaming ? "Generating…" : "Rebuilding preview…";
2731
+ const subText = isStreaming ? contextConfig().t("generating") : contextConfig().t("rebuildingPreview");
2644
2732
  return `
2645
2733
  <div class="segment segment-artifact-file"
2646
2734
  data-segment-id="${escapeHtml(segment.id)}"
@@ -2653,7 +2741,7 @@ function renderBinaryFileArtifact(segment, kind) {
2653
2741
  <div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(subText)}</div>
2654
2742
  </div>
2655
2743
  <div class="aparte-art-file__actions">
2656
- ${canRedownload ? '<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download" disabled>Download</button>' : ""}
2744
+ ${canRedownload ? `<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download" disabled>${downloadLabel}</button>` : ""}
2657
2745
  </div>
2658
2746
  </div>
2659
2747
  <div class="aparte-art-file__body">
@@ -2759,12 +2847,11 @@ function updateBinaryFileArtifact(element, segment, isStreaming) {
2759
2847
  const state = element.getAttribute("data-state");
2760
2848
  if (state === "ready") return;
2761
2849
  const cleanContent = stripCodeFences(segment.content || "");
2762
- const codeEl = element.querySelector('[data-role="code-pane"] code');
2763
- if (codeEl) {
2764
- codeEl.textContent = cleanContent;
2765
- }
2766
2850
  if (isStreaming) {
2767
- debounceHighlight(element, '[data-role="code-pane"]', cleanContent, "js", segment.id);
2851
+ streamHighlight(element, '[data-role="code-pane"]', cleanContent, "js", segment.id);
2852
+ } else {
2853
+ const codeEl = element.querySelector('[data-role="code-pane"] code');
2854
+ if (codeEl) codeEl.textContent = cleanContent;
2768
2855
  }
2769
2856
  if (!isStreaming && state === "streaming") {
2770
2857
  element.setAttribute("data-state", "compiling");
@@ -2811,9 +2898,9 @@ function showSandboxError(element, phase, errorMsg) {
2811
2898
  const short = (errorMsg.split("\n")[0] ?? "").slice(0, 240);
2812
2899
  body.innerHTML = `
2813
2900
  <div class="aparte-art-file__error">
2814
- <div class="aparte-art-file__error-title">The sandbox failed during generation.</div>
2901
+ <div class="aparte-art-file__error-title">${escapeHtml(contextConfig().t("sandboxError"))}</div>
2815
2902
  <div class="aparte-art-file__error-msg">${escapeHtml(short)}</div>
2816
- <div class="aparte-art-file__error-hint">Common cause: the model produced invalid code (undefined variable, wrong argument type). Retry the request — the model may produce different code.</div>
2903
+ <div class="aparte-art-file__error-hint">${escapeHtml(contextConfig().t("sandboxErrorHint"))}</div>
2817
2904
  </div>
2818
2905
  `;
2819
2906
  }
@@ -2831,6 +2918,10 @@ const artifactRenderer = {
2831
2918
  const displayLang = languageForKind(kind);
2832
2919
  const isStreaming = !!segment.isStreaming;
2833
2920
  const previewable = PREVIEWABLE_KINDS.has(kind);
2921
+ const cfg = contextConfig();
2922
+ const downloadLabel = cfg.t("download");
2923
+ const previewLabel = cfg.t("preview");
2924
+ const codeLabel = cfg.t("code");
2834
2925
  const isBinary = BINARY_FILE_KINDS.has(kind);
2835
2926
  const cleanContent = stripCodeFences(segment.content || "");
2836
2927
  return `
@@ -2848,17 +2939,17 @@ const artifactRenderer = {
2848
2939
  ${isStreaming ? '<span class="aparte-art-card__pulse" aria-label="Streaming"></span>' : ""}
2849
2940
  </div>
2850
2941
  <div class="aparte-art-card__actions">
2851
- <button type="button" class="aparte-art-card__btn" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}" aria-label="Copy">
2942
+ <button type="button" class="aparte-art-card__btn" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}" aria-label="${escapeAttr(contextConfig().t("copy"))}">
2852
2943
  ${contextConfig().getIcon("copy")}
2853
2944
  </button>
2854
- <button type="button" class="aparte-art-card__btn" data-action="download" title="Download" aria-label="Download" ${isStreaming ? "disabled" : ""}>
2945
+ <button type="button" class="aparte-art-card__btn" data-action="download" title="${escapeAttr(downloadLabel)}" aria-label="${escapeAttr(downloadLabel)}" ${isStreaming ? "disabled" : ""}>
2855
2946
  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v9m0 0l-3-3m3 3l3-3M2 13h12"/></svg>
2856
2947
  </button>
2857
2948
  </div>
2858
2949
  </header>
2859
2950
  <nav class="aparte-art-card__tabs" role="tablist">
2860
- ${previewable ? `<button type="button" role="tab" data-tab-target="preview" aria-selected="false" ${isStreaming ? "disabled" : ""}>Preview</button>` : ""}
2861
- <button type="button" role="tab" data-tab-target="code" aria-selected="true">Code</button>
2951
+ <button type="button" role="tab" data-tab-target="code" aria-selected="true">${escapeHtml(codeLabel)}</button>
2952
+ ${previewable ? `<button type="button" role="tab" data-tab-target="preview" aria-selected="false" ${isStreaming ? "disabled" : ""}>${escapeHtml(previewLabel)}</button>` : ""}
2862
2953
  </nav>
2863
2954
  <div class="aparte-art-card__body">
2864
2955
  <div class="aparte-art-card__pane" data-pane="code">
@@ -2868,13 +2959,44 @@ const artifactRenderer = {
2868
2959
  </div>
2869
2960
  ${previewable ? `
2870
2961
  <div class="aparte-art-card__pane" data-pane="preview">
2871
- <div class="aparte-art-card__pending">Press Preview to run this artifact.</div>
2962
+ <div class="aparte-art-card__pending">${escapeHtml(cfg.t("previewPending"))}</div>
2872
2963
  </div>
2873
2964
  ` : ""}
2874
2965
  </div>
2875
2966
  </div>
2876
2967
  `;
2877
2968
  },
2969
+ /**
2970
+ * Every string this card shows: the copy button's tooltip, glyph and accessible
2971
+ * name, the download button's title and label, and the two tab names. They were
2972
+ * all hardcoded literals until they got locale keys — including the copy button's
2973
+ * `aria-label`, which said "Copy" while its own `title` one attribute away already
2974
+ * went through `t('copy')`, so a French reader got a French tooltip and an English
2975
+ * announcement.
2976
+ *
2977
+ * Nothing here touches the tab state or the preview pane: a mounted iframe is
2978
+ * running model-authored code, and re-rendering this card is exactly what the
2979
+ * hook exists to avoid.
2980
+ */
2981
+ relabel: (element) => {
2982
+ const cfg = contextConfig();
2983
+ const copyBtn = element.querySelector('.aparte-art-card__btn[data-action="copy"]');
2984
+ if (copyBtn) {
2985
+ copyBtn.setAttribute("title", cfg.t("copy"));
2986
+ copyBtn.setAttribute("aria-label", cfg.t("copy"));
2987
+ copyBtn.innerHTML = cfg.getIcon("copy");
2988
+ }
2989
+ const dl = element.querySelector('.aparte-art-card__btn[data-action="download"]');
2990
+ if (dl) {
2991
+ const label = cfg.t("download");
2992
+ dl.setAttribute("title", label);
2993
+ dl.setAttribute("aria-label", label);
2994
+ }
2995
+ const previewTab = element.querySelector('[data-tab-target="preview"]');
2996
+ if (previewTab) previewTab.textContent = cfg.t("preview");
2997
+ const codeTab = element.querySelector('[data-tab-target="code"]');
2998
+ if (codeTab) codeTab.textContent = cfg.t("code");
2999
+ },
2878
3000
  setup: (element, segment) => {
2879
3001
  latestSegment.set(element, segment);
2880
3002
  const kind = (segment.artifactType || "").toLowerCase();
@@ -2955,22 +3077,24 @@ const artifactRenderer = {
2955
3077
  }
2956
3078
  const wasStreaming = element.getAttribute("data-streaming") === "true";
2957
3079
  const cleanContent = stripCodeFences(segment.content || "");
2958
- const codeEl = element.querySelector(".code-content-wrapper code");
2959
- if (codeEl) {
2960
- codeEl.textContent = cleanContent;
2961
- } else {
2962
- const wrapper = element.querySelector(".code-content-wrapper");
2963
- if (wrapper) {
2964
- const displayLang = languageForKind(kind);
2965
- wrapper.innerHTML = `<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>`;
2966
- }
2967
- }
2968
3080
  if (isStreaming) {
2969
3081
  const segId = element.getAttribute("data-segment-id") ?? segment.id;
2970
- debounceHighlight(element, ".code-content-wrapper", cleanContent, languageForKind(kind), segId);
3082
+ streamHighlight(element, ".code-content-wrapper", cleanContent, languageForKind(kind), segId);
3083
+ } else {
3084
+ const codeEl = element.querySelector(".code-content-wrapper code");
3085
+ if (codeEl) {
3086
+ codeEl.textContent = cleanContent;
3087
+ } else {
3088
+ const wrapper = element.querySelector(".code-content-wrapper");
3089
+ if (wrapper) {
3090
+ const displayLang = languageForKind(kind);
3091
+ wrapper.innerHTML = `<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>`;
3092
+ }
3093
+ }
2971
3094
  }
2972
3095
  if (wasStreaming && !isStreaming) {
2973
3096
  element.setAttribute("data-streaming", "false");
3097
+ element.querySelector(".aparte-art-card__pulse")?.remove();
2974
3098
  const wrapper = element.querySelector(".code-content-wrapper");
2975
3099
  if (wrapper) {
2976
3100
  const displayLang = languageForKind(kind);
@@ -3042,7 +3166,24 @@ const artifactRenderer = {
3042
3166
  }
3043
3167
  .aparte-art-card__btn:disabled { opacity: 0.4; cursor: not-allowed; }
3044
3168
  .aparte-art-card__tabs {
3045
- display: flex; gap: 2px;
3169
+ /* justify-content and the padding are DECLARED, not left to a default.
3170
+ Core is light DOM on purpose - no shadow root, no ::part(), any selector
3171
+ reaches in - and the corollary is that a component must state what its
3172
+ layout depends on, because an undeclared property has nothing to override
3173
+ a host rule with. A consuming page with a bare nav selector setting
3174
+ justify-content: space-between and padding-top (this library's own docs
3175
+ site had exactly that) otherwise pushes these two tabs to opposite ends of
3176
+ the card and pads the row out. No backticks in here: this block is a JS
3177
+ template literal, and one of them ended it.
3178
+
3179
+ flex-end, and Code first in the DOM. The card OPENS on Code - mounting the
3180
+ preview would execute model-authored code with no gesture (ratified
3181
+ decision #8) - and a selected tab sitting second reads backwards. Right,
3182
+ because the header above puts the artifact's identity on the left and its
3183
+ copy/download buttons on the right, so this keeps every control in one
3184
+ column. DOM order is also keyboard order, so the tab a reader lands on
3185
+ first is the one already showing. */
3186
+ display: flex; justify-content: flex-end; align-items: stretch; gap: 2px;
3046
3187
  padding: 4px 8px 0;
3047
3188
  border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
3048
3189
  background: var(--aparte-surface-2, rgba(0,0,0,0.02));
@@ -3065,17 +3206,37 @@ const artifactRenderer = {
3065
3206
  font-weight: 500;
3066
3207
  }
3067
3208
  .aparte-art-card__tabs button:disabled { opacity: 0.4; cursor: not-allowed; }
3209
+ /* The card's heights, as variables with ONE owner each.
3210
+ They were four hardcoded numbers in the only part of this card that did not
3211
+ use a variable - everything else here already reads var(--aparte-code-bg) and
3212
+ friends - and two of them had to agree while a third contradicted a fourth:
3213
+ the code pane repeated the body's 600px, and the "press Preview" placeholder
3214
+ was 120px tall inside a body whose min-height said 80, so that minimum applied
3215
+ to nothing.
3216
+ The frame stays a FIXED height rather than an aspect ratio, which is what
3217
+ embeds of arbitrary HTML actually do - CodeSandbox documents 500px, StackBlitz
3218
+ takes a height parameter - because a frame with an opaque origin cannot be
3219
+ measured and a 16/10 ratio on a wide card is enormous. The vh cap is the part
3220
+ that was missing: a fixed 480px should not own a phone screen.
3221
+ Each default lives in its read, as var(--x, literal), the way every other
3222
+ value in this file already does - not in a declaration block on top of the
3223
+ fallbacks, which would be two owners of one number again. It also means the
3224
+ docs' CSS-variable generator finds them: its sweep looks for reads that
3225
+ carry a fallback, so a read without one is a public knob nobody documents. */
3068
3226
  .aparte-art-card__body {
3069
3227
  position: relative;
3070
- min-height: 80px;
3071
- max-height: 600px;
3228
+ /* The placeholder is the tallest thing this box can hold while empty, so it
3229
+ IS the minimum - one number instead of two that disagreed. */
3230
+ min-height: var(--aparte-artifact-pending-height, 120px);
3231
+ max-height: var(--aparte-artifact-body-max, 600px);
3072
3232
  overflow: hidden;
3073
3233
  }
3074
3234
  .aparte-art-card__pane { display: none; height: 100%; }
3075
3235
  .segment-artifact-card[data-tab="code"] .aparte-art-card__pane[data-pane="code"] { display: block; }
3076
3236
  .segment-artifact-card[data-tab="preview"] .aparte-art-card__pane[data-pane="preview"] { display: block; }
3077
3237
  .aparte-art-card__pane[data-pane="code"] {
3078
- max-height: 600px; overflow: auto;
3238
+ /* The body already caps this; repeating the number was the second owner. */
3239
+ max-height: var(--aparte-artifact-body-max, 600px); overflow: auto;
3079
3240
  }
3080
3241
  .aparte-art-card__pane[data-pane="code"] pre {
3081
3242
  margin: 0; padding: 12px;
@@ -3084,13 +3245,14 @@ const artifactRenderer = {
3084
3245
  }
3085
3246
  .aparte-art-card__frame {
3086
3247
  display: block;
3087
- width: 100%; height: 480px;
3248
+ width: 100%;
3249
+ height: min(var(--aparte-artifact-frame-height, 480px), var(--aparte-artifact-frame-max, 70vh));
3088
3250
  border: 0;
3089
3251
  background: #fff;
3090
3252
  }
3091
3253
  .aparte-art-card__pending {
3092
3254
  display: flex; align-items: center; justify-content: center;
3093
- height: 120px;
3255
+ height: var(--aparte-artifact-pending-height, 120px);
3094
3256
  color: var(--aparte-text-muted, rgba(0,0,0,0.5));
3095
3257
  font-size: 0.85rem;
3096
3258
  font-style: italic;
@@ -3116,7 +3278,7 @@ const artifactRenderer = {
3116
3278
  position: relative;
3117
3279
  }
3118
3280
  .aparte-art-file__code-pane {
3119
- max-height: 360px; overflow: auto;
3281
+ max-height: var(--aparte-artifact-file-code-max, 360px); overflow: auto;
3120
3282
  background: var(--aparte-code-bg, #fafafa);
3121
3283
  }
3122
3284
  .aparte-art-file__code-pane pre {
@@ -3125,7 +3287,7 @@ const artifactRenderer = {
3125
3287
  font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
3126
3288
  }
3127
3289
  .aparte-art-file__preview-pane {
3128
- max-height: 460px;
3290
+ max-height: var(--aparte-artifact-file-preview-max, 460px);
3129
3291
  overflow: auto;
3130
3292
  background: #fff;
3131
3293
  /* Preview is a document view — force light scheme regardless of
@@ -3385,10 +3547,8 @@ const DEFAULT_RENDERERS = [
3385
3547
  textRenderer,
3386
3548
  thinkingRenderer,
3387
3549
  codeRenderer,
3388
- terminalRenderer,
3389
3550
  errorRenderer,
3390
3551
  progressRenderer,
3391
- fileTreeRenderer,
3392
3552
  toolCallRenderer,
3393
3553
  artifactRenderer,
3394
3554
  pipelineWaitingRenderer
@@ -3398,14 +3558,47 @@ const ACTIVITY_KEYS = ["content", "output", "result"];
3398
3558
  function isActivity(updates) {
3399
3559
  return ACTIVITY_KEYS.some((key) => key in updates);
3400
3560
  }
3401
- function stampSegmentOnInsert(segments, segment, messageId) {
3561
+ function stampSegmentOnInsert(segments, segment, messageId, defaults) {
3562
+ const base = applyDefaults(segment, defaults);
3402
3563
  return {
3403
- ...segment,
3564
+ ...base,
3404
3565
  messageId: segment.messageId ?? messageId,
3405
3566
  index: segment.index ?? segments.length,
3406
- startedAt: segment.startedAt ?? Date.now()
3567
+ // A start already present is kept — a segment a consumer rebuilt from their own
3568
+ // storage keeps the number it was persisted with. `adoptSegment` is the path
3569
+ // that never writes one at all.
3570
+ ...timingPatch(base, { startedAt: segmentTiming(segment)?.startedAt ?? Date.now() })
3407
3571
  };
3408
3572
  }
3573
+ function adoptSegment(segments, segment, messageId) {
3574
+ return {
3575
+ ...segment,
3576
+ messageId,
3577
+ index: segments.length,
3578
+ isStreaming: false
3579
+ };
3580
+ }
3581
+ const RESERVED = /* @__PURE__ */ new Set(["id", "type", "messageId", "index"]);
3582
+ function segmentTiming(segment) {
3583
+ return segment.meta?.aparte;
3584
+ }
3585
+ function timingPatch(segment, timing) {
3586
+ return { meta: { ...segment.meta, aparte: { ...segment.meta?.aparte, ...timing } } };
3587
+ }
3588
+ function applyDefaults(segment, defaults) {
3589
+ if (!defaults) return segment;
3590
+ const out = { ...segment };
3591
+ for (const [key, value] of Object.entries(defaults)) {
3592
+ if (RESERVED.has(key) || key in segment) continue;
3593
+ if (key === "meta") {
3594
+ const { aparte: _forged, ...rest } = value ?? {};
3595
+ out.meta = rest;
3596
+ continue;
3597
+ }
3598
+ out[key] = value;
3599
+ }
3600
+ return out;
3601
+ }
3409
3602
  function isSegmentSettled(segment) {
3410
3603
  if (segment.type === "tool_call") return !OPEN_TOOL_STATUSES.has(segment.status);
3411
3604
  return segment.isStreaming === false;
@@ -3414,17 +3607,32 @@ function stampSegmentOnUpdate(segment, updates) {
3414
3607
  if (isSegmentSettled(segment)) return updates;
3415
3608
  const merged = { ...segment, ...updates };
3416
3609
  if (isSegmentSettled(merged)) {
3417
- return { ...updates, endedAt: segment.endedAt ?? Date.now() };
3610
+ const ended = segmentTiming(segment)?.endedAt;
3611
+ return { ...updates, ...timingPatch(segment, { endedAt: ended ?? Date.now() }) };
3418
3612
  }
3419
- return isActivity(updates) ? { ...updates, endedAt: Date.now() } : updates;
3613
+ return isActivity(updates) ? { ...updates, ...timingPatch(segment, { endedAt: Date.now() }) } : updates;
3614
+ }
3615
+ function mergeSegmentUpdate(segment, updates) {
3616
+ const merged = { ...segment, ...updates };
3617
+ if (!("meta" in updates)) return merged;
3618
+ return {
3619
+ ...merged,
3620
+ meta: {
3621
+ ...segment.meta,
3622
+ ...updates.meta,
3623
+ // Neither side's timing wins by accident: whichever one carries it, wins,
3624
+ // and a consumer writing `meta` without an `aparte` key keeps ours.
3625
+ ...segment.meta?.aparte || updates.meta?.aparte ? { aparte: { ...segment.meta?.aparte, ...updates.meta?.aparte } } : {}
3626
+ }
3627
+ };
3420
3628
  }
3421
3629
  function segmentDuration(segment) {
3422
- const { startedAt, endedAt } = segment;
3630
+ const { startedAt, endedAt } = segmentTiming(segment) ?? {};
3423
3631
  if (startedAt == null || endedAt == null) return void 0;
3424
3632
  return endedAt - startedAt;
3425
3633
  }
3426
3634
  function stampSegmentActivity(segment) {
3427
- return isSegmentSettled(segment) ? {} : { endedAt: Date.now() };
3635
+ return isSegmentSettled(segment) ? {} : timingPatch(segment, { endedAt: Date.now() });
3428
3636
  }
3429
3637
  function segmentContentUpdate(segment) {
3430
3638
  const content = segment.content;
@@ -3439,6 +3647,16 @@ function isTerminalStatus(status) {
3439
3647
  function openSegmentIds(segments) {
3440
3648
  return segments.filter((segment) => !isSegmentSettled(segment)).map((segment) => segment.id);
3441
3649
  }
3650
+ function adoptMessageSegments(message) {
3651
+ if (!message.segments?.length) return { ...message };
3652
+ return {
3653
+ ...message,
3654
+ segments: message.segments.reduce((acc, segment) => {
3655
+ acc.push(adoptSegment(acc, segment, message.id));
3656
+ return acc;
3657
+ }, [])
3658
+ };
3659
+ }
3442
3660
  function renumberSegments(segments) {
3443
3661
  for (let i = 0; i < segments.length; i++) {
3444
3662
  const segment = segments[i];
@@ -4139,10 +4357,14 @@ class AparteChatHost {
4139
4357
  // Controller-driven flushes mirror AparteConversationManager state, not
4140
4358
  // user intent — deliberately NOT emitted as `messagesChange` to
4141
4359
  // avoid a feedback loop with hosts using a two-way `messages` input.
4360
+ // Adopted here rather than passed through: this is the conversation
4361
+ // controller handing back a stored list, and it goes STRAIGHT to the
4362
+ // framework's own reactive setter — so in every wrapper it never met the
4363
+ // stamping seam at all, tree or no tree.
4142
4364
  setMessages: (msgs) => {
4143
4365
  this._beginConversationSwap();
4144
4366
  this._vp()?.resetSpacer?.();
4145
- this.binding.setMessages([...msgs]);
4367
+ this.binding.setMessages(msgs.map(adoptMessageSegments));
4146
4368
  },
4147
4369
  appendMessage: (msg) => this.appendMessage(msg),
4148
4370
  getMessages: () => this.binding.getMessages(),
@@ -4193,11 +4415,23 @@ class AparteChatHost {
4193
4415
  * spec guards ("optimistic-append + parent-push race"). `onMessageAppended`
4194
4416
  * is the append-specific signal instead.
4195
4417
  */
4196
- appendMessage(message) {
4418
+ appendMessage(message, options) {
4197
4419
  this._flushStreamState();
4198
4420
  if (message.role === "user") this._vp()?.setAutoScroll?.(true);
4199
- this.binding.setMessages([...this.binding.getMessages(), message]);
4200
- this.binding.onMessageAppended?.(message);
4421
+ const stored = options?.historical ? adoptMessageSegments(message) : message.segments?.length ? {
4422
+ ...message,
4423
+ segments: message.segments.reduce((acc, segment) => {
4424
+ acc.push(stampSegmentOnInsert(
4425
+ acc,
4426
+ segment,
4427
+ message.id,
4428
+ resolveConfig(this.binding.host).getSegmentDefaults(segment.type)
4429
+ ));
4430
+ return acc;
4431
+ }, [])
4432
+ } : message;
4433
+ this.binding.setMessages([...this.binding.getMessages(), stored]);
4434
+ this.binding.onMessageAppended?.(stored);
4201
4435
  }
4202
4436
  /** Atomic partial update of a message by id. */
4203
4437
  updateMessage(messageId, updates) {
@@ -4240,7 +4474,15 @@ class AparteChatHost {
4240
4474
  this.binding.onTypingChange?.(false);
4241
4475
  const msgs = before;
4242
4476
  const last = target;
4243
- const stamped = last ? stampSegmentOnInsert(last.segments || [], segment, last.id) : segment;
4477
+ const stamped = last ? stampSegmentOnInsert(
4478
+ last.segments || [],
4479
+ segment,
4480
+ last.id,
4481
+ // The host attaches its config to `binding.host`, so resolving from
4482
+ // that element is what gives a framework-managed chat its OWN
4483
+ // defaults rather than the page's.
4484
+ resolveConfig(this.binding.host).getSegmentDefaults(segment.type)
4485
+ ) : segment;
4244
4486
  this._lastBubble()?.addSegment?.(stamped);
4245
4487
  if (!last) return;
4246
4488
  const segments = [...last.segments || [], stamped];
@@ -4259,7 +4501,7 @@ class AparteChatHost {
4259
4501
  this._lastBubble()?.updateSegment?.(segmentId, stamped);
4260
4502
  if (!last || !last.segments) return;
4261
4503
  const segments = last.segments.map(
4262
- (s) => s.id === segmentId ? { ...s, ...stamped } : s
4504
+ (s) => s.id === segmentId ? mergeSegmentUpdate(s, stamped) : s
4263
4505
  );
4264
4506
  const next = [...msgs.slice(0, -1), { ...last, segments }];
4265
4507
  this.binding.setMessages(next);
@@ -6847,6 +7089,10 @@ function isAwaitingReply(message) {
6847
7089
  if (message.status === "streaming" || message.status === "pending") return true;
6848
7090
  return message.status === void 0 && message.role === "assistant" && !message.content?.trim() && !message.segments?.length;
6849
7091
  }
7092
+ let _relabels = null;
7093
+ function onRelabel(fn) {
7094
+ _relabels?.push(fn);
7095
+ }
6850
7096
  function el(tag, className, text) {
6851
7097
  const node = document.createElement(tag);
6852
7098
  if (className) node.className = className;
@@ -6903,12 +7149,20 @@ function buildEnumField(field, onChange, fallbackLabel) {
6903
7149
  control.value = "__other__";
6904
7150
  const body = el("span", "aparte-elic-option-body");
6905
7151
  const t = contextConfig();
6906
- body.appendChild(el("span", "aparte-elic-option-title", t.t("elicitationOther")));
7152
+ const otherTitle = el("span", "aparte-elic-option-title", t.t("elicitationOther"));
7153
+ body.appendChild(otherTitle);
6907
7154
  otherText = el("input", "aparte-elic-other-input");
6908
7155
  otherText.type = "text";
6909
7156
  otherText.placeholder = t.t("elicitationOtherPlaceholder");
6910
7157
  otherText.style.display = "none";
6911
7158
  otherText.setAttribute("aria-label", t.t("elicitationOtherLabel"));
7159
+ const otherInput = otherText;
7160
+ onRelabel(() => {
7161
+ const now = contextConfig();
7162
+ otherTitle.textContent = now.t("elicitationOther");
7163
+ otherInput.placeholder = now.t("elicitationOtherPlaceholder");
7164
+ otherInput.setAttribute("aria-label", now.t("elicitationOtherLabel"));
7165
+ });
6912
7166
  body.appendChild(otherText);
6913
7167
  row.append(control, body);
6914
7168
  list.appendChild(row);
@@ -6952,7 +7206,7 @@ function buildBooleanField(field, onChange, fallbackLabel) {
6952
7206
  const list = el("div", "aparte-elic-options");
6953
7207
  labelGroup(list, { titleId, fallbackLabel });
6954
7208
  const name = `elic-${uuid()}`;
6955
- const mk = (val, label) => {
7209
+ const mk = (val, label, defaulted) => {
6956
7210
  const row = el("label", "aparte-elic-option");
6957
7211
  const control = el("input", "aparte-elic-control");
6958
7212
  control.type = "radio";
@@ -6960,13 +7214,17 @@ function buildBooleanField(field, onChange, fallbackLabel) {
6960
7214
  control.value = val;
6961
7215
  if (field.default != null && String(field.default) === val) control.checked = true;
6962
7216
  const body = el("span", "aparte-elic-option-body");
6963
- body.appendChild(el("span", "aparte-elic-option-title", label));
7217
+ const title = el("span", "aparte-elic-option-title", label);
7218
+ if (defaulted) onRelabel(() => {
7219
+ title.textContent = contextConfig().t(defaulted);
7220
+ });
7221
+ body.appendChild(title);
6964
7222
  row.append(control, body);
6965
7223
  list.appendChild(row);
6966
7224
  };
6967
7225
  const t = contextConfig();
6968
- mk("true", field.trueLabel ?? t.t("elicitationYes"));
6969
- mk("false", field.falseLabel ?? t.t("elicitationNo"));
7226
+ mk("true", field.trueLabel ?? t.t("elicitationYes"), field.trueLabel ? void 0 : "elicitationYes");
7227
+ mk("false", field.falseLabel ?? t.t("elicitationNo"), field.falseLabel ? void 0 : "elicitationNo");
6970
7228
  wrap.appendChild(list);
6971
7229
  list.addEventListener("change", onChange);
6972
7230
  const checked = () => list.querySelector("input:checked");
@@ -6985,7 +7243,14 @@ function buildStringField(field, onChange, fallbackLabel) {
6985
7243
  if (field.placeholder) input.setAttribute("placeholder", field.placeholder);
6986
7244
  if (field.default) input.value = field.default;
6987
7245
  if (field.maxLength != null) input.setAttribute("maxlength", String(field.maxLength));
6988
- input.setAttribute("aria-label", field.title ?? field.description ?? fallbackLabel ?? contextConfig().t("elicitationAnswerLabel"));
7246
+ const ownName = field.title || field.description || fallbackLabel || void 0;
7247
+ input.setAttribute("aria-label", ownName ?? contextConfig().t("elicitationAnswerLabel"));
7248
+ if (ownName == null) {
7249
+ const named = input;
7250
+ onRelabel(() => {
7251
+ named.setAttribute("aria-label", contextConfig().t("elicitationAnswerLabel"));
7252
+ });
7253
+ }
6989
7254
  wrap.appendChild(input);
6990
7255
  input.addEventListener("input", onChange);
6991
7256
  const required = field.required ?? true;
@@ -7021,6 +7286,19 @@ function buildField(field, onChange, fallbackLabel, key) {
7021
7286
  }
7022
7287
  }
7023
7288
  function buildElicitationPanel(message, schema, onChange) {
7289
+ const relabels = [];
7290
+ const outer = _relabels;
7291
+ _relabels = relabels;
7292
+ try {
7293
+ const built = buildPanel(message, schema, onChange);
7294
+ return { ...built, relabel: () => {
7295
+ for (const fn of relabels) fn();
7296
+ } };
7297
+ } finally {
7298
+ _relabels = outer;
7299
+ }
7300
+ }
7301
+ function buildPanel(message, schema, onChange) {
7024
7302
  const panel = el("div", "aparte-elic-panel");
7025
7303
  if (message) panel.appendChild(el("p", "aparte-elic-message", message));
7026
7304
  const body = el("div", "aparte-elic-body");
@@ -7124,67 +7402,72 @@ function requestUserInput(request) {
7124
7402
  return resolveConfig(request.target ?? null).requestUserInput(request);
7125
7403
  }
7126
7404
  export {
7127
- segmentDuration as $,
7128
- APARTE_CONVERSATION_SCHEMA_VERSION as A,
7129
- createStreamAdapter as B,
7130
- cssEscape as C,
7131
- declineDefaultRenderers as D,
7132
- defaultSanitizer as E,
7133
- deriveArtifactKind as F,
7134
- detachConfig as G,
7135
- escapeAttr as H,
7136
- escapeHtml as I,
7137
- filesToAttachments as J,
7138
- getAllRenderers as K,
7139
- getSegmentRenderer as L,
7140
- installDefaultRenderersOnce as M,
7141
- isAwaitingReply as N,
7142
- isFormatAdapter as O,
7143
- isSafeUrl as P,
7144
- isSegmentSettled as Q,
7145
- parseAparteEventStream as R,
7146
- parseMarkdownToSegments as S,
7147
- populateBubbleFromMessage as T,
7148
- readableToAsyncIterable as U,
7149
- registerDefaultRenderers as V,
7150
- registerSegmentRenderer as W,
7151
- requestUserInput as X,
7152
- resolveConfig as Y,
7153
- revokeAttachmentUrls as Z,
7154
- runWithConfig as _,
7155
- APARTE_DEFAULT_BUBBLE_ACTIONS as a,
7156
- unregisterSegmentRenderer as a0,
7157
- uuid as a1,
7158
- stampSegmentActivity as a2,
7159
- stampSegmentOnInsert as a3,
7160
- stampSegmentOnUpdate as a4,
7161
- renumberSegments as a5,
7162
- openSegmentIds as a6,
7163
- isTerminalStatus as a7,
7164
- APARTE_DEFAULT_HOST_HANDLERS as b,
7165
- APARTE_DEFAULT_ICON_FALLBACKS as c,
7166
- APARTE_DEFAULT_LOCALE as d,
7167
- APARTE_DEFAULT_SKELETON_FALLBACKS as e,
7168
- APARTE_DEFAULT_UI_EVENTS as f,
7169
- APARTE_HOST_ATTR as g,
7170
- AparteBackendTransport as h,
7171
- AparteChatHost as i,
7172
- AparteClient as j,
7173
- AparteConfig as k,
7174
- AparteConversationController as l,
7175
- AparteConversationManager as m,
7176
- AparteDirectTransport as n,
7177
- AparteError as o,
7178
- AparteErrorCode as p,
7179
- AparteMessageRepository as q,
7180
- AparteStreamParser as r,
7181
- aparteGlobalConfig as s,
7182
- applyElementProps as t,
7183
- attachConfig as u,
7184
- buildElicitationPanel as v,
7185
- collectRendererStyles as w,
7186
- contentToText as x,
7187
- contextConfig as y,
7188
- createAparteChatHandler as z
7405
+ runWithConfig as $,
7406
+ APARTE_CONFIG_CHANGE as A,
7407
+ createAparteChatHandler as B,
7408
+ createStreamAdapter as C,
7409
+ cssEscape as D,
7410
+ declineDefaultRenderers as E,
7411
+ defaultSanitizer as F,
7412
+ deriveArtifactKind as G,
7413
+ detachConfig as H,
7414
+ escapeAttr as I,
7415
+ escapeHtml as J,
7416
+ filesToAttachments as K,
7417
+ getAllRenderers as L,
7418
+ getSegmentRenderer as M,
7419
+ installDefaultRenderersOnce as N,
7420
+ isAwaitingReply as O,
7421
+ isFormatAdapter as P,
7422
+ isSafeUrl as Q,
7423
+ isSegmentSettled as R,
7424
+ parseAparteEventStream as S,
7425
+ parseMarkdownToSegments as T,
7426
+ populateBubbleFromMessage as U,
7427
+ readableToAsyncIterable as V,
7428
+ registerDefaultRenderers as W,
7429
+ registerSegmentRenderer as X,
7430
+ requestUserInput as Y,
7431
+ resolveConfig as Z,
7432
+ revokeAttachmentUrls as _,
7433
+ APARTE_CONVERSATION_SCHEMA_VERSION as a,
7434
+ segmentDuration as a0,
7435
+ segmentTiming as a1,
7436
+ subscribeConfigChange as a2,
7437
+ unregisterSegmentRenderer as a3,
7438
+ uuid as a4,
7439
+ mergeSegmentUpdate as a5,
7440
+ stampSegmentActivity as a6,
7441
+ stampSegmentOnInsert as a7,
7442
+ stampSegmentOnUpdate as a8,
7443
+ renumberSegments as a9,
7444
+ openSegmentIds as aa,
7445
+ isTerminalStatus as ab,
7446
+ adoptMessageSegments as ac,
7447
+ APARTE_DEFAULT_BUBBLE_ACTIONS as b,
7448
+ APARTE_DEFAULT_HOST_HANDLERS as c,
7449
+ APARTE_DEFAULT_ICON_FALLBACKS as d,
7450
+ APARTE_DEFAULT_LOCALE as e,
7451
+ APARTE_DEFAULT_SKELETON_FALLBACKS as f,
7452
+ APARTE_DEFAULT_UI_EVENTS as g,
7453
+ APARTE_HOST_ATTR as h,
7454
+ AparteBackendTransport as i,
7455
+ AparteChatHost as j,
7456
+ AparteClient as k,
7457
+ AparteConfig as l,
7458
+ AparteConversationController as m,
7459
+ AparteConversationManager as n,
7460
+ AparteDirectTransport as o,
7461
+ AparteError as p,
7462
+ AparteErrorCode as q,
7463
+ AparteMessageRepository as r,
7464
+ AparteStreamParser as s,
7465
+ aparteGlobalConfig as t,
7466
+ applyElementProps as u,
7467
+ attachConfig as v,
7468
+ buildElicitationPanel as w,
7469
+ collectRendererStyles as x,
7470
+ contentToText as y,
7471
+ contextConfig as z
7189
7472
  };
7190
- //# sourceMappingURL=index-hQQljwfX.js.map
7473
+ //# sourceMappingURL=index-BnKBciDG.js.map