@aparte/core 0.8.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 (75) hide show
  1. package/dist/client/aparte-client.d.ts.map +1 -1
  2. package/dist/client/stream-adapter.d.ts.map +1 -1
  3. package/dist/client/xml-artifact-feed.d.ts.map +1 -1
  4. package/dist/components/bubble/aparte-chat-bubble.d.ts +24 -3
  5. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  6. package/dist/components/bubble/bubble-sync.d.ts.map +1 -1
  7. package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
  8. package/dist/components/composer/aparte-composer-add-attachment.d.ts +11 -0
  9. package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
  10. package/dist/components/composer/aparte-composer-cancel.d.ts +8 -0
  11. package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
  12. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  13. package/dist/components/composer/aparte-composer-send.d.ts +32 -0
  14. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  15. package/dist/components/elicitation/aparte-elicitation.d.ts +10 -0
  16. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  17. package/dist/components/viewport/aparte-chat-viewport.d.ts +13 -1
  18. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  19. package/dist/config/aparte-config.d.ts +34 -1
  20. package/dist/config/aparte-config.d.ts.map +1 -1
  21. package/dist/config/config-subscribe.d.ts +4 -0
  22. package/dist/config/config-subscribe.d.ts.map +1 -0
  23. package/dist/config/index.d.ts +1 -0
  24. package/dist/config/index.d.ts.map +1 -1
  25. package/dist/config/locale.d.ts +98 -0
  26. package/dist/config/locale.d.ts.map +1 -1
  27. package/dist/config/sanitize.d.ts.map +1 -1
  28. package/dist/custom-elements.json +7590 -6959
  29. package/dist/elicitation/panel.d.ts +13 -0
  30. package/dist/elicitation/panel.d.ts.map +1 -1
  31. package/dist/host/aparte-chat-host.d.ts +3 -1
  32. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  33. package/dist/{index-BieFbKeW.js → index-BnKBciDG.js} +642 -248
  34. package/dist/index-BnKBciDG.js.map +1 -0
  35. package/dist/index.css +147 -310
  36. package/dist/index.d.ts +3 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +281 -92
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.node.d.ts +3 -1
  41. package/dist/index.node.d.ts.map +1 -1
  42. package/dist/index.node.js +60 -55
  43. package/dist/index.node.js.map +1 -1
  44. package/dist/parsers/aparte-stream-parser.d.ts +25 -0
  45. package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
  46. package/dist/renderers/highlight-stream.d.ts +17 -0
  47. package/dist/renderers/highlight-stream.d.ts.map +1 -0
  48. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  49. package/dist/renderers/segments/artifact/binary-file.d.ts.map +1 -1
  50. package/dist/renderers/segments/artifact/card.d.ts.map +1 -1
  51. package/dist/renderers/segments/artifact/shared.d.ts +19 -2
  52. package/dist/renderers/segments/artifact/shared.d.ts.map +1 -1
  53. package/dist/renderers/segments/code.d.ts.map +1 -1
  54. package/dist/renderers/segments/error.d.ts.map +1 -1
  55. package/dist/renderers/segments/pipeline-waiting.d.ts.map +1 -1
  56. package/dist/renderers/segments/thinking.d.ts.map +1 -1
  57. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  58. package/dist/types/event-map.d.ts +1 -2
  59. package/dist/types/event-map.d.ts.map +1 -1
  60. package/dist/types/events.d.ts +0 -16
  61. package/dist/types/events.d.ts.map +1 -1
  62. package/dist/types/index.d.ts +2 -2
  63. package/dist/types/index.d.ts.map +1 -1
  64. package/dist/types/models.d.ts +1 -6
  65. package/dist/types/models.d.ts.map +1 -1
  66. package/dist/types/segments.d.ts +103 -51
  67. package/dist/types/segments.d.ts.map +1 -1
  68. package/dist/utils/segments.d.ts +244 -0
  69. package/dist/utils/segments.d.ts.map +1 -0
  70. package/package.json +1 -1
  71. package/dist/index-BieFbKeW.js.map +0 -1
  72. package/dist/renderers/segments/file-tree.d.ts +0 -3
  73. package/dist/renderers/segments/file-tree.d.ts.map +0 -1
  74. package/dist/renderers/segments/terminal.d.ts +0 -3
  75. 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...",
@@ -237,11 +247,11 @@ class AparteStreamParser {
237
247
  if (seg.type === "text") content = stripTrailingFence(content);
238
248
  seg.content = content;
239
249
  }
240
- finalSegments.push(this._state.activeSegment);
250
+ finalSegments.push(this._closed(this._state.activeSegment));
241
251
  this._state.activeSegment = null;
242
252
  this._state.buffer = "";
243
253
  } else if (this._state.buffer.trim()) {
244
- finalSegments.push(this._createTextSegment(stripTrailingFence(this._state.buffer)));
254
+ finalSegments.push(this._closed(this._createTextSegment(stripTrailingFence(this._state.buffer))));
245
255
  this._state.buffer = "";
246
256
  }
247
257
  return finalSegments;
@@ -311,7 +321,7 @@ class AparteStreamParser {
311
321
  }
312
322
  let segmentToEmit = null;
313
323
  if (this._state.activeSegment && this._state.activeSegment.type === "text") {
314
- segmentToEmit = this._state.activeSegment;
324
+ segmentToEmit = this._closed(this._state.activeSegment);
315
325
  this._state.activeSegment = null;
316
326
  }
317
327
  if (firstPattern.type === "code") {
@@ -360,7 +370,7 @@ class AparteStreamParser {
360
370
  if (this._state.activeSegment && "content" in this._state.activeSegment) {
361
371
  this._state.activeSegment.content += codeContent2;
362
372
  }
363
- const segment2 = this._state.activeSegment;
373
+ const segment2 = this._closed(this._state.activeSegment);
364
374
  this._state.mode = "text";
365
375
  this._state.activeSegment = null;
366
376
  return { segment: segment2, remaining: "" };
@@ -378,7 +388,7 @@ class AparteStreamParser {
378
388
  if (this._state.activeSegment && "content" in this._state.activeSegment) {
379
389
  this._state.activeSegment.content += codeContent;
380
390
  }
381
- const segment = this._state.activeSegment;
391
+ const segment = this._closed(this._state.activeSegment);
382
392
  this._state.mode = "text";
383
393
  this._state.activeSegment = null;
384
394
  const afterClose = buffer.slice(closeIndex + 4);
@@ -420,7 +430,7 @@ class AparteStreamParser {
420
430
  if (this._state.activeSegment && "content" in this._state.activeSegment) {
421
431
  this._state.activeSegment.content += thinkingContent;
422
432
  }
423
- const segment = this._state.activeSegment;
433
+ const segment = this._closed(this._state.activeSegment);
424
434
  this._state.mode = "text";
425
435
  this._state.activeSegment = null;
426
436
  this._state.thinkingEnd = void 0;
@@ -482,7 +492,7 @@ class AparteStreamParser {
482
492
  if (this._state.activeSegment && "content" in this._state.activeSegment) {
483
493
  this._state.activeSegment.content += tail;
484
494
  }
485
- const segment = this._state.activeSegment;
495
+ const segment = this._closed(this._state.activeSegment);
486
496
  this._state.mode = "text";
487
497
  this._state.activeSegment = null;
488
498
  return { segment, remaining: buffer.slice(closeIndex + endDelim.length) };
@@ -503,6 +513,34 @@ class AparteStreamParser {
503
513
  ++this._state.segmentCounter;
504
514
  return `${this._options.idPrefix}-${uuid()}`;
505
515
  }
516
+ /**
517
+ * Mark a segment the parser has just CLOSED.
518
+ *
519
+ * The end of a delimited segment is not a guess: the closing token IS the end.
520
+ * `</think>`, a closing fence, `</artifact>` — and, for a text run, the opening
521
+ * of whatever comes next. The parser computes that moment at six sites and used
522
+ * to drop it, emitting the finished segment with no flag saying so.
523
+ *
524
+ * What that cost: nothing downstream could tell a finished reasoning block from
525
+ * one still streaming, so the only remaining signal was the END OF THE TURN. A
526
+ * reader watched "Thinking" for as long as the answer took to stream — twenty
527
+ * seconds after the model had stopped thinking — and the Markdown flush and the
528
+ * highlight-on-settle waited exactly as long. Two workarounds were written
529
+ * before this one: close at the end of the message (wrong value), and close when
530
+ * the next segment opens (right value, wrong moment, and no help for a reply
531
+ * that is reasoning and nothing else).
532
+ *
533
+ * Both agent loops share this parser, so saying it here says it once.
534
+ *
535
+ * Not every end is in band, and the exceptions are honest: a text run that ends
536
+ * only because the STREAM ended has no delimiter — `finalize()` is its truth —
537
+ * and a `tool_call` never passes through here at all, since its end is its
538
+ * `status`.
539
+ */
540
+ _closed(segment) {
541
+ if (segment) segment.isStreaming = false;
542
+ return segment;
543
+ }
506
544
  _createTextSegment(content) {
507
545
  return {
508
546
  id: this._options.autoGenerateIds ? this._generateId() : "",
@@ -523,7 +561,6 @@ class AparteStreamParser {
523
561
  id: this._options.autoGenerateIds ? this._generateId() : "",
524
562
  type: "thinking",
525
563
  content,
526
- collapsed: false,
527
564
  label: "Thinking"
528
565
  };
529
566
  }
@@ -911,6 +948,9 @@ const SAFE_STYLE_PROPS = /* @__PURE__ */ new Set([
911
948
  "text-decoration-style",
912
949
  "white-space"
913
950
  ]);
951
+ function isSafeCustomProperty(prop) {
952
+ return prop.startsWith("--") && !prop.startsWith("--aparte-");
953
+ }
914
954
  function scrubStyle(value) {
915
955
  const kept = [];
916
956
  for (const decl of value.split(";")) {
@@ -919,7 +959,8 @@ function scrubStyle(value) {
919
959
  if (idx === -1) continue;
920
960
  const prop = trimmed.slice(0, idx).trim().toLowerCase();
921
961
  const val = trimmed.slice(idx + 1);
922
- if (!val.trim() || !SAFE_STYLE_PROPS.has(prop)) continue;
962
+ if (!val.trim()) continue;
963
+ if (!SAFE_STYLE_PROPS.has(prop) && !isSafeCustomProperty(prop)) continue;
923
964
  if (val.includes("\\")) continue;
924
965
  if (/url\s*\(|expression\s*\(|javascript:|vbscript:|[<>]/i.test(val)) continue;
925
966
  kept.push(trimmed);
@@ -1006,7 +1047,6 @@ const APARTE_DEFAULT_BUBBLE_ACTIONS = {
1006
1047
  };
1007
1048
  const APARTE_DEFAULT_HOST_HANDLERS = {
1008
1049
  attachmentPreview: false,
1009
- terminalRun: false,
1010
1050
  artifactRedownload: false,
1011
1051
  artifactRehydrate: false
1012
1052
  };
@@ -1069,6 +1109,7 @@ class AparteConfig {
1069
1109
  // Tool Registry
1070
1110
  _tools = /* @__PURE__ */ new Map();
1071
1111
  _toolRenderers = /* @__PURE__ */ new Map();
1112
+ _segmentDefaults = /* @__PURE__ */ new Map();
1072
1113
  // Host handlers — what the app declares it can actually complete.
1073
1114
  _hostHandlers = { ...APARTE_DEFAULT_HOST_HANDLERS };
1074
1115
  // Bubble Actions — APARTE_DEFAULT_BUBBLE_ACTIONS is the single source of truth
@@ -1149,7 +1190,6 @@ class AparteConfig {
1149
1190
  getHostHandlers() {
1150
1191
  return {
1151
1192
  attachmentPreview: this._hostHandlers.attachmentPreview ?? APARTE_DEFAULT_HOST_HANDLERS.attachmentPreview,
1152
- terminalRun: this._hostHandlers.terminalRun ?? APARTE_DEFAULT_HOST_HANDLERS.terminalRun,
1153
1193
  artifactRedownload: this._hostHandlers.artifactRedownload ?? APARTE_DEFAULT_HOST_HANDLERS.artifactRedownload,
1154
1194
  artifactRehydrate: this._hostHandlers.artifactRehydrate ?? APARTE_DEFAULT_HOST_HANDLERS.artifactRehydrate
1155
1195
  };
@@ -1776,6 +1816,44 @@ class AparteConfig {
1776
1816
  registerToolRenderer(toolName, renderer) {
1777
1817
  this._toolRenderers.set(toolName, renderer);
1778
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
+ }
1779
1857
  /** Unregister a per-tool renderer */
1780
1858
  unregisterToolRenderer(toolName) {
1781
1859
  this._toolRenderers.delete(toolName);
@@ -1860,6 +1938,7 @@ class AparteConfig {
1860
1938
  this._siblingNavRenderer = void 0;
1861
1939
  this._bubbleShellRenderer = void 0;
1862
1940
  this._iconProvider = void 0;
1941
+ this._segmentDefaults.clear();
1863
1942
  this._avatarProvider = void 0;
1864
1943
  this._artifactPreviewBuilder = void 0;
1865
1944
  this._keyProvider = void 0;
@@ -1955,6 +2034,16 @@ function runWithConfig(config, fn) {
1955
2034
  function contextConfig(el2) {
1956
2035
  return _renderConfig ?? resolveConfig(el2);
1957
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
+ }
1958
2047
  const APARTE_DEFAULT_SKELETON_FALLBACKS = {
1959
2048
  message: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading...</div>',
1960
2049
  code: '<div class="aparte-skeleton-fallback" style="padding:16px;background:#1e293b;color:#64748b;border-radius:8px;">Loading code...</div>',
@@ -2035,7 +2124,15 @@ const thinkingRenderer = {
2035
2124
  * new class of risk. With no Markdown provider registered it degrades to the
2036
2125
  * zero-dependency default (escape + `<br>`), which is what it used to be.
2037
2126
  */
2038
- 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
+ },
2039
2136
  update: (el2, segment) => {
2040
2137
  const contentEl = el2.querySelector(".thinking-content");
2041
2138
  if (!contentEl) return;
@@ -2047,6 +2144,53 @@ const thinkingRenderer = {
2047
2144
  },
2048
2145
  getStyles: () => ``
2049
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
+ }
2050
2194
  const codeRenderer = {
2051
2195
  type: "code",
2052
2196
  render: (segment) => `
@@ -2063,6 +2207,13 @@ const codeRenderer = {
2063
2207
  </div>
2064
2208
  </div>
2065
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
+ },
2066
2217
  setup: (element, segment) => {
2067
2218
  const wrapper = element.querySelector(".code-content-wrapper");
2068
2219
  if (wrapper) {
@@ -2074,26 +2225,23 @@ const codeRenderer = {
2074
2225
  const copyBtn = element.querySelector(".code-copy");
2075
2226
  if (copyBtn) {
2076
2227
  copyBtn.addEventListener("click", () => {
2077
- void navigator.clipboard.writeText(segment.content || "").catch(() => {
2228
+ const source = element.querySelector(".code-content-wrapper")?.textContent ?? "";
2229
+ void navigator.clipboard.writeText(source).catch(() => {
2078
2230
  });
2079
2231
  copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
2080
2232
  copyBtn.setAttribute("title", contextConfig(copyBtn).t("copied"));
2233
+ copyBtn.dataset.copied = "1";
2081
2234
  setTimeout(() => {
2082
2235
  copyBtn.innerHTML = contextConfig(copyBtn).getIcon("copy");
2083
2236
  copyBtn.setAttribute("title", contextConfig(copyBtn).t("copy"));
2237
+ delete copyBtn.dataset.copied;
2084
2238
  }, 1500);
2085
2239
  });
2086
2240
  }
2087
2241
  },
2088
2242
  update: (element, segment) => {
2089
2243
  if (segment.isStreaming) {
2090
- const codeEl = element.querySelector(".code-content-wrapper code");
2091
- if (codeEl) {
2092
- codeEl.textContent = segment.content;
2093
- } else {
2094
- const wrapper = element.querySelector(".code-content-wrapper");
2095
- if (wrapper) wrapper.innerHTML = `<pre><code class="language-${escapeHtml(segment.language || "text")}">${escapeHtml(segment.content)}</code></pre>`;
2096
- }
2244
+ streamHighlight(element, ".code-content-wrapper", segment.content, segment.language || "", segment.id);
2097
2245
  } else {
2098
2246
  const wrapper = element.querySelector(".code-content-wrapper");
2099
2247
  if (wrapper) {
@@ -2106,58 +2254,6 @@ const codeRenderer = {
2106
2254
  },
2107
2255
  getStyles: () => ``
2108
2256
  };
2109
- const terminalRenderer = {
2110
- type: "terminal",
2111
- render: (segment) => `
2112
- <div class="segment segment-terminal" data-segment-id="${escapeHtml(segment.id)}">
2113
- <div class="terminal-command-block">
2114
- <div class="terminal-icon">
2115
- ${contextConfig().getIcon("terminal")}
2116
- </div>
2117
- <code class="terminal-command">${escapeHtml(segment.command || "")}</code>
2118
- <div class="terminal-actions">
2119
- ${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>` : ""}
2120
- <button class="terminal-copy-btn" data-action="copy" aria-label="${escapeAttr(contextConfig().t("copy"))}" title="${escapeAttr(contextConfig().t("copy"))}">
2121
- ${contextConfig().getIcon("copy")}
2122
- </button>
2123
- </div>
2124
- </div>
2125
- ${segment.output ? `<div class="terminal-output">${escapeHtml(segment.output)}</div>` : ""}
2126
- ${segment.exitCode !== void 0 && segment.exitCode !== 0 ? `<div class="terminal-error">Command failed with exit code ${segment.exitCode}</div>` : ""}
2127
- </div>
2128
- `,
2129
- setup: (element) => {
2130
- const copyBtn = element.querySelector(".terminal-copy-btn");
2131
- const command = element.querySelector(".terminal-command");
2132
- if (copyBtn && command) {
2133
- copyBtn.addEventListener("click", () => {
2134
- void navigator.clipboard.writeText(command.textContent || "").catch(() => {
2135
- });
2136
- copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
2137
- copyBtn.setAttribute("title", contextConfig(copyBtn).t("copied"));
2138
- setTimeout(() => {
2139
- copyBtn.innerHTML = contextConfig(copyBtn).getIcon("copy");
2140
- copyBtn.setAttribute("title", contextConfig(copyBtn).t("copy"));
2141
- }, 1500);
2142
- });
2143
- }
2144
- const runBtn = element.querySelector(".terminal-run-btn");
2145
- if (runBtn) {
2146
- runBtn.addEventListener("click", () => {
2147
- const segmentId = element.getAttribute("data-segment-id");
2148
- element.dispatchEvent(new CustomEvent("aparte-terminal-run", {
2149
- bubbles: true,
2150
- composed: true,
2151
- detail: {
2152
- segmentId,
2153
- command: command?.textContent || ""
2154
- }
2155
- }));
2156
- });
2157
- }
2158
- },
2159
- getStyles: () => ``
2160
- };
2161
2257
  const errorRenderer = {
2162
2258
  type: "error",
2163
2259
  render: (segment) => {
@@ -2176,13 +2272,28 @@ const errorRenderer = {
2176
2272
  ${contextConfig().getIcon("error") || "⚠"}
2177
2273
  </div>
2178
2274
  <div class="error-content">
2179
- <div class="error-title">Error</div>
2275
+ <div class="error-title">${escapeHtml(contextConfig().t("error"))}</div>
2180
2276
  <div class="error-message">${escapeHtml(segment.content)}</div>
2181
2277
  ${segment.details ? `<div class="error-details">${escapeHtml(segment.details)}</div>` : ""}
2182
2278
  </div>
2183
2279
  </div>
2184
2280
  `;
2185
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
+ },
2186
2297
  getStyles: () => ``
2187
2298
  };
2188
2299
  const progressRenderer = {
@@ -2194,31 +2305,6 @@ const progressRenderer = {
2194
2305
  },
2195
2306
  getStyles: () => ``
2196
2307
  };
2197
- function renderFileNode(node, depth = 0) {
2198
- const indent = depth * 16;
2199
- const icon = node.type === "directory" ? "📁" : "📄";
2200
- const statusClass = node.status ? `file-status-${escapeHtml(node.status)}` : "";
2201
- 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>`;
2202
- if (node.children) {
2203
- for (const child of node.children) {
2204
- html += renderFileNode(child, depth + 1);
2205
- }
2206
- }
2207
- return html;
2208
- }
2209
- const fileTreeRenderer = {
2210
- type: "file-tree",
2211
- render: (segment) => {
2212
- let filesHtml = "";
2213
- if (segment.files) {
2214
- for (const file of segment.files) {
2215
- filesHtml += renderFileNode(file, 0);
2216
- }
2217
- }
2218
- 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>`;
2219
- },
2220
- getStyles: () => ``
2221
- };
2222
2308
  const toolCallRenderer = {
2223
2309
  type: "tool_call",
2224
2310
  render: (segment) => {
@@ -2260,6 +2346,39 @@ const toolCallRenderer = {
2260
2346
  </div>
2261
2347
  `;
2262
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
+ },
2263
2382
  setup: (element, segment) => {
2264
2383
  if (segment.status === "awaiting-approval") {
2265
2384
  const toolCallId = segment.toolCall?.id;
@@ -2326,11 +2445,12 @@ const toolCallRenderer = {
2326
2445
  @keyframes tool-spin { to { transform: rotate(360deg); } }
2327
2446
  `
2328
2447
  };
2448
+ const waitingLabel = () => contextConfig().t("generating");
2329
2449
  const pipelineWaitingRenderer = {
2330
2450
  type: "pipeline-waiting",
2331
2451
  render: (segment) => {
2332
2452
  return `
2333
- <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">
2334
2454
  <span class="pw-dot"></span>
2335
2455
  <span class="pw-dot"></span>
2336
2456
  <span class="pw-dot"></span>
@@ -2338,6 +2458,15 @@ const pipelineWaitingRenderer = {
2338
2458
  },
2339
2459
  update: () => {
2340
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
+ },
2341
2470
  setup: (el2) => {
2342
2471
  const parent = el2.parentElement;
2343
2472
  if (!parent) return;
@@ -2422,26 +2551,6 @@ function findClosingFence(s) {
2422
2551
  }
2423
2552
  return -1;
2424
2553
  }
2425
- const _lastHighlightAt = /* @__PURE__ */ new Map();
2426
- function markThrottle(map, id, at) {
2427
- map.delete(id);
2428
- if (map.size >= MAX_THROTTLE_ENTRIES) {
2429
- const oldest = map.keys().next().value;
2430
- if (oldest !== void 0) map.delete(oldest);
2431
- }
2432
- map.set(id, at);
2433
- }
2434
- function debounceHighlight(element, paneSelector, content, lang, segId) {
2435
- const now = Date.now();
2436
- const last = _lastHighlightAt.get(segId) ?? 0;
2437
- if (now - last < HIGHLIGHT_DEBOUNCE_MS) return;
2438
- markThrottle(_lastHighlightAt, segId, now);
2439
- void contextConfig(element).highlightCode(content, lang).then((html) => {
2440
- const wrapper = element.querySelector(paneSelector);
2441
- if (wrapper) wrapper.innerHTML = html;
2442
- }).catch(() => {
2443
- });
2444
- }
2445
2554
  function labelForKind(kind) {
2446
2555
  switch (kind) {
2447
2556
  case "react":
@@ -2480,8 +2589,6 @@ function labelForKind(kind) {
2480
2589
  return "Artifact";
2481
2590
  }
2482
2591
  }
2483
- const MAX_THROTTLE_ENTRIES = 256;
2484
- const HIGHLIGHT_DEBOUNCE_MS = 400;
2485
2592
  const PREVIEW_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:";
2486
2593
  function withMetaCsp(doc) {
2487
2594
  const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttr(PREVIEW_CSP)}">`;
@@ -2498,9 +2605,18 @@ function buildPreviewBody(kind, body, title) {
2498
2605
  if (startsWithDoctype(body)) return body;
2499
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>`;
2500
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.
2501
2617
  case "svg":
2502
2618
  return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
2503
- <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>
2504
2620
  </head><body>${body}</body></html>`;
2505
2621
  case "js": {
2506
2622
  const safeBody = escapeClosingScriptTag(body);
@@ -2583,6 +2699,7 @@ function renderBinaryFileArtifact(segment, kind) {
2583
2699
  const iconLabel = FILE_ICON_LABEL[kind] ?? kind.toUpperCase();
2584
2700
  const isStreaming = !!segment.isStreaming;
2585
2701
  const canRedownload = contextConfig().getHostHandlers().artifactRedownload;
2702
+ const downloadLabel = escapeHtml(contextConfig().t("download"));
2586
2703
  const cached = _binaryArtifactCache.get(segment.id);
2587
2704
  if (cached && !isStreaming) {
2588
2705
  const previewBody = cached.previewHtml ? contextConfig().sanitizeHtml(cached.previewHtml) : `<div class="aparte-art-file__preview-empty">Preview not available for ${escapeHtml(kind)} yet</div>`;
@@ -2598,7 +2715,7 @@ function renderBinaryFileArtifact(segment, kind) {
2598
2715
  <div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(formatBytes(cached.bytes))} · ${escapeHtml(kind.toUpperCase())}</div>
2599
2716
  </div>
2600
2717
  <div class="aparte-art-file__actions">
2601
- ${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>` : ""}
2602
2719
  </div>
2603
2720
  </div>
2604
2721
  <div class="aparte-art-file__body">
@@ -2611,7 +2728,7 @@ function renderBinaryFileArtifact(segment, kind) {
2611
2728
  `;
2612
2729
  }
2613
2730
  const cleanContent = stripCodeFences(segment.content || "");
2614
- const subText = isStreaming ? "Generating…" : "Rebuilding preview…";
2731
+ const subText = isStreaming ? contextConfig().t("generating") : contextConfig().t("rebuildingPreview");
2615
2732
  return `
2616
2733
  <div class="segment segment-artifact-file"
2617
2734
  data-segment-id="${escapeHtml(segment.id)}"
@@ -2624,7 +2741,7 @@ function renderBinaryFileArtifact(segment, kind) {
2624
2741
  <div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(subText)}</div>
2625
2742
  </div>
2626
2743
  <div class="aparte-art-file__actions">
2627
- ${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>` : ""}
2628
2745
  </div>
2629
2746
  </div>
2630
2747
  <div class="aparte-art-file__body">
@@ -2730,12 +2847,11 @@ function updateBinaryFileArtifact(element, segment, isStreaming) {
2730
2847
  const state = element.getAttribute("data-state");
2731
2848
  if (state === "ready") return;
2732
2849
  const cleanContent = stripCodeFences(segment.content || "");
2733
- const codeEl = element.querySelector('[data-role="code-pane"] code');
2734
- if (codeEl) {
2735
- codeEl.textContent = cleanContent;
2736
- }
2737
2850
  if (isStreaming) {
2738
- 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;
2739
2855
  }
2740
2856
  if (!isStreaming && state === "streaming") {
2741
2857
  element.setAttribute("data-state", "compiling");
@@ -2782,9 +2898,9 @@ function showSandboxError(element, phase, errorMsg) {
2782
2898
  const short = (errorMsg.split("\n")[0] ?? "").slice(0, 240);
2783
2899
  body.innerHTML = `
2784
2900
  <div class="aparte-art-file__error">
2785
- <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>
2786
2902
  <div class="aparte-art-file__error-msg">${escapeHtml(short)}</div>
2787
- <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>
2788
2904
  </div>
2789
2905
  `;
2790
2906
  }
@@ -2802,6 +2918,10 @@ const artifactRenderer = {
2802
2918
  const displayLang = languageForKind(kind);
2803
2919
  const isStreaming = !!segment.isStreaming;
2804
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");
2805
2925
  const isBinary = BINARY_FILE_KINDS.has(kind);
2806
2926
  const cleanContent = stripCodeFences(segment.content || "");
2807
2927
  return `
@@ -2819,17 +2939,17 @@ const artifactRenderer = {
2819
2939
  ${isStreaming ? '<span class="aparte-art-card__pulse" aria-label="Streaming"></span>' : ""}
2820
2940
  </div>
2821
2941
  <div class="aparte-art-card__actions">
2822
- <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"))}">
2823
2943
  ${contextConfig().getIcon("copy")}
2824
2944
  </button>
2825
- <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" : ""}>
2826
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>
2827
2947
  </button>
2828
2948
  </div>
2829
2949
  </header>
2830
2950
  <nav class="aparte-art-card__tabs" role="tablist">
2831
- ${previewable ? `<button type="button" role="tab" data-tab-target="preview" aria-selected="false" ${isStreaming ? "disabled" : ""}>Preview</button>` : ""}
2832
- <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>` : ""}
2833
2953
  </nav>
2834
2954
  <div class="aparte-art-card__body">
2835
2955
  <div class="aparte-art-card__pane" data-pane="code">
@@ -2839,13 +2959,44 @@ const artifactRenderer = {
2839
2959
  </div>
2840
2960
  ${previewable ? `
2841
2961
  <div class="aparte-art-card__pane" data-pane="preview">
2842
- <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>
2843
2963
  </div>
2844
2964
  ` : ""}
2845
2965
  </div>
2846
2966
  </div>
2847
2967
  `;
2848
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
+ },
2849
3000
  setup: (element, segment) => {
2850
3001
  latestSegment.set(element, segment);
2851
3002
  const kind = (segment.artifactType || "").toLowerCase();
@@ -2926,22 +3077,24 @@ const artifactRenderer = {
2926
3077
  }
2927
3078
  const wasStreaming = element.getAttribute("data-streaming") === "true";
2928
3079
  const cleanContent = stripCodeFences(segment.content || "");
2929
- const codeEl = element.querySelector(".code-content-wrapper code");
2930
- if (codeEl) {
2931
- codeEl.textContent = cleanContent;
2932
- } else {
2933
- const wrapper = element.querySelector(".code-content-wrapper");
2934
- if (wrapper) {
2935
- const displayLang = languageForKind(kind);
2936
- wrapper.innerHTML = `<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>`;
2937
- }
2938
- }
2939
3080
  if (isStreaming) {
2940
3081
  const segId = element.getAttribute("data-segment-id") ?? segment.id;
2941
- 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
+ }
2942
3094
  }
2943
3095
  if (wasStreaming && !isStreaming) {
2944
3096
  element.setAttribute("data-streaming", "false");
3097
+ element.querySelector(".aparte-art-card__pulse")?.remove();
2945
3098
  const wrapper = element.querySelector(".code-content-wrapper");
2946
3099
  if (wrapper) {
2947
3100
  const displayLang = languageForKind(kind);
@@ -3013,7 +3166,24 @@ const artifactRenderer = {
3013
3166
  }
3014
3167
  .aparte-art-card__btn:disabled { opacity: 0.4; cursor: not-allowed; }
3015
3168
  .aparte-art-card__tabs {
3016
- 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;
3017
3187
  padding: 4px 8px 0;
3018
3188
  border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
3019
3189
  background: var(--aparte-surface-2, rgba(0,0,0,0.02));
@@ -3036,17 +3206,37 @@ const artifactRenderer = {
3036
3206
  font-weight: 500;
3037
3207
  }
3038
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. */
3039
3226
  .aparte-art-card__body {
3040
3227
  position: relative;
3041
- min-height: 80px;
3042
- 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);
3043
3232
  overflow: hidden;
3044
3233
  }
3045
3234
  .aparte-art-card__pane { display: none; height: 100%; }
3046
3235
  .segment-artifact-card[data-tab="code"] .aparte-art-card__pane[data-pane="code"] { display: block; }
3047
3236
  .segment-artifact-card[data-tab="preview"] .aparte-art-card__pane[data-pane="preview"] { display: block; }
3048
3237
  .aparte-art-card__pane[data-pane="code"] {
3049
- 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;
3050
3240
  }
3051
3241
  .aparte-art-card__pane[data-pane="code"] pre {
3052
3242
  margin: 0; padding: 12px;
@@ -3055,13 +3245,14 @@ const artifactRenderer = {
3055
3245
  }
3056
3246
  .aparte-art-card__frame {
3057
3247
  display: block;
3058
- width: 100%; height: 480px;
3248
+ width: 100%;
3249
+ height: min(var(--aparte-artifact-frame-height, 480px), var(--aparte-artifact-frame-max, 70vh));
3059
3250
  border: 0;
3060
3251
  background: #fff;
3061
3252
  }
3062
3253
  .aparte-art-card__pending {
3063
3254
  display: flex; align-items: center; justify-content: center;
3064
- height: 120px;
3255
+ height: var(--aparte-artifact-pending-height, 120px);
3065
3256
  color: var(--aparte-text-muted, rgba(0,0,0,0.5));
3066
3257
  font-size: 0.85rem;
3067
3258
  font-style: italic;
@@ -3087,7 +3278,7 @@ const artifactRenderer = {
3087
3278
  position: relative;
3088
3279
  }
3089
3280
  .aparte-art-file__code-pane {
3090
- max-height: 360px; overflow: auto;
3281
+ max-height: var(--aparte-artifact-file-code-max, 360px); overflow: auto;
3091
3282
  background: var(--aparte-code-bg, #fafafa);
3092
3283
  }
3093
3284
  .aparte-art-file__code-pane pre {
@@ -3096,7 +3287,7 @@ const artifactRenderer = {
3096
3287
  font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
3097
3288
  }
3098
3289
  .aparte-art-file__preview-pane {
3099
- max-height: 460px;
3290
+ max-height: var(--aparte-artifact-file-preview-max, 460px);
3100
3291
  overflow: auto;
3101
3292
  background: #fff;
3102
3293
  /* Preview is a document view — force light scheme regardless of
@@ -3344,27 +3535,140 @@ function injectRendererStyles(config = contextConfig()) {
3344
3535
  if (added) styleElement.textContent = Array.from(injectedRendererStyles).join("\n");
3345
3536
  }
3346
3537
  function registerDefaultRenderers(config = contextConfig()) {
3347
- registryFor(config).defaultsInstalled = true;
3348
- for (const renderer of DEFAULT_RENDERERS) registerSegmentRenderer(renderer, config);
3538
+ const reg = registryFor(config);
3539
+ reg.defaultsInstalled = true;
3540
+ for (const renderer of DEFAULT_RENDERERS) {
3541
+ if (!reg.renderers.has(renderer.type)) registerSegmentRenderer(renderer, config);
3542
+ }
3543
+ injectRendererStyles(config);
3349
3544
  installArtifactReadyHook();
3350
3545
  }
3351
3546
  const DEFAULT_RENDERERS = [
3352
3547
  textRenderer,
3353
3548
  thinkingRenderer,
3354
3549
  codeRenderer,
3355
- terminalRenderer,
3356
3550
  errorRenderer,
3357
3551
  progressRenderer,
3358
- fileTreeRenderer,
3359
3552
  toolCallRenderer,
3360
3553
  artifactRenderer,
3361
3554
  pipelineWaitingRenderer
3362
3555
  ];
3556
+ const OPEN_TOOL_STATUSES = /* @__PURE__ */ new Set(["pending", "awaiting-approval"]);
3557
+ const ACTIVITY_KEYS = ["content", "output", "result"];
3558
+ function isActivity(updates) {
3559
+ return ACTIVITY_KEYS.some((key) => key in updates);
3560
+ }
3561
+ function stampSegmentOnInsert(segments, segment, messageId, defaults) {
3562
+ const base = applyDefaults(segment, defaults);
3563
+ return {
3564
+ ...base,
3565
+ messageId: segment.messageId ?? messageId,
3566
+ index: segment.index ?? segments.length,
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() })
3571
+ };
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
+ }
3602
+ function isSegmentSettled(segment) {
3603
+ if (segment.type === "tool_call") return !OPEN_TOOL_STATUSES.has(segment.status);
3604
+ return segment.isStreaming === false;
3605
+ }
3606
+ function stampSegmentOnUpdate(segment, updates) {
3607
+ if (isSegmentSettled(segment)) return updates;
3608
+ const merged = { ...segment, ...updates };
3609
+ if (isSegmentSettled(merged)) {
3610
+ const ended = segmentTiming(segment)?.endedAt;
3611
+ return { ...updates, ...timingPatch(segment, { endedAt: ended ?? Date.now() }) };
3612
+ }
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
+ };
3628
+ }
3629
+ function segmentDuration(segment) {
3630
+ const { startedAt, endedAt } = segmentTiming(segment) ?? {};
3631
+ if (startedAt == null || endedAt == null) return void 0;
3632
+ return endedAt - startedAt;
3633
+ }
3634
+ function stampSegmentActivity(segment) {
3635
+ return isSegmentSettled(segment) ? {} : timingPatch(segment, { endedAt: Date.now() });
3636
+ }
3637
+ function segmentContentUpdate(segment) {
3638
+ const content = segment.content;
3639
+ return {
3640
+ content,
3641
+ ...segment.isStreaming === false ? { isStreaming: false } : {}
3642
+ };
3643
+ }
3644
+ function isTerminalStatus(status) {
3645
+ return status != null && status !== "streaming" && status !== "pending";
3646
+ }
3647
+ function openSegmentIds(segments) {
3648
+ return segments.filter((segment) => !isSegmentSettled(segment)).map((segment) => segment.id);
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
+ }
3660
+ function renumberSegments(segments) {
3661
+ for (let i = 0; i < segments.length; i++) {
3662
+ const segment = segments[i];
3663
+ if (segment.index === i) continue;
3664
+ segments[i] = { ...segment, index: i };
3665
+ }
3666
+ }
3363
3667
  function populateBubbleFromMessage(bubble, message, siblingInfo) {
3364
3668
  if (message.segments && message.segments.length > 0) {
3365
3669
  const existing = bubble.getSegments?.() ?? [];
3366
3670
  if (existing.length === message.segments.length) {
3367
- const settled = message.status != null && message.status !== "streaming" && message.status !== "pending";
3671
+ const settled = isTerminalStatus(message.status);
3368
3672
  for (const seg of message.segments) {
3369
3673
  if ("content" in seg) {
3370
3674
  const patch = { content: seg.content };
@@ -4053,10 +4357,14 @@ class AparteChatHost {
4053
4357
  // Controller-driven flushes mirror AparteConversationManager state, not
4054
4358
  // user intent — deliberately NOT emitted as `messagesChange` to
4055
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.
4056
4364
  setMessages: (msgs) => {
4057
4365
  this._beginConversationSwap();
4058
4366
  this._vp()?.resetSpacer?.();
4059
- this.binding.setMessages([...msgs]);
4367
+ this.binding.setMessages(msgs.map(adoptMessageSegments));
4060
4368
  },
4061
4369
  appendMessage: (msg) => this.appendMessage(msg),
4062
4370
  getMessages: () => this.binding.getMessages(),
@@ -4107,11 +4415,23 @@ class AparteChatHost {
4107
4415
  * spec guards ("optimistic-append + parent-push race"). `onMessageAppended`
4108
4416
  * is the append-specific signal instead.
4109
4417
  */
4110
- appendMessage(message) {
4418
+ appendMessage(message, options) {
4111
4419
  this._flushStreamState();
4112
4420
  if (message.role === "user") this._vp()?.setAutoScroll?.(true);
4113
- this.binding.setMessages([...this.binding.getMessages(), message]);
4114
- 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);
4115
4435
  }
4116
4436
  /** Atomic partial update of a message by id. */
4117
4437
  updateMessage(messageId, updates) {
@@ -4120,12 +4440,18 @@ class AparteChatHost {
4120
4440
  const index = msgs.findIndex((m) => m.id === messageId);
4121
4441
  if (index === -1) return;
4122
4442
  const next = [...msgs];
4123
- next[index] = { ...next[index], ...updates };
4443
+ const merged = { ...next[index], ...updates };
4444
+ next[index] = merged;
4124
4445
  this.binding.setMessages(next);
4125
4446
  this.binding.onMessagesChange?.(next);
4126
4447
  if (updates.status) {
4127
4448
  this._bubbleById(messageId)?.updateMessage?.({ status: updates.status });
4128
4449
  }
4450
+ if (isTerminalStatus(merged.status) && merged.segments) {
4451
+ for (const id of openSegmentIds(merged.segments)) {
4452
+ this.updateSegment(id, { isStreaming: false });
4453
+ }
4454
+ }
4129
4455
  }
4130
4456
  /** Append/replace the last message content (streaming text). */
4131
4457
  updateLastMessage(content, options) {
@@ -4142,13 +4468,24 @@ class AparteChatHost {
4142
4468
  /** Add a segment to the last message (immediate bubble + state sync). */
4143
4469
  addSegment(segment) {
4144
4470
  this._flushStreamState();
4145
- const msgs = this.binding.getMessages();
4146
- const last = msgs[msgs.length - 1];
4147
- if (last && this._isOrphan(last.id)) return;
4471
+ const before = this.binding.getMessages();
4472
+ const target = before[before.length - 1];
4473
+ if (target && this._isOrphan(target.id)) return;
4148
4474
  this.binding.onTypingChange?.(false);
4149
- this._lastBubble()?.addSegment?.(segment);
4475
+ const msgs = before;
4476
+ const last = target;
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;
4486
+ this._lastBubble()?.addSegment?.(stamped);
4150
4487
  if (!last) return;
4151
- const segments = [...last.segments || [], segment];
4488
+ const segments = [...last.segments || [], stamped];
4152
4489
  const next = [...msgs.slice(0, -1), { ...last, segments }];
4153
4490
  this.binding.setMessages(next);
4154
4491
  this.binding.onMessagesChange?.(next);
@@ -4159,10 +4496,12 @@ class AparteChatHost {
4159
4496
  const msgs = this.binding.getMessages();
4160
4497
  const last = msgs[msgs.length - 1];
4161
4498
  if (last && this._isOrphan(last.id)) return;
4162
- this._lastBubble()?.updateSegment?.(segmentId, updates);
4499
+ const current = last?.segments?.find((s) => s.id === segmentId);
4500
+ const stamped = current ? stampSegmentOnUpdate(current, updates) : updates;
4501
+ this._lastBubble()?.updateSegment?.(segmentId, stamped);
4163
4502
  if (!last || !last.segments) return;
4164
4503
  const segments = last.segments.map(
4165
- (s) => s.id === segmentId ? { ...s, ...updates } : s
4504
+ (s) => s.id === segmentId ? mergeSegmentUpdate(s, stamped) : s
4166
4505
  );
4167
4506
  const next = [...msgs.slice(0, -1), { ...last, segments }];
4168
4507
  this.binding.setMessages(next);
@@ -4177,6 +4516,7 @@ class AparteChatHost {
4177
4516
  if (!last?.segments) return;
4178
4517
  const segments = last.segments.filter((s) => s.id !== segmentId);
4179
4518
  if (segments.length === last.segments.length) return;
4519
+ renumberSegments(segments);
4180
4520
  const next = [...msgs.slice(0, -1), { ...last, segments }];
4181
4521
  this.binding.setMessages(next);
4182
4522
  this.binding.onMessagesChange?.(next);
@@ -4258,7 +4598,12 @@ class AparteChatHost {
4258
4598
  if (last?.segments) {
4259
4599
  const segments = last.segments.map((s) => {
4260
4600
  const buffered = pending.get(s.id);
4261
- return buffered !== void 0 && "content" in s ? { ...s, content: buffered.base + buffered.text } : s;
4601
+ if (buffered === void 0 || !("content" in s)) return s;
4602
+ return {
4603
+ ...s,
4604
+ content: buffered.base + buffered.text,
4605
+ ...stampSegmentActivity(s)
4606
+ };
4262
4607
  });
4263
4608
  msgs = [...msgs.slice(0, -1), { ...last, segments }];
4264
4609
  changed = true;
@@ -4587,7 +4932,7 @@ function feedXmlArtifactDelta(delta, xml, ctx) {
4587
4932
  targetElement.addSegment?.(seg);
4588
4933
  streamingSegmentIds.add(seg.id);
4589
4934
  } else if ("content" in seg) {
4590
- targetElement.updateSegment?.(seg.id, { content: seg.content });
4935
+ targetElement.updateSegment?.(seg.id, segmentContentUpdate(seg));
4591
4936
  }
4592
4937
  }
4593
4938
  const active = textParser.getState().activeSegment;
@@ -4596,7 +4941,7 @@ function feedXmlArtifactDelta(delta, xml, ctx) {
4596
4941
  targetElement.addSegment?.(active);
4597
4942
  streamingSegmentIds.add(active.id);
4598
4943
  } else {
4599
- targetElement.updateSegment?.(active.id, { content: active.content });
4944
+ targetElement.updateSegment?.(active.id, segmentContentUpdate(active));
4600
4945
  }
4601
4946
  } else if (!r.segments.length && !active) ;
4602
4947
  };
@@ -4707,7 +5052,7 @@ function finalizeXmlArtifact(xml, ctx) {
4707
5052
  targetElement.addSegment?.(seg);
4708
5053
  ctx.streamingSegmentIds.add(seg.id);
4709
5054
  } else if ("content" in seg) {
4710
- targetElement.updateSegment?.(seg.id, { content: seg.content });
5055
+ targetElement.updateSegment?.(seg.id, segmentContentUpdate(seg));
4711
5056
  }
4712
5057
  }
4713
5058
  const active = ctx.textParser.getState().activeSegment;
@@ -4716,7 +5061,7 @@ function finalizeXmlArtifact(xml, ctx) {
4716
5061
  targetElement.addSegment?.(active);
4717
5062
  ctx.streamingSegmentIds.add(active.id);
4718
5063
  } else {
4719
- targetElement.updateSegment?.(active.id, { content: active.content });
5064
+ targetElement.updateSegment?.(active.id, segmentContentUpdate(active));
4720
5065
  }
4721
5066
  }
4722
5067
  }
@@ -4782,7 +5127,7 @@ function createStreamAdapter(opts) {
4782
5127
  }
4783
5128
  case "text-delta": {
4784
5129
  if (thinkingId && !thinkingCollapsed) {
4785
- target.updateSegment?.(thinkingId, { collapsed: true });
5130
+ target.updateSegment?.(thinkingId, { collapsed: true, isStreaming: false });
4786
5131
  thinkingCollapsed = true;
4787
5132
  }
4788
5133
  if (e.reduced) {
@@ -4792,7 +5137,7 @@ function createStreamAdapter(opts) {
4792
5137
  target.addSegment?.(segment);
4793
5138
  streaming.add(segment.id);
4794
5139
  } else if ("content" in segment) {
4795
- target.updateSegment?.(segment.id, { content: segment.content });
5140
+ target.updateSegment?.(segment.id, segmentContentUpdate(segment));
4796
5141
  }
4797
5142
  }
4798
5143
  break;
@@ -4816,7 +5161,7 @@ function createStreamAdapter(opts) {
4816
5161
  target.addSegment?.(segment);
4817
5162
  streaming.add(segment.id);
4818
5163
  } else if ("content" in segment) {
4819
- target.updateSegment?.(segment.id, { content: segment.content });
5164
+ target.updateSegment?.(segment.id, segmentContentUpdate(segment));
4820
5165
  }
4821
5166
  if (segment.type === "artifact") {
4822
5167
  dispatchArtifactLifecycle(target, messageId, segment, artifactProgress, true);
@@ -4831,7 +5176,7 @@ function createStreamAdapter(opts) {
4831
5176
  dispatchArtifactLifecycle(target, messageId, active, artifactProgress, false);
4832
5177
  }
4833
5178
  } else {
4834
- target.updateSegment?.(active.id, { content: active.content });
5179
+ target.updateSegment?.(active.id, segmentContentUpdate(active));
4835
5180
  if (active.type === "artifact") {
4836
5181
  dispatchArtifactLifecycle(target, messageId, active, artifactProgress, false);
4837
5182
  }
@@ -4862,7 +5207,7 @@ function createStreamAdapter(opts) {
4862
5207
  }
4863
5208
  for (const s of finals) {
4864
5209
  if (!streaming.has(s.id)) target.addSegment?.(s);
4865
- else if ("content" in s) target.updateSegment?.(s.id, { content: s.content });
5210
+ else if ("content" in s) target.updateSegment?.(s.id, segmentContentUpdate(s));
4866
5211
  if (s.type === "artifact") {
4867
5212
  dispatchArtifactLifecycle(target, messageId, s, artifactProgress, true);
4868
5213
  }
@@ -6203,7 +6548,7 @@ ${content}
6203
6548
  }
6204
6549
  case "text": {
6205
6550
  if (thinkingSegmentId && !thinkingCollapsed) {
6206
- targetElement.updateSegment?.(thinkingSegmentId, { collapsed: true });
6551
+ targetElement.updateSegment?.(thinkingSegmentId, { collapsed: true, isStreaming: false });
6207
6552
  thinkingCollapsed = true;
6208
6553
  }
6209
6554
  precedingText += event.delta;
@@ -6250,7 +6595,7 @@ ${content}
6250
6595
  targetElement.addSegment?.(segment);
6251
6596
  streamingSegmentIds.add(segment.id);
6252
6597
  } else if ("content" in segment) {
6253
- targetElement.updateSegment?.(segment.id, { content: segment.content });
6598
+ targetElement.updateSegment?.(segment.id, segmentContentUpdate(segment));
6254
6599
  }
6255
6600
  if (segment.type === "artifact") {
6256
6601
  dispatchArtifactLifecycle(targetElement, messageId, segment, artifactProgress, true);
@@ -6265,7 +6610,7 @@ ${content}
6265
6610
  dispatchArtifactLifecycle(targetElement, messageId, active, artifactProgress, false);
6266
6611
  }
6267
6612
  } else {
6268
- targetElement.updateSegment?.(active.id, { content: active.content });
6613
+ targetElement.updateSegment?.(active.id, segmentContentUpdate(active));
6269
6614
  if (active.type === "artifact") {
6270
6615
  dispatchArtifactLifecycle(targetElement, messageId, active, artifactProgress, false);
6271
6616
  }
@@ -6335,7 +6680,7 @@ ${content}
6335
6680
  if (!streamingSegmentIds.has(s.id)) {
6336
6681
  targetElement.addSegment?.(s);
6337
6682
  } else if ("content" in s) {
6338
- targetElement.updateSegment?.(s.id, { content: s.content });
6683
+ targetElement.updateSegment?.(s.id, segmentContentUpdate(s));
6339
6684
  }
6340
6685
  if (s.type === "artifact") {
6341
6686
  dispatchArtifactLifecycle(targetElement, messageId, s, artifactProgress, true);
@@ -6744,6 +7089,10 @@ function isAwaitingReply(message) {
6744
7089
  if (message.status === "streaming" || message.status === "pending") return true;
6745
7090
  return message.status === void 0 && message.role === "assistant" && !message.content?.trim() && !message.segments?.length;
6746
7091
  }
7092
+ let _relabels = null;
7093
+ function onRelabel(fn) {
7094
+ _relabels?.push(fn);
7095
+ }
6747
7096
  function el(tag, className, text) {
6748
7097
  const node = document.createElement(tag);
6749
7098
  if (className) node.className = className;
@@ -6800,12 +7149,20 @@ function buildEnumField(field, onChange, fallbackLabel) {
6800
7149
  control.value = "__other__";
6801
7150
  const body = el("span", "aparte-elic-option-body");
6802
7151
  const t = contextConfig();
6803
- 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);
6804
7154
  otherText = el("input", "aparte-elic-other-input");
6805
7155
  otherText.type = "text";
6806
7156
  otherText.placeholder = t.t("elicitationOtherPlaceholder");
6807
7157
  otherText.style.display = "none";
6808
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
+ });
6809
7166
  body.appendChild(otherText);
6810
7167
  row.append(control, body);
6811
7168
  list.appendChild(row);
@@ -6849,7 +7206,7 @@ function buildBooleanField(field, onChange, fallbackLabel) {
6849
7206
  const list = el("div", "aparte-elic-options");
6850
7207
  labelGroup(list, { titleId, fallbackLabel });
6851
7208
  const name = `elic-${uuid()}`;
6852
- const mk = (val, label) => {
7209
+ const mk = (val, label, defaulted) => {
6853
7210
  const row = el("label", "aparte-elic-option");
6854
7211
  const control = el("input", "aparte-elic-control");
6855
7212
  control.type = "radio";
@@ -6857,13 +7214,17 @@ function buildBooleanField(field, onChange, fallbackLabel) {
6857
7214
  control.value = val;
6858
7215
  if (field.default != null && String(field.default) === val) control.checked = true;
6859
7216
  const body = el("span", "aparte-elic-option-body");
6860
- 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);
6861
7222
  row.append(control, body);
6862
7223
  list.appendChild(row);
6863
7224
  };
6864
7225
  const t = contextConfig();
6865
- mk("true", field.trueLabel ?? t.t("elicitationYes"));
6866
- 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");
6867
7228
  wrap.appendChild(list);
6868
7229
  list.addEventListener("change", onChange);
6869
7230
  const checked = () => list.querySelector("input:checked");
@@ -6882,7 +7243,14 @@ function buildStringField(field, onChange, fallbackLabel) {
6882
7243
  if (field.placeholder) input.setAttribute("placeholder", field.placeholder);
6883
7244
  if (field.default) input.value = field.default;
6884
7245
  if (field.maxLength != null) input.setAttribute("maxlength", String(field.maxLength));
6885
- 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
+ }
6886
7254
  wrap.appendChild(input);
6887
7255
  input.addEventListener("input", onChange);
6888
7256
  const required = field.required ?? true;
@@ -6918,6 +7286,19 @@ function buildField(field, onChange, fallbackLabel, key) {
6918
7286
  }
6919
7287
  }
6920
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) {
6921
7302
  const panel = el("div", "aparte-elic-panel");
6922
7303
  if (message) panel.appendChild(el("p", "aparte-elic-message", message));
6923
7304
  const body = el("div", "aparte-elic-body");
@@ -7021,59 +7402,72 @@ function requestUserInput(request) {
7021
7402
  return resolveConfig(request.target ?? null).requestUserInput(request);
7022
7403
  }
7023
7404
  export {
7024
- uuid as $,
7025
- APARTE_CONVERSATION_SCHEMA_VERSION as A,
7026
- createStreamAdapter as B,
7027
- cssEscape as C,
7028
- declineDefaultRenderers as D,
7029
- defaultSanitizer as E,
7030
- deriveArtifactKind as F,
7031
- detachConfig as G,
7032
- escapeAttr as H,
7033
- escapeHtml as I,
7034
- filesToAttachments as J,
7035
- getAllRenderers as K,
7036
- getSegmentRenderer as L,
7037
- installDefaultRenderersOnce as M,
7038
- isAwaitingReply as N,
7039
- isFormatAdapter as O,
7040
- isSafeUrl as P,
7041
- parseAparteEventStream as Q,
7042
- parseMarkdownToSegments as R,
7043
- populateBubbleFromMessage as S,
7044
- readableToAsyncIterable as T,
7045
- registerDefaultRenderers as U,
7046
- registerSegmentRenderer as V,
7047
- requestUserInput as W,
7048
- resolveConfig as X,
7049
- revokeAttachmentUrls as Y,
7050
- runWithConfig as Z,
7051
- unregisterSegmentRenderer as _,
7052
- APARTE_DEFAULT_BUBBLE_ACTIONS as a,
7053
- APARTE_DEFAULT_HOST_HANDLERS as b,
7054
- APARTE_DEFAULT_ICON_FALLBACKS as c,
7055
- APARTE_DEFAULT_LOCALE as d,
7056
- APARTE_DEFAULT_SKELETON_FALLBACKS as e,
7057
- APARTE_DEFAULT_UI_EVENTS as f,
7058
- APARTE_HOST_ATTR as g,
7059
- AparteBackendTransport as h,
7060
- AparteChatHost as i,
7061
- AparteClient as j,
7062
- AparteConfig as k,
7063
- AparteConversationController as l,
7064
- AparteConversationManager as m,
7065
- AparteDirectTransport as n,
7066
- AparteError as o,
7067
- AparteErrorCode as p,
7068
- AparteMessageRepository as q,
7069
- AparteStreamParser as r,
7070
- aparteGlobalConfig as s,
7071
- applyElementProps as t,
7072
- attachConfig as u,
7073
- buildElicitationPanel as v,
7074
- collectRendererStyles as w,
7075
- contentToText as x,
7076
- contextConfig as y,
7077
- 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
7078
7472
  };
7079
- //# sourceMappingURL=index-BieFbKeW.js.map
7473
+ //# sourceMappingURL=index-BnKBciDG.js.map