@aparte/core 0.9.0 → 0.11.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 (109) hide show
  1. package/dist/client/aparte-client.d.ts +54 -33
  2. package/dist/client/aparte-client.d.ts.map +1 -1
  3. package/dist/client/stream-adapter.d.ts +6 -1
  4. package/dist/client/stream-adapter.d.ts.map +1 -1
  5. package/dist/components/bubble/aparte-chat-bubble.d.ts +49 -7
  6. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  7. package/dist/components/chat/aparte-chat.d.ts +8 -4
  8. package/dist/components/chat/aparte-chat.d.ts.map +1 -1
  9. package/dist/components/composer/aparte-composer-action.d.ts +5 -5
  10. package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
  11. package/dist/components/composer/aparte-composer-add-attachment.d.ts +16 -4
  12. package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
  13. package/dist/components/composer/aparte-composer-attachments.d.ts +5 -3
  14. package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
  15. package/dist/components/composer/aparte-composer-cancel.d.ts +10 -2
  16. package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
  17. package/dist/components/composer/aparte-composer-input.d.ts +8 -5
  18. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  19. package/dist/components/composer/aparte-composer-send.d.ts +34 -2
  20. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  21. package/dist/components/composer/aparte-composer-toolbar.d.ts +3 -3
  22. package/dist/components/composer/aparte-composer.d.ts +54 -10
  23. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  24. package/dist/components/conversation-list/aparte-conversation-list.d.ts +19 -12
  25. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  26. package/dist/components/elicitation/aparte-elicitation.d.ts +22 -0
  27. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  28. package/dist/components/status/aparte-chat-status.d.ts +9 -0
  29. package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
  30. package/dist/components/viewport/aparte-chat-viewport.d.ts +18 -5
  31. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  32. package/dist/config/aparte-config.d.ts +54 -2
  33. package/dist/config/aparte-config.d.ts.map +1 -1
  34. package/dist/config/config-subscribe.d.ts +4 -0
  35. package/dist/config/config-subscribe.d.ts.map +1 -0
  36. package/dist/config/index.d.ts +1 -0
  37. package/dist/config/index.d.ts.map +1 -1
  38. package/dist/config/locale.d.ts +141 -0
  39. package/dist/config/locale.d.ts.map +1 -1
  40. package/dist/config/sanitize.d.ts.map +1 -1
  41. package/dist/custom-elements.json +5905 -4694
  42. package/dist/elicitation/approval-panel.d.ts +42 -0
  43. package/dist/elicitation/approval-panel.d.ts.map +1 -0
  44. package/dist/elicitation/index.d.ts +4 -1
  45. package/dist/elicitation/index.d.ts.map +1 -1
  46. package/dist/elicitation/panel.d.ts +14 -1
  47. package/dist/elicitation/panel.d.ts.map +1 -1
  48. package/dist/elicitation/types.d.ts +116 -9
  49. package/dist/elicitation/types.d.ts.map +1 -1
  50. package/dist/generated/element-attributes.d.ts +173 -0
  51. package/dist/generated/element-attributes.d.ts.map +1 -0
  52. package/dist/host/aparte-chat-host.d.ts +3 -1
  53. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  54. package/dist/index.css +752 -392
  55. package/dist/index.d.ts +5 -4
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +538 -290
  58. package/dist/index.js.map +1 -1
  59. package/dist/index.node.d.ts +5 -4
  60. package/dist/index.node.d.ts.map +1 -1
  61. package/dist/index.node.js +62 -57
  62. package/dist/index.node.js.map +1 -1
  63. package/dist/interop/element-props.d.ts +26 -1
  64. package/dist/interop/element-props.d.ts.map +1 -1
  65. package/dist/{index-hQQljwfX.js → is-awaiting-reply-Cb7H3YfN.js} +1159 -972
  66. package/dist/is-awaiting-reply-Cb7H3YfN.js.map +1 -0
  67. package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
  68. package/dist/primitives/select/aparte-optgroup.d.ts +3 -0
  69. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  70. package/dist/primitives/select/aparte-option.d.ts +1 -0
  71. package/dist/primitives/select/aparte-option.d.ts.map +1 -1
  72. package/dist/primitives/select/aparte-select.d.ts +22 -18
  73. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  74. package/dist/renderers/highlight-stream.d.ts +17 -0
  75. package/dist/renderers/highlight-stream.d.ts.map +1 -0
  76. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  77. package/dist/renderers/segments/artifact/binary-file.d.ts.map +1 -1
  78. package/dist/renderers/segments/artifact/card.d.ts.map +1 -1
  79. package/dist/renderers/segments/artifact/shared.d.ts +19 -2
  80. package/dist/renderers/segments/artifact/shared.d.ts.map +1 -1
  81. package/dist/renderers/segments/code.d.ts.map +1 -1
  82. package/dist/renderers/segments/error.d.ts.map +1 -1
  83. package/dist/renderers/segments/pipeline-waiting.d.ts.map +1 -1
  84. package/dist/renderers/segments/thinking.d.ts.map +1 -1
  85. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  86. package/dist/types/element-attributes.d.ts +42 -0
  87. package/dist/types/element-attributes.d.ts.map +1 -0
  88. package/dist/types/event-map.d.ts +2 -4
  89. package/dist/types/event-map.d.ts.map +1 -1
  90. package/dist/types/events.d.ts +0 -22
  91. package/dist/types/events.d.ts.map +1 -1
  92. package/dist/types/index.d.ts +4 -3
  93. package/dist/types/index.d.ts.map +1 -1
  94. package/dist/types/models.d.ts +1 -6
  95. package/dist/types/models.d.ts.map +1 -1
  96. package/dist/types/segments.d.ts +87 -74
  97. package/dist/types/segments.d.ts.map +1 -1
  98. package/dist/types/tools.d.ts +15 -21
  99. package/dist/types/tools.d.ts.map +1 -1
  100. package/dist/utils/segments.d.ts +83 -5
  101. package/dist/utils/segments.d.ts.map +1 -1
  102. package/package.json +3 -2
  103. package/dist/index-hQQljwfX.js.map +0 -1
  104. package/dist/renderers/segments/file-tree.d.ts +0 -3
  105. package/dist/renderers/segments/file-tree.d.ts.map +0 -1
  106. package/dist/renderers/segments/progress.d.ts +0 -3
  107. package/dist/renderers/segments/progress.d.ts.map +0 -1
  108. package/dist/renderers/segments/terminal.d.ts +0 -3
  109. package/dist/renderers/segments/terminal.d.ts.map +0 -1
@@ -69,11 +69,31 @@ 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
+ approvalInstructionPlaceholder: "Or tell it what to do instead…",
79
+ approvalAsk: "Run {tool}?",
80
+ approvalWaiting: "waiting for you",
81
+ toolRunning: "Running",
82
+ toolCompleted: "Done",
83
+ toolRejected: "Rejected",
84
+ toolStopped: "Stopped",
85
+ toolInput: "Input",
86
+ toolOutput: "Output",
87
+ approvalOptionsLabel: "Your decision",
88
+ actionUpload: "Attach file",
89
+ download: "Download",
90
+ previewPending: "Press Preview to run this artifact.",
91
+ sandboxError: "The sandbox failed during generation.",
92
+ sandboxErrorHint: "Common cause: the model produced invalid code (undefined variable, wrong argument type). Retry the request — the model may produce different code.",
93
+ preview: "Preview",
94
+ code: "Code",
95
+ generating: "Generating…",
96
+ rebuildingPreview: "Rebuilding preview…",
77
97
  typing: "Typing...",
78
98
  error: "Error",
79
99
  running: "Running...",
@@ -551,7 +571,6 @@ class AparteStreamParser {
551
571
  id: this._options.autoGenerateIds ? this._generateId() : "",
552
572
  type: "thinking",
553
573
  content,
554
- collapsed: false,
555
574
  label: "Thinking"
556
575
  };
557
576
  }
@@ -939,6 +958,9 @@ const SAFE_STYLE_PROPS = /* @__PURE__ */ new Set([
939
958
  "text-decoration-style",
940
959
  "white-space"
941
960
  ]);
961
+ function isSafeCustomProperty(prop) {
962
+ return prop.startsWith("--") && !prop.startsWith("--aparte-");
963
+ }
942
964
  function scrubStyle(value) {
943
965
  const kept = [];
944
966
  for (const decl of value.split(";")) {
@@ -947,7 +969,8 @@ function scrubStyle(value) {
947
969
  if (idx === -1) continue;
948
970
  const prop = trimmed.slice(0, idx).trim().toLowerCase();
949
971
  const val = trimmed.slice(idx + 1);
950
- if (!val.trim() || !SAFE_STYLE_PROPS.has(prop)) continue;
972
+ if (!val.trim()) continue;
973
+ if (!SAFE_STYLE_PROPS.has(prop) && !isSafeCustomProperty(prop)) continue;
951
974
  if (val.includes("\\")) continue;
952
975
  if (/url\s*\(|expression\s*\(|javascript:|vbscript:|[<>]/i.test(val)) continue;
953
976
  kept.push(trimmed);
@@ -1011,6 +1034,23 @@ const defaultSanitizer = (html) => {
1011
1034
  sanitizeChildren(doc.body, container, doc);
1012
1035
  return container.innerHTML;
1013
1036
  };
1037
+ class AparteElicitationAbortError extends DOMException {
1038
+ /**
1039
+ * Which of the two ways it ended.
1040
+ *
1041
+ * `'no-presenter'` is a developer's setup problem and `'aborted'` is a person
1042
+ * stopping something; a host that reports them the same way reports a bug as a
1043
+ * user action. The distinction is why this carries a field at all.
1044
+ */
1045
+ reason;
1046
+ constructor(reason = "aborted") {
1047
+ super(
1048
+ reason === "no-presenter" ? "No elicitation presenter is mounted, so the request could not be shown." : "The request ended without an answer.",
1049
+ "AbortError"
1050
+ );
1051
+ this.reason = reason;
1052
+ }
1053
+ }
1014
1054
  function escapeHtml(value) {
1015
1055
  let out = "";
1016
1056
  for (let i = 0; i < value.length; i++) {
@@ -1034,10 +1074,11 @@ const APARTE_DEFAULT_BUBBLE_ACTIONS = {
1034
1074
  };
1035
1075
  const APARTE_DEFAULT_HOST_HANDLERS = {
1036
1076
  attachmentPreview: false,
1037
- terminalRun: false,
1038
1077
  artifactRedownload: false,
1039
1078
  artifactRehydrate: false
1040
1079
  };
1080
+ const NOOP = () => {
1081
+ };
1041
1082
  class AparteConfig {
1042
1083
  _markdownProvider;
1043
1084
  _streamingMarkdownProvider;
@@ -1094,9 +1135,27 @@ class AparteConfig {
1094
1135
  _elicitationPresenter;
1095
1136
  /** Warn once per config, not once per request — a tool loop can ask repeatedly. */
1096
1137
  _warnedNoPresenter = false;
1138
+ /**
1139
+ * The tail of the queue of requests waiting for the human, or `null` when none is.
1140
+ *
1141
+ * One request reaches the presenter at a time — not because a presenter could not
1142
+ * cope, but because the COMPOSER cannot: it has one panel slot, and a second
1143
+ * request used to clobber the first's DOM. The previous protection lived in the
1144
+ * default presenter, which answered the second request `cancel` on the spot; that
1145
+ * is a refusal invented for a question nobody was ever shown, the same class of
1146
+ * lie as telling the model a stopped turn was refused. WAITING is the honest
1147
+ * behaviour.
1148
+ *
1149
+ * Here rather than in the presenter because this is the only place that sees
1150
+ * EVERY request whatever raised it — a tool handler, an app's own button, and
1151
+ * (soon) the tool-approval gate. A custom presenter registered by a consumer gets
1152
+ * the same protection for free, which it previously had none of.
1153
+ */
1154
+ _elicitationQueue = null;
1097
1155
  // Tool Registry
1098
1156
  _tools = /* @__PURE__ */ new Map();
1099
1157
  _toolRenderers = /* @__PURE__ */ new Map();
1158
+ _segmentDefaults = /* @__PURE__ */ new Map();
1100
1159
  // Host handlers — what the app declares it can actually complete.
1101
1160
  _hostHandlers = { ...APARTE_DEFAULT_HOST_HANDLERS };
1102
1161
  // Bubble Actions — APARTE_DEFAULT_BUBBLE_ACTIONS is the single source of truth
@@ -1177,7 +1236,6 @@ class AparteConfig {
1177
1236
  getHostHandlers() {
1178
1237
  return {
1179
1238
  attachmentPreview: this._hostHandlers.attachmentPreview ?? APARTE_DEFAULT_HOST_HANDLERS.attachmentPreview,
1180
- terminalRun: this._hostHandlers.terminalRun ?? APARTE_DEFAULT_HOST_HANDLERS.terminalRun,
1181
1239
  artifactRedownload: this._hostHandlers.artifactRedownload ?? APARTE_DEFAULT_HOST_HANDLERS.artifactRedownload,
1182
1240
  artifactRehydrate: this._hostHandlers.artifactRehydrate ?? APARTE_DEFAULT_HOST_HANDLERS.artifactRehydrate
1183
1241
  };
@@ -1799,11 +1857,49 @@ class AparteConfig {
1799
1857
  *
1800
1858
  * @example
1801
1859
  * // Custom pill for a web-search tool
1802
- * aparteGlobalConfig.registerToolRenderer('web_search', { render: (seg) => `<div class="tool-pill">Searching...</div>` });
1860
+ * aparteGlobalConfig.registerToolRenderer('web_search', { render: (seg) => `<div class="aparte-tool-label">Searching...</div>` });
1803
1861
  */
1804
1862
  registerToolRenderer(toolName, renderer) {
1805
1863
  this._toolRenderers.set(toolName, renderer);
1806
1864
  }
1865
+ /**
1866
+ * Field defaults for a segment TYPE, filled in when a segment enters a message.
1867
+ *
1868
+ * One function for every type rather than one per field: a `setThinkingOpen()`
1869
+ * would need a sibling the next time any type wanted a default, and the type key
1870
+ * is a string, so a consumer's own segment type is covered by the same call.
1871
+ *
1872
+ * The problem it solves: a consumer streaming a reply does not CONSTRUCT its
1873
+ * segments — the parser does — so there was no hook at all for "reasoning blocks
1874
+ * open in my app". Per-segment fields only reach segments you build yourself.
1875
+ *
1876
+ * Applied where identity is stamped (`utils/segments.ts`), so it covers every
1877
+ * path a segment can arrive by — the parser, `addSegment`, and the segments
1878
+ * seeded on an `appendMessage` — and no renderer has to look anything up.
1879
+ *
1880
+ * A field the producer set always wins, and identity is never defaulted. Read at
1881
+ * insertion and baked in: changing a default later does not reach segments already
1882
+ * on screen, because a block the reader opened has state the data does not.
1883
+ *
1884
+ * @example
1885
+ * // Reasoning blocks arrive open in this app, streaming or settled.
1886
+ * aparteGlobalConfig.setSegmentDefaults('thinking', { collapsed: false });
1887
+ *
1888
+ * @example
1889
+ * // A consumer's own type, same call.
1890
+ * aparteGlobalConfig.setSegmentDefaults('my-chart', { theme: 'dark' });
1891
+ */
1892
+ setSegmentDefaults(type, defaults) {
1893
+ this._segmentDefaults.set(type, { ...defaults });
1894
+ }
1895
+ /** The defaults registered for a type, or undefined. */
1896
+ getSegmentDefaults(type) {
1897
+ return this._segmentDefaults.get(type);
1898
+ }
1899
+ /** Drop the defaults for a type. */
1900
+ clearSegmentDefaults(type) {
1901
+ this._segmentDefaults.delete(type);
1902
+ }
1807
1903
  /** Unregister a per-tool renderer */
1808
1904
  unregisterToolRenderer(toolName) {
1809
1905
  this._toolRenderers.delete(toolName);
@@ -1858,15 +1954,29 @@ class AparteConfig {
1858
1954
  * itself — nothing to register", which is how this failure mode was found.
1859
1955
  */
1860
1956
  requestUserInput(request) {
1957
+ const previous = this._elicitationQueue;
1958
+ const settled = previous === null ? this._present(request) : previous.then(
1959
+ () => this._present(request),
1960
+ () => this._present(request)
1961
+ );
1962
+ const mine = settled.then(NOOP, NOOP).then(() => {
1963
+ if (this._elicitationQueue === mine) this._elicitationQueue = null;
1964
+ });
1965
+ this._elicitationQueue = mine;
1966
+ return settled;
1967
+ }
1968
+ /** Hand ONE request to the presenter. Called only from the queue. */
1969
+ _present(request) {
1861
1970
  if (!this._elicitationPresenter) {
1862
1971
  if (!this._warnedNoPresenter) {
1863
1972
  this._warnedNoPresenter = true;
1864
1973
  console.warn(
1865
- "[aparte] requestUserInput() was called with no elicitation presenter, so it resolved `cancel` the model will read that as the user refusing, but nothing was ever shown. Add <aparte-elicitation></aparte-elicitation> inside your <aparte-chat>, or register your own by calling setElicitationPresenter() on the config this chat resolves — the scoped one if you passed a `config`, aparteGlobalConfig otherwise."
1974
+ "[aparte] requestUserInput() was called with no elicitation presenter, so it rejected with an AbortError nothing was ever shown to anybody. Add <aparte-elicitation></aparte-elicitation> inside your <aparte-chat>, or register your own by calling setElicitationPresenter() on the config this chat resolves — the scoped one if you passed a `config`, aparteGlobalConfig otherwise."
1866
1975
  );
1867
1976
  }
1868
- return Promise.resolve({ action: "cancel" });
1977
+ return Promise.reject(new AparteElicitationAbortError("no-presenter"));
1869
1978
  }
1979
+ if (request.signal?.aborted) return Promise.reject(new AparteElicitationAbortError("aborted"));
1870
1980
  return this._elicitationPresenter(request);
1871
1981
  }
1872
1982
  /**
@@ -1888,11 +1998,13 @@ class AparteConfig {
1888
1998
  this._siblingNavRenderer = void 0;
1889
1999
  this._bubbleShellRenderer = void 0;
1890
2000
  this._iconProvider = void 0;
2001
+ this._segmentDefaults.clear();
1891
2002
  this._avatarProvider = void 0;
1892
2003
  this._artifactPreviewBuilder = void 0;
1893
2004
  this._keyProvider = void 0;
1894
2005
  this._conversationManager = void 0;
1895
2006
  this._elicitationPresenter = void 0;
2007
+ this._elicitationQueue = null;
1896
2008
  this._locale = APARTE_DEFAULT_LOCALE;
1897
2009
  this._actions = [];
1898
2010
  this._sanitizer = defaultSanitizer;
@@ -1983,6 +2095,16 @@ function runWithConfig(config, fn) {
1983
2095
  function contextConfig(el2) {
1984
2096
  return _renderConfig ?? resolveConfig(el2);
1985
2097
  }
2098
+ const APARTE_CONFIG_CHANGE = "aparte-config-change";
2099
+ function subscribeConfigChange(el2, handler) {
2100
+ const listener = (e) => {
2101
+ const detail = e.detail;
2102
+ if (detail?.config && detail.config !== resolveConfig(el2)) return;
2103
+ handler();
2104
+ };
2105
+ window.addEventListener(APARTE_CONFIG_CHANGE, listener);
2106
+ return () => window.removeEventListener(APARTE_CONFIG_CHANGE, listener);
2107
+ }
1986
2108
  const APARTE_DEFAULT_SKELETON_FALLBACKS = {
1987
2109
  message: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading...</div>',
1988
2110
  code: '<div class="aparte-skeleton-fallback" style="padding:16px;background:#1e293b;color:#64748b;border-radius:8px;">Loading code...</div>',
@@ -2033,9 +2155,9 @@ function highlightMarkdownFences(contentEl) {
2033
2155
  }
2034
2156
  const textRenderer = {
2035
2157
  type: "text",
2036
- render: (segment) => `<div class="segment segment-text" data-segment-id="${escapeHtml(segment.id)}"><div class="segment-content">${contextConfig().renderMarkdown(segment.content)}</div></div>`,
2158
+ render: (segment) => `<div class="aparte-segment aparte-segment-text" data-segment-id="${escapeHtml(segment.id)}"><div class="aparte-segment-content">${contextConfig().renderMarkdown(segment.content)}</div></div>`,
2037
2159
  update: (el2, segment) => {
2038
- const contentEl = el2.querySelector(".segment-content");
2160
+ const contentEl = el2.querySelector(".aparte-segment-content");
2039
2161
  if (!contentEl) return;
2040
2162
  writeStreamedMarkdown(
2041
2163
  el2,
@@ -2063,9 +2185,17 @@ const thinkingRenderer = {
2063
2185
  * new class of risk. With no Markdown provider registered it degrades to the
2064
2186
  * zero-dependency default (escape + `<br>`), which is what it used to be.
2065
2187
  */
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>`,
2188
+ render: (segment) => `<details class="aparte-segment aparte-segment-thinking" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed === false ? "open" : ""}><summary class="aparte-thinking-header"><span class="aparte-thinking-label">${escapeHtml(segment.label || contextConfig().t("thinking"))}</span><span class="aparte-thinking-toggle"></span></summary><div class="aparte-thinking-content">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
2189
+ /**
2190
+ * The default label is the only config-derived text here — and `segment.label`
2191
+ * still wins, exactly as in `render`, because that string is the app's.
2192
+ */
2193
+ relabel: (el2, segment) => {
2194
+ const label = el2.querySelector(".aparte-thinking-label");
2195
+ if (label) label.textContent = segment.label || contextConfig().t("thinking");
2196
+ },
2067
2197
  update: (el2, segment) => {
2068
- const contentEl = el2.querySelector(".thinking-content");
2198
+ const contentEl = el2.querySelector(".aparte-thinking-content");
2069
2199
  if (!contentEl) return;
2070
2200
  const wasAtBottom = contentEl.scrollHeight - contentEl.scrollTop - contentEl.clientHeight <= THINKING_ANCHOR_SLACK;
2071
2201
  const streaming = segment.isStreaming !== false;
@@ -2075,56 +2205,106 @@ const thinkingRenderer = {
2075
2205
  },
2076
2206
  getStyles: () => ``
2077
2207
  };
2208
+ const HIGHLIGHT_DEBOUNCE_MS = 400;
2209
+ const MAX_THROTTLE_ENTRIES = 256;
2210
+ const _lastHighlightAt = /* @__PURE__ */ new Map();
2211
+ function markThrottle(map, id, at) {
2212
+ map.delete(id);
2213
+ if (map.size >= MAX_THROTTLE_ENTRIES) {
2214
+ const oldest = map.keys().next().value;
2215
+ if (oldest !== void 0) map.delete(oldest);
2216
+ }
2217
+ map.set(id, at);
2218
+ }
2219
+ function highlightedLen(pane) {
2220
+ const n = Number(pane.dataset.aparteHlLen);
2221
+ return Number.isFinite(n) && n > 0 ? n : 0;
2222
+ }
2223
+ function streamHighlight(element, paneSelector, content, lang, segId) {
2224
+ const pane = element.querySelector(paneSelector);
2225
+ if (!pane) return;
2226
+ const tail = pane.querySelector("[data-aparte-tail]");
2227
+ const hlLen = highlightedLen(pane);
2228
+ if (tail && hlLen <= content.length) {
2229
+ tail.textContent = content.slice(hlLen);
2230
+ } else {
2231
+ delete pane.dataset.aparteHlLen;
2232
+ const codeEl = pane.querySelector("code");
2233
+ if (codeEl) codeEl.textContent = content;
2234
+ else pane.innerHTML = `<pre><code class="language-${escapeHtml(lang || "text")}">${escapeHtml(content)}</code></pre>`;
2235
+ }
2236
+ const cut = content.lastIndexOf("\n") + 1;
2237
+ if (cut <= hlLen) return;
2238
+ const now = Date.now();
2239
+ if (now - (_lastHighlightAt.get(segId) ?? 0) < HIGHLIGHT_DEBOUNCE_MS) return;
2240
+ markThrottle(_lastHighlightAt, segId, now);
2241
+ void contextConfig(element).highlightCode(content.slice(0, cut), lang).then((html) => {
2242
+ const live = element.querySelector(paneSelector);
2243
+ if (!live) return;
2244
+ if (cut <= highlightedLen(live)) return;
2245
+ live.innerHTML = html;
2246
+ const codeEl = live.querySelector("code") ?? live;
2247
+ const span = document.createElement("span");
2248
+ span.dataset.aparteTail = "";
2249
+ span.textContent = content.slice(cut);
2250
+ codeEl.appendChild(span);
2251
+ live.dataset.aparteHlLen = String(cut);
2252
+ }).catch(() => {
2253
+ });
2254
+ }
2078
2255
  const codeRenderer = {
2079
2256
  type: "code",
2080
2257
  render: (segment) => `
2081
- <div class="segment segment-code" data-segment-id="${escapeHtml(segment.id)}">
2082
- <div class="code-header">
2083
- ${segment.filename ? `<span class="code-filename">${escapeHtml(segment.filename)}</span>` : `<span class="code-header-filler"></span>`}
2084
- <span class="code-language">${escapeHtml(segment.language || "")}</span>
2085
- <button class="code-copy" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}">
2258
+ <div class="aparte-segment aparte-segment-code" data-segment-id="${escapeHtml(segment.id)}">
2259
+ <div class="aparte-code-header">
2260
+ ${segment.filename ? `<span class="aparte-code-filename">${escapeHtml(segment.filename)}</span>` : `<span class="aparte-code-header-filler"></span>`}
2261
+ <span class="aparte-code-language">${escapeHtml(segment.language || "")}</span>
2262
+ <button class="aparte-code-copy" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}">
2086
2263
  ${contextConfig().getIcon("copy")}
2087
2264
  </button>
2088
2265
  </div>
2089
- <div class="code-content-wrapper">
2266
+ <div class="aparte-code-content-wrapper">
2090
2267
  <pre><code class="language-${escapeHtml(segment.language || "text")}">${escapeHtml(segment.content)}</code></pre>
2091
2268
  </div>
2092
2269
  </div>
2093
2270
  `,
2271
+ /** The copy button's icon and tooltip — nothing else here comes from config. */
2272
+ relabel: (element) => {
2273
+ const copyBtn = element.querySelector(".aparte-code-copy");
2274
+ if (!copyBtn || copyBtn.dataset.copied) return;
2275
+ copyBtn.setAttribute("title", contextConfig().t("copy"));
2276
+ copyBtn.innerHTML = contextConfig().getIcon("copy");
2277
+ },
2094
2278
  setup: (element, segment) => {
2095
- const wrapper = element.querySelector(".code-content-wrapper");
2279
+ const wrapper = element.querySelector(".aparte-code-content-wrapper");
2096
2280
  if (wrapper) {
2097
2281
  void contextConfig().highlightCode(segment.content, segment.language || "").then((html) => {
2098
2282
  wrapper.innerHTML = html;
2099
2283
  }).catch(() => {
2100
2284
  });
2101
2285
  }
2102
- const copyBtn = element.querySelector(".code-copy");
2286
+ const copyBtn = element.querySelector(".aparte-code-copy");
2103
2287
  if (copyBtn) {
2104
2288
  copyBtn.addEventListener("click", () => {
2105
- const source = element.querySelector(".code-content-wrapper")?.textContent ?? "";
2289
+ const source = element.querySelector(".aparte-code-content-wrapper")?.textContent ?? "";
2106
2290
  void navigator.clipboard.writeText(source).catch(() => {
2107
2291
  });
2108
2292
  copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
2109
2293
  copyBtn.setAttribute("title", contextConfig(copyBtn).t("copied"));
2294
+ copyBtn.dataset.copied = "1";
2110
2295
  setTimeout(() => {
2111
2296
  copyBtn.innerHTML = contextConfig(copyBtn).getIcon("copy");
2112
2297
  copyBtn.setAttribute("title", contextConfig(copyBtn).t("copy"));
2298
+ delete copyBtn.dataset.copied;
2113
2299
  }, 1500);
2114
2300
  });
2115
2301
  }
2116
2302
  },
2117
2303
  update: (element, segment) => {
2118
2304
  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
- }
2305
+ streamHighlight(element, ".aparte-code-content-wrapper", segment.content, segment.language || "", segment.id);
2126
2306
  } else {
2127
- const wrapper = element.querySelector(".code-content-wrapper");
2307
+ const wrapper = element.querySelector(".aparte-code-content-wrapper");
2128
2308
  if (wrapper) {
2129
2309
  void contextConfig().highlightCode(segment.content, segment.language || "").then((html) => {
2130
2310
  wrapper.innerHTML = html;
@@ -2135,58 +2315,6 @@ const codeRenderer = {
2135
2315
  },
2136
2316
  getStyles: () => ``
2137
2317
  };
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
2318
  const errorRenderer = {
2191
2319
  type: "error",
2192
2320
  render: (segment) => {
@@ -2200,173 +2328,239 @@ const errorRenderer = {
2200
2328
  return out;
2201
2329
  }
2202
2330
  return `
2203
- <div class="segment segment-error" data-segment-id="${escapeHtml(segment.id)}">
2204
- <div class="error-icon-wrapper">
2331
+ <div class="aparte-segment aparte-segment-error" data-segment-id="${escapeHtml(segment.id)}">
2332
+ <div class="aparte-error-icon-wrapper">
2205
2333
  ${contextConfig().getIcon("error") || "⚠"}
2206
2334
  </div>
2207
- <div class="error-content">
2208
- <div class="error-title">Error</div>
2209
- <div class="error-message">${escapeHtml(segment.content)}</div>
2210
- ${segment.details ? `<div class="error-details">${escapeHtml(segment.details)}</div>` : ""}
2335
+ <div class="aparte-error-content">
2336
+ <div class="aparte-error-title">${escapeHtml(contextConfig().t("error"))}</div>
2337
+ <div class="aparte-error-message">${escapeHtml(segment.content)}</div>
2338
+ ${segment.details ? `<div class="aparte-error-details">${escapeHtml(segment.details)}</div>` : ""}
2211
2339
  </div>
2212
2340
  </div>
2213
2341
  `;
2214
2342
  },
2215
- getStyles: () => ``
2216
- };
2217
- const progressRenderer = {
2218
- type: "progress",
2219
- render: (segment) => {
2220
- const label = escapeHtml(segment.label || "Progress");
2221
- const pct = Math.round(segment.percent || 0);
2222
- return `<div class="segment segment-progress" data-segment-id="${escapeHtml(segment.id)}"><div class="progress-header"><span class="progress-label">${label}</span><span class="progress-value">${pct}%</span></div><div class="progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${pct}" aria-label="${label}"><div class="progress-fill" style="width: ${pct}%"></div></div></div>`;
2343
+ /**
2344
+ * The icon and the heading. The heading used to be the hardcoded literal `Error`
2345
+ * while `locale.error` — a REQUIRED key, documented, and already translated to
2346
+ * "Erreur" — was read by nothing at all. A translated string with no consumer and
2347
+ * a literal with no translation, in the same card.
2348
+ *
2349
+ * Not the message: that is the model's or the transport's text, in whatever
2350
+ * language it arrived in. Relabelling it would be inventing content.
2351
+ */
2352
+ relabel: (element) => {
2353
+ const wrap = element.querySelector(".aparte-error-icon-wrapper");
2354
+ if (wrap) wrap.innerHTML = contextConfig().getIcon("error") || "⚠";
2355
+ const title = element.querySelector(".aparte-error-title");
2356
+ if (title) title.textContent = contextConfig().t("error");
2223
2357
  },
2224
2358
  getStyles: () => ``
2225
2359
  };
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
- }
2360
+ function describeInput(segment) {
2361
+ const input = segment.toolCall?.input;
2362
+ if (!input || typeof input !== "object" || Object.keys(input).length === 0) return "";
2363
+ try {
2364
+ return JSON.stringify(input, null, 2);
2365
+ } catch {
2366
+ return "";
2235
2367
  }
2236
- return html;
2237
2368
  }
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
- };
2369
+ function stateBadge(segment) {
2370
+ const cfg = contextConfig();
2371
+ const status = segment.status ?? "pending";
2372
+ const word = (key) => escapeHtml(cfg.t(key));
2373
+ if (status === "awaiting-approval") return word("approvalWaiting");
2374
+ if (status === "pending") return word("toolRunning");
2375
+ if (status === "resolved") return `${cfg.getIcon("check")}${word("toolCompleted")}`;
2376
+ if (status === "rejected") return `${cfg.getIcon("close")}${word("toolRejected")}`;
2377
+ if (status === "aborted") return `${cfg.getIcon("close")}${word("toolStopped")}`;
2378
+ return "";
2379
+ }
2380
+ function setPart(element, part, text) {
2381
+ const existing = element.querySelector(`[data-part="${part}"]`);
2382
+ if (!text) {
2383
+ existing?.remove();
2384
+ return;
2385
+ }
2386
+ if (existing) {
2387
+ const code2 = existing.querySelector("code");
2388
+ if (code2 && code2.textContent !== text) code2.textContent = text;
2389
+ return;
2390
+ }
2391
+ const detail = element.querySelector(".aparte-tool-detail");
2392
+ if (!detail) return;
2393
+ const cfg = contextConfig();
2394
+ const wrap = document.createElement("div");
2395
+ wrap.className = "aparte-tool-part";
2396
+ wrap.dataset["part"] = part;
2397
+ const label = document.createElement("span");
2398
+ label.className = "aparte-tool-part-label";
2399
+ label.textContent = cfg.t(part === "input" ? "toolInput" : "toolOutput");
2400
+ const body = document.createElement("div");
2401
+ body.className = "aparte-tool-part-body";
2402
+ const pre = document.createElement("pre");
2403
+ const code = document.createElement("code");
2404
+ code.textContent = text;
2405
+ pre.appendChild(code);
2406
+ body.appendChild(pre);
2407
+ wrap.append(label, body);
2408
+ if (part === "input") detail.prepend(wrap);
2409
+ else detail.appendChild(wrap);
2410
+ }
2411
+ function highlightParts(element) {
2412
+ const cfg = contextConfig();
2413
+ for (const body of element.querySelectorAll(".aparte-tool-part-body")) {
2414
+ const text = body.querySelector("code")?.textContent ?? "";
2415
+ if (!text) continue;
2416
+ void cfg.highlightCode(text, "json").then((html) => {
2417
+ if ((body.querySelector("code")?.textContent ?? "") !== text) return;
2418
+ body.innerHTML = html;
2419
+ }).catch(() => {
2420
+ });
2421
+ }
2422
+ }
2423
+ function pillMarkup(segment, name, status) {
2424
+ const badge = stateBadge(segment);
2425
+ return `
2426
+ <span class="aparte-tool-label">
2427
+ <span class="aparte-tool-icon">${contextConfig().getIcon("tool")}</span>
2428
+ <span class="aparte-tool-name">${escapeHtml(name)}</span>
2429
+ </span>
2430
+ <span class="aparte-tool-spinner" aria-hidden="true"${status === "pending" ? "" : " hidden"}></span>
2431
+ <span class="aparte-tool-state">${badge}</span>`;
2432
+ }
2251
2433
  const toolCallRenderer = {
2252
2434
  type: "tool_call",
2253
2435
  render: (segment) => {
2254
2436
  const name = segment.toolCall?.name ?? "tool";
2255
2437
  const status = segment.status ?? "pending";
2256
2438
  const toolCallId = segment.toolCall?.id ?? "";
2257
- if (status === "awaiting-approval") {
2258
- const loc = contextConfig().getLocale();
2259
- const approve = loc.approveTool ?? "Approve";
2260
- const reject = loc.rejectTool ?? "Reject";
2261
- return `
2262
- <div class="segment segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="awaiting-approval" data-tool-call-id="${escapeAttr(toolCallId)}">
2263
- <span class="tool-pill">
2264
- <span class="tool-pill-icon">${contextConfig().getIcon("tool")}</span>
2265
- <span class="tool-pill-name">${escapeHtml(name)}</span>
2266
- </span>
2267
- <span class="tool-approval" role="group" aria-label="${escapeAttr(name)}">
2268
- <button type="button" class="tool-approve-btn" data-tool-decision="approve" aria-label="${escapeAttr(approve)}">${escapeHtml(approve)}</button>
2269
- <button type="button" class="tool-reject-btn" data-tool-decision="reject" aria-label="${escapeAttr(reject)}">${escapeHtml(reject)}</button>
2270
- </span>
2271
- </div>
2272
- `;
2273
- }
2274
2439
  const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
2275
2440
  if (customRenderer) {
2276
2441
  const out = customRenderer.render(segment);
2277
2442
  if (out) return out;
2278
2443
  }
2279
- const statusIcon = status === "resolved" ? contextConfig().getIcon("check") : status === "aborted" || status === "rejected" ? contextConfig().getIcon("close") : "";
2280
- const spinner = status === "pending" ? `<span class="tool-pill-spinner" aria-hidden="true"></span>` : "";
2281
- return `
2282
- <div class="segment segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}">
2283
- <span class="tool-pill">
2284
- <span class="tool-pill-icon">${contextConfig().getIcon("tool")}</span>
2285
- <span class="tool-pill-name">${escapeHtml(name)}</span>
2286
- ${spinner}
2287
- ${statusIcon ? `<span class="tool-pill-status">${statusIcon}</span>` : ""}
2288
- </span>
2444
+ const pill = pillMarkup(segment, name, status);
2445
+ const input = describeInput(segment);
2446
+ const result = segment.result;
2447
+ if (!input && !result) {
2448
+ return `
2449
+ <div class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}">${pill}
2289
2450
  </div>
2290
2451
  `;
2452
+ }
2453
+ const cfg = contextConfig();
2454
+ return `
2455
+ <details class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}">
2456
+ <summary class="aparte-tool-summary"><span class="aparte-tool-toggle"></span>${pill}</summary>
2457
+ <div class="aparte-tool-detail">
2458
+ ${input ? `
2459
+ <div class="aparte-tool-part" data-part="input">
2460
+ <span class="aparte-tool-part-label">${escapeHtml(cfg.t("toolInput"))}</span>
2461
+ <div class="aparte-tool-part-body"><pre><code>${escapeHtml(input)}</code></pre></div>
2462
+ </div>` : ""}
2463
+ ${result ? `
2464
+ <div class="aparte-tool-part" data-part="output">
2465
+ <span class="aparte-tool-part-label">${escapeHtml(cfg.t("toolOutput"))}</span>
2466
+ <div class="aparte-tool-part-body"><pre><code>${escapeHtml(result)}</code></pre></div>
2467
+ </div>` : ""}
2468
+ </div>
2469
+ </details>
2470
+ `;
2291
2471
  },
2292
- setup: (element, segment) => {
2293
- if (segment.status === "awaiting-approval") {
2294
- const toolCallId = segment.toolCall?.id;
2295
- if (!toolCallId) return;
2296
- const host = element.closest("[data-aparte-host], aparte-chat, aparte-chat-viewport");
2297
- const decide = (approved) => element.dispatchEvent(new CustomEvent("aparte-tool-decision", {
2298
- bubbles: true,
2299
- composed: true,
2300
- detail: { toolCallId, approved, targetId: host?.id || void 0 }
2301
- }));
2302
- element.querySelector('[data-tool-decision="approve"]')?.addEventListener("click", () => decide(true));
2303
- element.querySelector('[data-tool-decision="reject"]')?.addEventListener("click", () => decide(false));
2472
+ /**
2473
+ * The pill's glyphs, and the label it wears while a person is deciding.
2474
+ *
2475
+ * The two highest-stakes strings in the library — the approval labels — are no
2476
+ * longer here: they are on the panel, which relabels itself. This still matters
2477
+ * though, because the request the pill describes stays open for as long as
2478
+ * somebody takes to decide, which is exactly long enough for a language switch.
2479
+ *
2480
+ * No child node is added or removed, per the relabel contract.
2481
+ */
2482
+ relabel: (element, segment) => {
2483
+ const cfg = contextConfig();
2484
+ const icon = element.querySelector(".aparte-tool-icon");
2485
+ if (icon) icon.innerHTML = cfg.getIcon("tool");
2486
+ const statusEl = element.querySelector(".aparte-tool-state");
2487
+ if (statusEl) {
2488
+ const s = segment.status;
2489
+ statusEl.innerHTML = s === "resolved" ? cfg.getIcon("check") : s === "aborted" || s === "rejected" ? cfg.getIcon("close") : "";
2490
+ }
2491
+ if (statusEl && segment.status === "awaiting-approval") {
2492
+ statusEl.textContent = cfg.t("approvalWaiting");
2493
+ }
2494
+ },
2495
+ /**
2496
+ * Patch in place. NEVER re-render, and never touch `open`.
2497
+ *
2498
+ * Without an `update` the bubble replaces the element wholesale — and a tool call
2499
+ * changes status several times per turn, so a disclosure the reader opened would
2500
+ * slam shut under them every time. The same rule the reasoning block has, for the
2501
+ * same reason: `collapsed` is honoured only when an update explicitly carries it.
2502
+ *
2503
+ * The one case that cannot be patched is a change of ELEMENT: a call that had
2504
+ * neither arguments nor a result rendered as a plain pill, and gaining a result
2505
+ * makes it a `<details>`. That falls back to a rebuild, which costs nothing because
2506
+ * there was no disclosure to lose.
2507
+ */
2508
+ update: (element, segment) => {
2509
+ const status = segment.status ?? "pending";
2510
+ const input = describeInput(segment);
2511
+ const wantsDetail = !!input || !!segment.result;
2512
+ const custom = contextConfig().getToolRenderer(segment.toolCall?.name);
2513
+ if (custom || wantsDetail !== (element.tagName === "DETAILS")) {
2514
+ const out = toolCallRenderer.render(segment);
2515
+ const tpl = document.createElement("template");
2516
+ tpl.innerHTML = typeof out === "string" ? out.trim() : "";
2517
+ const rebuilt = typeof out === "string" ? tpl.content.firstElementChild : out;
2518
+ if (rebuilt) {
2519
+ element.replaceWith(rebuilt);
2520
+ toolCallRenderer.setup?.(rebuilt, segment);
2521
+ }
2304
2522
  return;
2305
2523
  }
2524
+ element.setAttribute("data-status", status);
2525
+ const badge = element.querySelector(".aparte-tool-state");
2526
+ if (badge) badge.innerHTML = stateBadge(segment);
2527
+ element.querySelector(".aparte-tool-spinner")?.toggleAttribute("hidden", status !== "pending");
2528
+ setPart(element, "input", input);
2529
+ setPart(element, "output", segment.result ?? "");
2530
+ highlightParts(element);
2531
+ },
2532
+ setup: (element, segment) => {
2533
+ highlightParts(element);
2306
2534
  const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
2307
2535
  customRenderer?.setup?.(element, segment);
2308
2536
  },
2309
- getStyles: () => `
2310
- .segment-tool-call { display: flex; padding: 2px 0; }
2311
- .tool-pill {
2312
- display: inline-flex;
2313
- align-items: center;
2314
- gap: 5px;
2315
- padding: 3px 10px 3px 7px;
2316
- border-radius: 99px;
2317
- font-size: 0.78rem;
2318
- font-weight: 500;
2319
- border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
2320
- background: var(--aparte-surface, #f8f8f8);
2321
- color: var(--aparte-text-secondary, rgba(0,0,0,0.55));
2322
- user-select: none;
2323
- }
2324
- [data-status="resolved"] .tool-pill {
2325
- border-color: var(--aparte-success-border, rgba(34,197,94,0.3));
2326
- background: var(--aparte-success-surface, rgba(34,197,94,0.06));
2327
- color: var(--aparte-success, rgb(21,128,61));
2328
- }
2329
- [data-status="aborted"] .tool-pill,
2330
- [data-status="rejected"] .tool-pill {
2331
- border-color: var(--aparte-error-border, rgba(239,68,68,0.3));
2332
- background: var(--aparte-error-surface, rgba(239,68,68,0.06));
2333
- color: var(--aparte-error, rgb(185,28,28));
2334
- }
2335
- .tool-approval { display: inline-flex; gap: 6px; margin-left: 8px; vertical-align: middle; }
2336
- .tool-approve-btn, .tool-reject-btn {
2337
- font: inherit; font-size: 0.78rem; font-weight: 600; line-height: 1;
2338
- padding: 4px 12px; border-radius: 99px; cursor: pointer;
2339
- border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
2340
- background: var(--aparte-surface, #f8f8f8);
2341
- }
2342
- .tool-approve-btn { color: var(--aparte-success, rgb(21,128,61)); border-color: var(--aparte-success-border, rgba(34,197,94,0.4)); }
2343
- .tool-reject-btn { color: var(--aparte-error, rgb(185,28,28)); border-color: var(--aparte-error-border, rgba(239,68,68,0.4)); }
2344
- .tool-approve-btn:hover { background: var(--aparte-success-surface, rgba(34,197,94,0.1)); }
2345
- .tool-reject-btn:hover { background: var(--aparte-error-surface, rgba(239,68,68,0.1)); }
2346
- .tool-pill-spinner {
2347
- width: 10px; height: 10px;
2348
- border: 1.5px solid currentColor;
2349
- border-top-color: transparent;
2350
- border-radius: 50%;
2351
- display: inline-block;
2352
- animation: tool-spin 0.7s linear infinite;
2353
- }
2354
- .tool-pill-status { font-size: 0.75rem; }
2355
- @keyframes tool-spin { to { transform: rotate(360deg); } }
2356
- `
2537
+ // CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
2538
+ // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
2539
+ // has no other way onto the page.
2540
+ getStyles: () => ""
2357
2541
  };
2542
+ const waitingLabel = () => contextConfig().t("generating");
2358
2543
  const pipelineWaitingRenderer = {
2359
2544
  type: "pipeline-waiting",
2360
2545
  render: (segment) => {
2361
2546
  return `
2362
- <div class="segment segment-pipeline-waiting" data-segment-id="${escapeHtml(segment.id)}" aria-label="Generating…" role="status">
2363
- <span class="pw-dot"></span>
2364
- <span class="pw-dot"></span>
2365
- <span class="pw-dot"></span>
2547
+ <div class="aparte-segment aparte-segment-pipeline-waiting" data-segment-id="${escapeHtml(segment.id)}" aria-label="${escapeAttr(waitingLabel())}" role="status">
2548
+ <span class="aparte-pw-dot"></span>
2549
+ <span class="aparte-pw-dot"></span>
2550
+ <span class="aparte-pw-dot"></span>
2366
2551
  </div>`;
2367
2552
  },
2368
2553
  update: () => {
2369
2554
  },
2555
+ /**
2556
+ * Three dots and an accessible name. The dots are CSS; the name is the whole
2557
+ * content as far as a screen reader is concerned, and it was hardcoded English in
2558
+ * every locale — the one string in this library where being untranslated is
2559
+ * invisible to everyone who can see the screen.
2560
+ */
2561
+ relabel: (element) => {
2562
+ element.setAttribute("aria-label", waitingLabel());
2563
+ },
2370
2564
  setup: (el2) => {
2371
2565
  const parent = el2.parentElement;
2372
2566
  if (!parent) return;
@@ -2378,30 +2572,10 @@ const pipelineWaitingRenderer = {
2378
2572
  });
2379
2573
  observer.observe(parent, { childList: true });
2380
2574
  },
2381
- getStyles: () => `
2382
- .segment-pipeline-waiting {
2383
- display: flex;
2384
- align-items: center;
2385
- gap: 5px;
2386
- padding: 6px 2px;
2387
- min-height: 28px;
2388
- }
2389
- .pw-dot {
2390
- display: inline-block;
2391
- width: 8px;
2392
- height: 8px;
2393
- border-radius: 50%;
2394
- background: var(--aparte-text-muted, #6b7280);
2395
- opacity: 0.3;
2396
- animation: pw-pulse 1.2s ease-in-out infinite;
2397
- }
2398
- .pw-dot:nth-child(2) { animation-delay: 0.2s; }
2399
- .pw-dot:nth-child(3) { animation-delay: 0.4s; }
2400
- @keyframes pw-pulse {
2401
- 0%, 80%, 100% { opacity: 0.3; transform: scale(0.85); }
2402
- 40% { opacity: 1; transform: scale(1.1); }
2403
- }
2404
- `
2575
+ // CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
2576
+ // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
2577
+ // has no other way onto the page.
2578
+ getStyles: () => ""
2405
2579
  };
2406
2580
  function stripCodeFences(content) {
2407
2581
  let s = content;
@@ -2451,26 +2625,6 @@ function findClosingFence(s) {
2451
2625
  }
2452
2626
  return -1;
2453
2627
  }
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
2628
  function labelForKind(kind) {
2475
2629
  switch (kind) {
2476
2630
  case "react":
@@ -2509,8 +2663,6 @@ function labelForKind(kind) {
2509
2663
  return "Artifact";
2510
2664
  }
2511
2665
  }
2512
- const MAX_THROTTLE_ENTRIES = 256;
2513
- const HIGHLIGHT_DEBOUNCE_MS = 400;
2514
2666
  const PREVIEW_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:";
2515
2667
  function withMetaCsp(doc) {
2516
2668
  const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttr(PREVIEW_CSP)}">`;
@@ -2527,9 +2679,18 @@ function buildPreviewBody(kind, body, title) {
2527
2679
  if (startsWithDoctype(body)) return body;
2528
2680
  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
2681
  }
2682
+ // An SVG with only a `viewBox` — the recommended, responsive form, and the one
2683
+ // a model writes most often — has NO intrinsic size. As a flex item its cross
2684
+ // size then collapses to zero and the frame is blank: the preview worked for an
2685
+ // SVG that declared `width`/`height` and silently showed nothing for the
2686
+ // idiomatic one. Reported from the landing the moment its demo payload became a
2687
+ // real file instead of a hand-written chart with dimensions on it.
2688
+ //
2689
+ // The rule is narrowed by attribute selector rather than applied to every SVG,
2690
+ // so an SVG that DOES state its size keeps the size it asked for.
2530
2691
  case "svg":
2531
2692
  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>
2693
+ <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
2694
  </head><body>${body}</body></html>`;
2534
2695
  case "js": {
2535
2696
  const safeBody = escapeClosingScriptTag(body);
@@ -2612,11 +2773,12 @@ function renderBinaryFileArtifact(segment, kind) {
2612
2773
  const iconLabel = FILE_ICON_LABEL[kind] ?? kind.toUpperCase();
2613
2774
  const isStreaming = !!segment.isStreaming;
2614
2775
  const canRedownload = contextConfig().getHostHandlers().artifactRedownload;
2776
+ const downloadLabel = escapeHtml(contextConfig().t("download"));
2615
2777
  const cached = _binaryArtifactCache.get(segment.id);
2616
2778
  if (cached && !isStreaming) {
2617
2779
  const previewBody = cached.previewHtml ? contextConfig().sanitizeHtml(cached.previewHtml) : `<div class="aparte-art-file__preview-empty">Preview not available for ${escapeHtml(kind)} yet</div>`;
2618
2780
  return `
2619
- <div class="segment segment-artifact-file"
2781
+ <div class="aparte-segment aparte-segment-artifact-file"
2620
2782
  data-segment-id="${escapeHtml(segment.id)}"
2621
2783
  data-artifact-type="${escapeHtml(kind)}"
2622
2784
  data-state="ready">
@@ -2627,7 +2789,7 @@ function renderBinaryFileArtifact(segment, kind) {
2627
2789
  <div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(formatBytes(cached.bytes))} · ${escapeHtml(kind.toUpperCase())}</div>
2628
2790
  </div>
2629
2791
  <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>' : ""}
2792
+ ${canRedownload ? `<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download">${downloadLabel}</button>` : ""}
2631
2793
  </div>
2632
2794
  </div>
2633
2795
  <div class="aparte-art-file__body">
@@ -2640,9 +2802,9 @@ function renderBinaryFileArtifact(segment, kind) {
2640
2802
  `;
2641
2803
  }
2642
2804
  const cleanContent = stripCodeFences(segment.content || "");
2643
- const subText = isStreaming ? "Generating…" : "Rebuilding preview…";
2805
+ const subText = isStreaming ? contextConfig().t("generating") : contextConfig().t("rebuildingPreview");
2644
2806
  return `
2645
- <div class="segment segment-artifact-file"
2807
+ <div class="aparte-segment aparte-segment-artifact-file"
2646
2808
  data-segment-id="${escapeHtml(segment.id)}"
2647
2809
  data-artifact-type="${escapeHtml(kind)}"
2648
2810
  data-state="${isStreaming ? "streaming" : "compiling"}">
@@ -2653,7 +2815,7 @@ function renderBinaryFileArtifact(segment, kind) {
2653
2815
  <div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(subText)}</div>
2654
2816
  </div>
2655
2817
  <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>' : ""}
2818
+ ${canRedownload ? `<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download" disabled>${downloadLabel}</button>` : ""}
2657
2819
  </div>
2658
2820
  </div>
2659
2821
  <div class="aparte-art-file__body">
@@ -2759,12 +2921,11 @@ function updateBinaryFileArtifact(element, segment, isStreaming) {
2759
2921
  const state = element.getAttribute("data-state");
2760
2922
  if (state === "ready") return;
2761
2923
  const cleanContent = stripCodeFences(segment.content || "");
2762
- const codeEl = element.querySelector('[data-role="code-pane"] code');
2763
- if (codeEl) {
2764
- codeEl.textContent = cleanContent;
2765
- }
2766
2924
  if (isStreaming) {
2767
- debounceHighlight(element, '[data-role="code-pane"]', cleanContent, "js", segment.id);
2925
+ streamHighlight(element, '[data-role="code-pane"]', cleanContent, "js", segment.id);
2926
+ } else {
2927
+ const codeEl = element.querySelector('[data-role="code-pane"] code');
2928
+ if (codeEl) codeEl.textContent = cleanContent;
2768
2929
  }
2769
2930
  if (!isStreaming && state === "streaming") {
2770
2931
  element.setAttribute("data-state", "compiling");
@@ -2811,9 +2972,9 @@ function showSandboxError(element, phase, errorMsg) {
2811
2972
  const short = (errorMsg.split("\n")[0] ?? "").slice(0, 240);
2812
2973
  body.innerHTML = `
2813
2974
  <div class="aparte-art-file__error">
2814
- <div class="aparte-art-file__error-title">The sandbox failed during generation.</div>
2975
+ <div class="aparte-art-file__error-title">${escapeHtml(contextConfig().t("sandboxError"))}</div>
2815
2976
  <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>
2977
+ <div class="aparte-art-file__error-hint">${escapeHtml(contextConfig().t("sandboxErrorHint"))}</div>
2817
2978
  </div>
2818
2979
  `;
2819
2980
  }
@@ -2831,10 +2992,14 @@ const artifactRenderer = {
2831
2992
  const displayLang = languageForKind(kind);
2832
2993
  const isStreaming = !!segment.isStreaming;
2833
2994
  const previewable = PREVIEWABLE_KINDS.has(kind);
2995
+ const cfg = contextConfig();
2996
+ const downloadLabel = cfg.t("download");
2997
+ const previewLabel = cfg.t("preview");
2998
+ const codeLabel = cfg.t("code");
2834
2999
  const isBinary = BINARY_FILE_KINDS.has(kind);
2835
3000
  const cleanContent = stripCodeFences(segment.content || "");
2836
3001
  return `
2837
- <div class="segment segment-artifact-card"
3002
+ <div class="aparte-segment aparte-segment-artifact-card"
2838
3003
  data-segment-id="${escapeHtml(segment.id)}"
2839
3004
  data-artifact-type="${escapeHtml(kind)}"
2840
3005
  data-streaming="${isStreaming ? "true" : "false"}"
@@ -2848,33 +3013,64 @@ const artifactRenderer = {
2848
3013
  ${isStreaming ? '<span class="aparte-art-card__pulse" aria-label="Streaming"></span>' : ""}
2849
3014
  </div>
2850
3015
  <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">
3016
+ <button type="button" class="aparte-art-card__btn" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}" aria-label="${escapeAttr(contextConfig().t("copy"))}">
2852
3017
  ${contextConfig().getIcon("copy")}
2853
3018
  </button>
2854
- <button type="button" class="aparte-art-card__btn" data-action="download" title="Download" aria-label="Download" ${isStreaming ? "disabled" : ""}>
3019
+ <button type="button" class="aparte-art-card__btn" data-action="download" title="${escapeAttr(downloadLabel)}" aria-label="${escapeAttr(downloadLabel)}" ${isStreaming ? "disabled" : ""}>
2855
3020
  <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
3021
  </button>
2857
3022
  </div>
2858
3023
  </header>
2859
3024
  <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>
3025
+ <button type="button" role="tab" data-tab-target="code" aria-selected="true">${escapeHtml(codeLabel)}</button>
3026
+ ${previewable ? `<button type="button" role="tab" data-tab-target="preview" aria-selected="false" ${isStreaming ? "disabled" : ""}>${escapeHtml(previewLabel)}</button>` : ""}
2862
3027
  </nav>
2863
3028
  <div class="aparte-art-card__body">
2864
3029
  <div class="aparte-art-card__pane" data-pane="code">
2865
- <div class="code-content-wrapper">
3030
+ <div class="aparte-code-content-wrapper">
2866
3031
  <pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>
2867
3032
  </div>
2868
3033
  </div>
2869
3034
  ${previewable ? `
2870
3035
  <div class="aparte-art-card__pane" data-pane="preview">
2871
- <div class="aparte-art-card__pending">Press Preview to run this artifact.</div>
3036
+ <div class="aparte-art-card__pending">${escapeHtml(cfg.t("previewPending"))}</div>
2872
3037
  </div>
2873
3038
  ` : ""}
2874
3039
  </div>
2875
3040
  </div>
2876
3041
  `;
2877
3042
  },
3043
+ /**
3044
+ * Every string this card shows: the copy button's tooltip, glyph and accessible
3045
+ * name, the download button's title and label, and the two tab names. They were
3046
+ * all hardcoded literals until they got locale keys — including the copy button's
3047
+ * `aria-label`, which said "Copy" while its own `title` one attribute away already
3048
+ * went through `t('copy')`, so a French reader got a French tooltip and an English
3049
+ * announcement.
3050
+ *
3051
+ * Nothing here touches the tab state or the preview pane: a mounted iframe is
3052
+ * running model-authored code, and re-rendering this card is exactly what the
3053
+ * hook exists to avoid.
3054
+ */
3055
+ relabel: (element) => {
3056
+ const cfg = contextConfig();
3057
+ const copyBtn = element.querySelector('.aparte-art-card__btn[data-action="copy"]');
3058
+ if (copyBtn) {
3059
+ copyBtn.setAttribute("title", cfg.t("copy"));
3060
+ copyBtn.setAttribute("aria-label", cfg.t("copy"));
3061
+ copyBtn.innerHTML = cfg.getIcon("copy");
3062
+ }
3063
+ const dl = element.querySelector('.aparte-art-card__btn[data-action="download"]');
3064
+ if (dl) {
3065
+ const label = cfg.t("download");
3066
+ dl.setAttribute("title", label);
3067
+ dl.setAttribute("aria-label", label);
3068
+ }
3069
+ const previewTab = element.querySelector('[data-tab-target="preview"]');
3070
+ if (previewTab) previewTab.textContent = cfg.t("preview");
3071
+ const codeTab = element.querySelector('[data-tab-target="code"]');
3072
+ if (codeTab) codeTab.textContent = cfg.t("code");
3073
+ },
2878
3074
  setup: (element, segment) => {
2879
3075
  latestSegment.set(element, segment);
2880
3076
  const kind = (segment.artifactType || "").toLowerCase();
@@ -2882,7 +3078,7 @@ const artifactRenderer = {
2882
3078
  setupBinaryFileArtifact(element, segment, kind);
2883
3079
  return;
2884
3080
  }
2885
- const wrapper = element.querySelector(".code-content-wrapper");
3081
+ const wrapper = element.querySelector(".aparte-code-content-wrapper");
2886
3082
  if (wrapper) {
2887
3083
  const displayLang = languageForKind(kind);
2888
3084
  const cleanContent = stripCodeFences(segment.content || "");
@@ -2955,23 +3151,25 @@ const artifactRenderer = {
2955
3151
  }
2956
3152
  const wasStreaming = element.getAttribute("data-streaming") === "true";
2957
3153
  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
3154
  if (isStreaming) {
2969
3155
  const segId = element.getAttribute("data-segment-id") ?? segment.id;
2970
- debounceHighlight(element, ".code-content-wrapper", cleanContent, languageForKind(kind), segId);
3156
+ streamHighlight(element, ".aparte-code-content-wrapper", cleanContent, languageForKind(kind), segId);
3157
+ } else {
3158
+ const codeEl = element.querySelector(".aparte-code-content-wrapper code");
3159
+ if (codeEl) {
3160
+ codeEl.textContent = cleanContent;
3161
+ } else {
3162
+ const wrapper = element.querySelector(".aparte-code-content-wrapper");
3163
+ if (wrapper) {
3164
+ const displayLang = languageForKind(kind);
3165
+ wrapper.innerHTML = `<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>`;
3166
+ }
3167
+ }
2971
3168
  }
2972
3169
  if (wasStreaming && !isStreaming) {
2973
3170
  element.setAttribute("data-streaming", "false");
2974
- const wrapper = element.querySelector(".code-content-wrapper");
3171
+ element.querySelector(".aparte-art-card__pulse")?.remove();
3172
+ const wrapper = element.querySelector(".aparte-code-content-wrapper");
2975
3173
  if (wrapper) {
2976
3174
  const displayLang = languageForKind(kind);
2977
3175
  void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
@@ -2984,258 +3182,10 @@ const artifactRenderer = {
2984
3182
  });
2985
3183
  }
2986
3184
  },
2987
- getStyles: () => `
2988
- .segment-artifact-card {
2989
- display: flex; flex-direction: column;
2990
- margin: 8px 0;
2991
- border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
2992
- border-radius: 12px;
2993
- background: var(--aparte-surface, #fff);
2994
- overflow: hidden;
2995
- font: inherit;
2996
- color: var(--aparte-text, inherit);
2997
- }
2998
- .aparte-art-card__header {
2999
- display: flex; align-items: center; justify-content: space-between;
3000
- padding: 8px 10px;
3001
- border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
3002
- background: var(--aparte-surface-2, rgba(0,0,0,0.02));
3003
- min-height: 36px;
3004
- }
3005
- .aparte-art-card__title-block { display: flex; align-items: center; gap: 8px; min-width: 0; }
3006
- .aparte-art-card__kind {
3007
- font-size: 0.7rem; font-weight: 600;
3008
- text-transform: uppercase; letter-spacing: 0.04em;
3009
- padding: 2px 6px; border-radius: 4px;
3010
- background: var(--aparte-surface, #fff);
3011
- border: 1px solid var(--aparte-border, rgba(0,0,0,0.1));
3012
- color: var(--aparte-text-muted, rgba(0,0,0,0.6));
3013
- }
3014
- .aparte-art-card__title {
3015
- font-size: 0.85rem; font-weight: 500;
3016
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
3017
- }
3018
- .aparte-art-card__pulse {
3019
- width: 8px; height: 8px; border-radius: 50%;
3020
- background: var(--aparte-accent, #3b82f6);
3021
- animation: aparte-art-card-pulse 1.2s ease-in-out infinite;
3022
- }
3023
- @keyframes aparte-art-card-pulse {
3024
- 0%, 100% { opacity: 0.35; transform: scale(0.85); }
3025
- 50% { opacity: 1; transform: scale(1.1); }
3026
- }
3027
- .aparte-art-card__actions { display: flex; gap: 4px; }
3028
- .aparte-art-card__btn {
3029
- display: inline-flex; align-items: center; justify-content: center;
3030
- width: 28px; height: 28px;
3031
- background: transparent;
3032
- border: 1px solid transparent;
3033
- border-radius: 6px;
3034
- color: var(--aparte-text-muted, rgba(0,0,0,0.55));
3035
- cursor: pointer;
3036
- transition: background 0.12s, border-color 0.12s, color 0.12s;
3037
- }
3038
- .aparte-art-card__btn:hover:not(:disabled) {
3039
- background: var(--aparte-surface-hover, rgba(0,0,0,0.04));
3040
- border-color: var(--aparte-border, rgba(0,0,0,0.1));
3041
- color: var(--aparte-text, inherit);
3042
- }
3043
- .aparte-art-card__btn:disabled { opacity: 0.4; cursor: not-allowed; }
3044
- .aparte-art-card__tabs {
3045
- display: flex; gap: 2px;
3046
- padding: 4px 8px 0;
3047
- border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
3048
- background: var(--aparte-surface-2, rgba(0,0,0,0.02));
3049
- }
3050
- .aparte-art-card__tabs button {
3051
- padding: 6px 12px;
3052
- font-size: 0.78rem;
3053
- background: transparent;
3054
- border: 1px solid transparent;
3055
- border-bottom: none;
3056
- border-radius: 6px 6px 0 0;
3057
- color: var(--aparte-text-muted, rgba(0,0,0,0.6));
3058
- cursor: pointer;
3059
- transition: background 0.12s, color 0.12s;
3060
- }
3061
- .aparte-art-card__tabs button[aria-selected="true"] {
3062
- background: var(--aparte-surface, #fff);
3063
- border-color: var(--aparte-border, rgba(0,0,0,0.08));
3064
- color: var(--aparte-text, inherit);
3065
- font-weight: 500;
3066
- }
3067
- .aparte-art-card__tabs button:disabled { opacity: 0.4; cursor: not-allowed; }
3068
- .aparte-art-card__body {
3069
- position: relative;
3070
- min-height: 80px;
3071
- max-height: 600px;
3072
- overflow: hidden;
3073
- }
3074
- .aparte-art-card__pane { display: none; height: 100%; }
3075
- .segment-artifact-card[data-tab="code"] .aparte-art-card__pane[data-pane="code"] { display: block; }
3076
- .segment-artifact-card[data-tab="preview"] .aparte-art-card__pane[data-pane="preview"] { display: block; }
3077
- .aparte-art-card__pane[data-pane="code"] {
3078
- max-height: 600px; overflow: auto;
3079
- }
3080
- .aparte-art-card__pane[data-pane="code"] pre {
3081
- margin: 0; padding: 12px;
3082
- font-size: 0.82rem;
3083
- background: var(--aparte-code-bg, #fafafa);
3084
- }
3085
- .aparte-art-card__frame {
3086
- display: block;
3087
- width: 100%; height: 480px;
3088
- border: 0;
3089
- background: #fff;
3090
- }
3091
- .aparte-art-card__pending {
3092
- display: flex; align-items: center; justify-content: center;
3093
- height: 120px;
3094
- color: var(--aparte-text-muted, rgba(0,0,0,0.5));
3095
- font-size: 0.85rem;
3096
- font-style: italic;
3097
- }
3098
- /* ── Binary file artifact (xlsx/pdf/docx) ──────────────────── */
3099
- .segment-artifact-file {
3100
- display: flex; flex-direction: column;
3101
- margin: 8px 0;
3102
- border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
3103
- border-radius: 12px;
3104
- background: var(--aparte-surface, #fff);
3105
- overflow: hidden;
3106
- color: var(--aparte-text, inherit);
3107
- font: inherit;
3108
- }
3109
- .aparte-art-file__card {
3110
- display: flex; align-items: center; gap: 12px;
3111
- padding: 12px 14px;
3112
- background: var(--aparte-surface-2, rgba(0,0,0,0.02));
3113
- border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
3114
- }
3115
- .aparte-art-file__body {
3116
- position: relative;
3117
- }
3118
- .aparte-art-file__code-pane {
3119
- max-height: 360px; overflow: auto;
3120
- background: var(--aparte-code-bg, #fafafa);
3121
- }
3122
- .aparte-art-file__code-pane pre {
3123
- margin: 0; padding: 12px;
3124
- font-size: 0.82rem;
3125
- font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
3126
- }
3127
- .aparte-art-file__preview-pane {
3128
- max-height: 460px;
3129
- overflow: auto;
3130
- background: #fff;
3131
- /* Preview is a document view — force light scheme regardless of
3132
- the app theme, with a dark text colour so cells stay readable. */
3133
- color: #1f2937;
3134
- }
3135
- .aparte-art-file__icon {
3136
- width: 40px; height: 40px;
3137
- border-radius: 8px;
3138
- display: flex; align-items: center; justify-content: center;
3139
- background: linear-gradient(135deg, #1d6f42, #0f5132);
3140
- color: #fff;
3141
- font-weight: 700;
3142
- font-size: 0.78rem;
3143
- letter-spacing: 0.04em;
3144
- flex-shrink: 0;
3145
- }
3146
- .aparte-art-file__icon[data-kind="pdf"] { background: linear-gradient(135deg, #c0392b, #7d1f17); }
3147
- .aparte-art-file__icon[data-kind="docx"] { background: linear-gradient(135deg, #1e5288, #0f3060); }
3148
- .aparte-art-file__meta { flex: 1 1 auto; min-width: 0; }
3149
- .aparte-art-file__meta-name {
3150
- font-weight: 600; font-size: 0.92rem;
3151
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
3152
- }
3153
- .aparte-art-file__meta-sub {
3154
- font-size: 0.78rem;
3155
- color: var(--aparte-text-muted, rgba(0,0,0,0.55));
3156
- }
3157
- .aparte-art-file__actions { display: flex; gap: 6px; flex-shrink: 0; }
3158
- .aparte-art-file__btn {
3159
- border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
3160
- background: var(--aparte-surface, #fff);
3161
- color: var(--aparte-text, inherit);
3162
- padding: 6px 10px;
3163
- border-radius: 6px;
3164
- font-size: 0.8rem;
3165
- cursor: pointer;
3166
- transition: background 0.12s ease;
3167
- }
3168
- .aparte-art-file__btn:hover { background: var(--aparte-surface-2, rgba(0,0,0,0.05)); }
3169
- .aparte-art-file__btn--primary {
3170
- background: var(--aparte-primary, #6366f1);
3171
- color: #fff;
3172
- border-color: var(--aparte-primary, #6366f1);
3173
- }
3174
- .aparte-art-file__btn--primary:hover { filter: brightness(1.07); }
3175
- /* Hide the download button until the sandbox has produced the buffer.
3176
- Avoids confusing the user with a disabled-but-styled-primary button
3177
- during streaming / compiling. */
3178
- .segment-artifact-file:not([data-state="ready"]) .aparte-art-file__btn[data-action="download"] {
3179
- display: none;
3180
- }
3181
- .aparte-art-file__preview-pane table {
3182
- border-collapse: collapse;
3183
- width: 100%;
3184
- font-size: 0.82rem;
3185
- font-family: inherit;
3186
- }
3187
- .aparte-art-file__preview-pane th,
3188
- .aparte-art-file__preview-pane td {
3189
- border: 1px solid rgba(0,0,0,0.10);
3190
- padding: 6px 10px;
3191
- text-align: left;
3192
- white-space: nowrap;
3193
- color: #1f2937;
3194
- background: #fff;
3195
- }
3196
- .aparte-art-file__preview-pane tr:nth-child(odd) td { background: #f9fafb; }
3197
- .aparte-art-file__preview-pane tr:first-child td {
3198
- background: #f3f4f6;
3199
- font-weight: 600;
3200
- position: sticky; top: 0;
3201
- color: #111827;
3202
- }
3203
- .aparte-art-file__preview-empty {
3204
- padding: 20px; text-align: center;
3205
- color: var(--aparte-text-muted, rgba(0,0,0,0.5));
3206
- font-size: 0.85rem; font-style: italic;
3207
- }
3208
- .segment-artifact-file[data-state="error"] .aparte-art-file__icon {
3209
- background: linear-gradient(135deg, #c0392b, #7d1f17);
3210
- }
3211
- .aparte-art-file__error {
3212
- padding: 16px 18px;
3213
- background: rgba(239, 68, 68, 0.05);
3214
- border-top: 1px solid rgba(239, 68, 68, 0.2);
3215
- color: var(--aparte-text, inherit);
3216
- }
3217
- .aparte-art-file__error-title {
3218
- font-weight: 600;
3219
- font-size: 0.9rem;
3220
- margin-bottom: 6px;
3221
- color: #b91c1c;
3222
- }
3223
- .aparte-art-file__error-msg {
3224
- font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
3225
- font-size: 0.78rem;
3226
- padding: 6px 10px;
3227
- background: rgba(0,0,0,0.04);
3228
- border-radius: 4px;
3229
- margin-bottom: 8px;
3230
- color: #7f1d1d;
3231
- word-break: break-word;
3232
- }
3233
- .aparte-art-file__error-hint {
3234
- font-size: 0.78rem;
3235
- color: var(--aparte-text-muted, rgba(0,0,0,0.55));
3236
- font-style: italic;
3237
- }
3238
- `
3185
+ // CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
3186
+ // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
3187
+ // has no other way onto the page.
3188
+ getStyles: () => ""
3239
3189
  };
3240
3190
  function languageForKind(kind) {
3241
3191
  if (kind === "react") return "jsx";
@@ -3385,10 +3335,7 @@ const DEFAULT_RENDERERS = [
3385
3335
  textRenderer,
3386
3336
  thinkingRenderer,
3387
3337
  codeRenderer,
3388
- terminalRenderer,
3389
3338
  errorRenderer,
3390
- progressRenderer,
3391
- fileTreeRenderer,
3392
3339
  toolCallRenderer,
3393
3340
  artifactRenderer,
3394
3341
  pipelineWaitingRenderer
@@ -3398,14 +3345,48 @@ const ACTIVITY_KEYS = ["content", "output", "result"];
3398
3345
  function isActivity(updates) {
3399
3346
  return ACTIVITY_KEYS.some((key) => key in updates);
3400
3347
  }
3401
- function stampSegmentOnInsert(segments, segment, messageId) {
3348
+ function stampSegmentOnInsert(segments, segment, messageId, defaults) {
3349
+ const base = applyDefaults(segment, defaults);
3402
3350
  return {
3403
- ...segment,
3351
+ ...base,
3404
3352
  messageId: segment.messageId ?? messageId,
3405
3353
  index: segment.index ?? segments.length,
3406
- startedAt: segment.startedAt ?? Date.now()
3354
+ // A start already present is kept — a segment a consumer rebuilt from their own
3355
+ // storage keeps the number it was persisted with. `adoptSegment` is the path
3356
+ // that never writes one at all.
3357
+ ...timingPatch(base, { startedAt: segmentTiming(segment)?.startedAt ?? Date.now() })
3358
+ };
3359
+ }
3360
+ function adoptSegment(segments, segment, messageId) {
3361
+ return {
3362
+ ...segment,
3363
+ messageId,
3364
+ index: segments.length,
3365
+ isStreaming: false,
3366
+ ...segment.type === "tool_call" && segment.status === "awaiting-approval" ? { status: "aborted" } : {}
3407
3367
  };
3408
3368
  }
3369
+ const RESERVED = /* @__PURE__ */ new Set(["id", "type", "messageId", "index"]);
3370
+ function segmentTiming(segment) {
3371
+ return segment.meta?.aparte;
3372
+ }
3373
+ function timingPatch(segment, timing) {
3374
+ return { meta: { ...segment.meta, aparte: { ...segment.meta?.aparte, ...timing } } };
3375
+ }
3376
+ function applyDefaults(segment, defaults) {
3377
+ if (!defaults) return segment;
3378
+ const out = { ...segment };
3379
+ for (const [key, value] of Object.entries(defaults)) {
3380
+ if (RESERVED.has(key) || key in segment) continue;
3381
+ if (key === "meta") {
3382
+ const { aparte: _forged, ...rest } = value ?? {};
3383
+ out.meta = rest;
3384
+ continue;
3385
+ }
3386
+ out[key] = value;
3387
+ }
3388
+ return out;
3389
+ }
3409
3390
  function isSegmentSettled(segment) {
3410
3391
  if (segment.type === "tool_call") return !OPEN_TOOL_STATUSES.has(segment.status);
3411
3392
  return segment.isStreaming === false;
@@ -3414,17 +3395,32 @@ function stampSegmentOnUpdate(segment, updates) {
3414
3395
  if (isSegmentSettled(segment)) return updates;
3415
3396
  const merged = { ...segment, ...updates };
3416
3397
  if (isSegmentSettled(merged)) {
3417
- return { ...updates, endedAt: segment.endedAt ?? Date.now() };
3398
+ const ended = segmentTiming(segment)?.endedAt;
3399
+ return { ...updates, ...timingPatch(segment, { endedAt: ended ?? Date.now() }) };
3418
3400
  }
3419
- return isActivity(updates) ? { ...updates, endedAt: Date.now() } : updates;
3401
+ return isActivity(updates) ? { ...updates, ...timingPatch(segment, { endedAt: Date.now() }) } : updates;
3402
+ }
3403
+ function mergeSegmentUpdate(segment, updates) {
3404
+ const merged = { ...segment, ...updates };
3405
+ if (!("meta" in updates)) return merged;
3406
+ return {
3407
+ ...merged,
3408
+ meta: {
3409
+ ...segment.meta,
3410
+ ...updates.meta,
3411
+ // Neither side's timing wins by accident: whichever one carries it, wins,
3412
+ // and a consumer writing `meta` without an `aparte` key keeps ours.
3413
+ ...segment.meta?.aparte || updates.meta?.aparte ? { aparte: { ...segment.meta?.aparte, ...updates.meta?.aparte } } : {}
3414
+ }
3415
+ };
3420
3416
  }
3421
3417
  function segmentDuration(segment) {
3422
- const { startedAt, endedAt } = segment;
3418
+ const { startedAt, endedAt } = segmentTiming(segment) ?? {};
3423
3419
  if (startedAt == null || endedAt == null) return void 0;
3424
3420
  return endedAt - startedAt;
3425
3421
  }
3426
3422
  function stampSegmentActivity(segment) {
3427
- return isSegmentSettled(segment) ? {} : { endedAt: Date.now() };
3423
+ return isSegmentSettled(segment) ? {} : timingPatch(segment, { endedAt: Date.now() });
3428
3424
  }
3429
3425
  function segmentContentUpdate(segment) {
3430
3426
  const content = segment.content;
@@ -3439,6 +3435,16 @@ function isTerminalStatus(status) {
3439
3435
  function openSegmentIds(segments) {
3440
3436
  return segments.filter((segment) => !isSegmentSettled(segment)).map((segment) => segment.id);
3441
3437
  }
3438
+ function adoptMessageSegments(message) {
3439
+ if (!message.segments?.length) return { ...message };
3440
+ return {
3441
+ ...message,
3442
+ segments: message.segments.reduce((acc, segment) => {
3443
+ acc.push(adoptSegment(acc, segment, message.id));
3444
+ return acc;
3445
+ }, [])
3446
+ };
3447
+ }
3442
3448
  function renumberSegments(segments) {
3443
3449
  for (let i = 0; i < segments.length; i++) {
3444
3450
  const segment = segments[i];
@@ -4139,10 +4145,14 @@ class AparteChatHost {
4139
4145
  // Controller-driven flushes mirror AparteConversationManager state, not
4140
4146
  // user intent — deliberately NOT emitted as `messagesChange` to
4141
4147
  // avoid a feedback loop with hosts using a two-way `messages` input.
4148
+ // Adopted here rather than passed through: this is the conversation
4149
+ // controller handing back a stored list, and it goes STRAIGHT to the
4150
+ // framework's own reactive setter — so in every wrapper it never met the
4151
+ // stamping seam at all, tree or no tree.
4142
4152
  setMessages: (msgs) => {
4143
4153
  this._beginConversationSwap();
4144
4154
  this._vp()?.resetSpacer?.();
4145
- this.binding.setMessages([...msgs]);
4155
+ this.binding.setMessages(msgs.map(adoptMessageSegments));
4146
4156
  },
4147
4157
  appendMessage: (msg) => this.appendMessage(msg),
4148
4158
  getMessages: () => this.binding.getMessages(),
@@ -4193,11 +4203,23 @@ class AparteChatHost {
4193
4203
  * spec guards ("optimistic-append + parent-push race"). `onMessageAppended`
4194
4204
  * is the append-specific signal instead.
4195
4205
  */
4196
- appendMessage(message) {
4206
+ appendMessage(message, options) {
4197
4207
  this._flushStreamState();
4198
4208
  if (message.role === "user") this._vp()?.setAutoScroll?.(true);
4199
- this.binding.setMessages([...this.binding.getMessages(), message]);
4200
- this.binding.onMessageAppended?.(message);
4209
+ const stored = options?.historical ? adoptMessageSegments(message) : message.segments?.length ? {
4210
+ ...message,
4211
+ segments: message.segments.reduce((acc, segment) => {
4212
+ acc.push(stampSegmentOnInsert(
4213
+ acc,
4214
+ segment,
4215
+ message.id,
4216
+ resolveConfig(this.binding.host).getSegmentDefaults(segment.type)
4217
+ ));
4218
+ return acc;
4219
+ }, [])
4220
+ } : message;
4221
+ this.binding.setMessages([...this.binding.getMessages(), stored]);
4222
+ this.binding.onMessageAppended?.(stored);
4201
4223
  }
4202
4224
  /** Atomic partial update of a message by id. */
4203
4225
  updateMessage(messageId, updates) {
@@ -4240,7 +4262,15 @@ class AparteChatHost {
4240
4262
  this.binding.onTypingChange?.(false);
4241
4263
  const msgs = before;
4242
4264
  const last = target;
4243
- const stamped = last ? stampSegmentOnInsert(last.segments || [], segment, last.id) : segment;
4265
+ const stamped = last ? stampSegmentOnInsert(
4266
+ last.segments || [],
4267
+ segment,
4268
+ last.id,
4269
+ // The host attaches its config to `binding.host`, so resolving from
4270
+ // that element is what gives a framework-managed chat its OWN
4271
+ // defaults rather than the page's.
4272
+ resolveConfig(this.binding.host).getSegmentDefaults(segment.type)
4273
+ ) : segment;
4244
4274
  this._lastBubble()?.addSegment?.(stamped);
4245
4275
  if (!last) return;
4246
4276
  const segments = [...last.segments || [], stamped];
@@ -4259,7 +4289,7 @@ class AparteChatHost {
4259
4289
  this._lastBubble()?.updateSegment?.(segmentId, stamped);
4260
4290
  if (!last || !last.segments) return;
4261
4291
  const segments = last.segments.map(
4262
- (s) => s.id === segmentId ? { ...s, ...stamped } : s
4292
+ (s) => s.id === segmentId ? mergeSegmentUpdate(s, stamped) : s
4263
4293
  );
4264
4294
  const next = [...msgs.slice(0, -1), { ...last, segments }];
4265
4295
  this.binding.setMessages(next);
@@ -5120,21 +5150,398 @@ function readableToAsyncIterable(stream, signal) {
5120
5150
  }
5121
5151
  };
5122
5152
  }
5123
- const DEFAULT_TOOL_HANDLER_TIMEOUT_MS = 5 * 60 * 1e3;
5124
- function withToolTimeout(work, controller, timeoutMs) {
5125
- let timer;
5126
- const timedOut = new Promise((_resolve, reject) => {
5127
- timer = setTimeout(() => {
5128
- controller.abort();
5129
- reject(Object.assign(new Error(`tool handler exceeded ${timeoutMs}ms`), { name: "AbortError" }));
5130
- }, timeoutMs);
5131
- });
5132
- return {
5133
- raced: Promise.race([work(controller.signal), timedOut]),
5134
- done: () => clearTimeout(timer)
5135
- };
5153
+ let _relabels = null;
5154
+ function onRelabel(fn) {
5155
+ _relabels?.push(fn);
5136
5156
  }
5137
- class AparteClient {
5157
+ function el$1(tag, className, text) {
5158
+ const node = document.createElement(tag);
5159
+ if (className) node.className = className;
5160
+ if (text != null) node.textContent = text;
5161
+ return node;
5162
+ }
5163
+ function fieldHeader(parent, field, skipTitle) {
5164
+ let titleId;
5165
+ if (field.title && field.title.trim() !== skipTitle?.trim()) {
5166
+ const title = el$1("p", "aparte-elic-title", field.title);
5167
+ titleId = `elic-title-${uuid()}`;
5168
+ title.id = titleId;
5169
+ parent.appendChild(title);
5170
+ }
5171
+ if (field.description) parent.appendChild(el$1("p", "aparte-elic-desc", field.description));
5172
+ return titleId;
5173
+ }
5174
+ function labelGroup(list, opts) {
5175
+ list.setAttribute("role", opts.multiple ? "group" : "radiogroup");
5176
+ if (opts.titleId) list.setAttribute("aria-labelledby", opts.titleId);
5177
+ else if (opts.fallbackLabel) list.setAttribute("aria-label", opts.fallbackLabel);
5178
+ }
5179
+ function buildEnumField(field, onChange, fallbackLabel) {
5180
+ const wrap = el$1("div", "aparte-elic-field aparte-elic-enum");
5181
+ const titleId = fieldHeader(wrap, field, fallbackLabel);
5182
+ const list = el$1("div", "aparte-elic-options");
5183
+ labelGroup(list, { multiple: field.multiple, titleId, fallbackLabel });
5184
+ const name = `elic-${uuid()}`;
5185
+ const type = field.multiple ? "checkbox" : "radio";
5186
+ const allowOther = field.allowOther ?? contextConfig().getElicitationOptions().allowOther;
5187
+ const defaults = new Set(Array.isArray(field.default) ? field.default : field.default != null ? [field.default] : []);
5188
+ const buildOption = (value, label, description, recommended) => {
5189
+ const row = el$1("label", "aparte-elic-option" + (recommended ? " aparte-elic-option--recommended" : ""));
5190
+ const control = el$1("input", "aparte-elic-control");
5191
+ control.type = type;
5192
+ control.name = name;
5193
+ control.value = value;
5194
+ if (defaults.has(value)) control.checked = true;
5195
+ const body = el$1("span", "aparte-elic-option-body");
5196
+ body.appendChild(el$1("span", "aparte-elic-option-title", label));
5197
+ if (description) body.appendChild(el$1("span", "aparte-elic-option-desc", description));
5198
+ row.append(control, body);
5199
+ return row;
5200
+ };
5201
+ for (const opt of field.options) {
5202
+ list.appendChild(buildOption(opt.value, opt.label ?? opt.value, opt.description, opt.recommended));
5203
+ }
5204
+ let otherText = null;
5205
+ if (allowOther) {
5206
+ const row = el$1("label", "aparte-elic-option aparte-elic-option--other");
5207
+ const control = el$1("input", "aparte-elic-control");
5208
+ control.type = type;
5209
+ control.name = name;
5210
+ control.value = "__other__";
5211
+ const body = el$1("span", "aparte-elic-option-body");
5212
+ const t = contextConfig();
5213
+ const otherTitle = el$1("span", "aparte-elic-option-title", t.t("elicitationOther"));
5214
+ body.appendChild(otherTitle);
5215
+ otherText = el$1("input", "aparte-elic-other-input");
5216
+ otherText.type = "text";
5217
+ otherText.placeholder = t.t("elicitationOtherPlaceholder");
5218
+ otherText.style.display = "none";
5219
+ otherText.setAttribute("aria-label", t.t("elicitationOtherLabel"));
5220
+ const otherInput = otherText;
5221
+ onRelabel(() => {
5222
+ const now = contextConfig();
5223
+ otherTitle.textContent = now.t("elicitationOther");
5224
+ otherInput.placeholder = now.t("elicitationOtherPlaceholder");
5225
+ otherInput.setAttribute("aria-label", now.t("elicitationOtherLabel"));
5226
+ });
5227
+ body.appendChild(otherText);
5228
+ row.append(control, body);
5229
+ list.appendChild(row);
5230
+ }
5231
+ wrap.appendChild(list);
5232
+ const controls = () => Array.from(list.querySelectorAll(".aparte-elic-control"));
5233
+ const other = () => list.querySelector('input[value="__other__"]');
5234
+ list.addEventListener("change", () => {
5235
+ const o = other();
5236
+ if (o && otherText) {
5237
+ otherText.style.display = o.checked ? "" : "none";
5238
+ if (o.checked) otherText.focus();
5239
+ }
5240
+ onChange();
5241
+ });
5242
+ otherText?.addEventListener("input", onChange);
5243
+ const getValue = () => {
5244
+ const otherVal = other()?.checked && otherText?.value.trim() ? otherText.value.trim() : "";
5245
+ if (field.multiple) {
5246
+ const vals = controls().filter((c) => c.checked && c.value !== "__other__").map((c) => c.value);
5247
+ if (otherVal) vals.push(otherVal);
5248
+ return vals;
5249
+ }
5250
+ const checked = controls().find((c) => c.checked);
5251
+ if (!checked) return "";
5252
+ return checked.value === "__other__" ? otherVal : checked.value;
5253
+ };
5254
+ return {
5255
+ el: wrap,
5256
+ getValue,
5257
+ isComplete: () => {
5258
+ const v = getValue();
5259
+ return field.multiple ? v.length > 0 : v !== "";
5260
+ },
5261
+ focus: () => controls()[0]?.focus()
5262
+ };
5263
+ }
5264
+ function buildBooleanField(field, onChange, fallbackLabel) {
5265
+ const wrap = el$1("div", "aparte-elic-field aparte-elic-boolean");
5266
+ const titleId = fieldHeader(wrap, field, fallbackLabel);
5267
+ const list = el$1("div", "aparte-elic-options");
5268
+ labelGroup(list, { titleId, fallbackLabel });
5269
+ const name = `elic-${uuid()}`;
5270
+ const mk = (val, label, defaulted) => {
5271
+ const row = el$1("label", "aparte-elic-option");
5272
+ const control = el$1("input", "aparte-elic-control");
5273
+ control.type = "radio";
5274
+ control.name = name;
5275
+ control.value = val;
5276
+ if (field.default != null && String(field.default) === val) control.checked = true;
5277
+ const body = el$1("span", "aparte-elic-option-body");
5278
+ const title = el$1("span", "aparte-elic-option-title", label);
5279
+ if (defaulted) onRelabel(() => {
5280
+ title.textContent = contextConfig().t(defaulted);
5281
+ });
5282
+ body.appendChild(title);
5283
+ row.append(control, body);
5284
+ list.appendChild(row);
5285
+ };
5286
+ const t = contextConfig();
5287
+ mk("true", field.trueLabel ?? t.t("elicitationYes"), field.trueLabel ? void 0 : "elicitationYes");
5288
+ mk("false", field.falseLabel ?? t.t("elicitationNo"), field.falseLabel ? void 0 : "elicitationNo");
5289
+ wrap.appendChild(list);
5290
+ list.addEventListener("change", onChange);
5291
+ const checked = () => list.querySelector("input:checked");
5292
+ return {
5293
+ el: wrap,
5294
+ getValue: () => checked()?.value === "true",
5295
+ isComplete: () => checked() != null,
5296
+ focus: () => list.querySelector("input")?.focus()
5297
+ };
5298
+ }
5299
+ function buildStringField(field, onChange, fallbackLabel) {
5300
+ const wrap = el$1("div", "aparte-elic-field aparte-elic-string");
5301
+ fieldHeader(wrap, field, fallbackLabel);
5302
+ const input = field.multiline ? el$1("textarea", "aparte-elic-text") : el$1("input", "aparte-elic-text");
5303
+ if (!field.multiline) input.type = "text";
5304
+ if (field.placeholder) input.setAttribute("placeholder", field.placeholder);
5305
+ if (field.default) input.value = field.default;
5306
+ if (field.maxLength != null) input.setAttribute("maxlength", String(field.maxLength));
5307
+ const ownName = field.title || field.description || fallbackLabel || void 0;
5308
+ input.setAttribute("aria-label", ownName ?? contextConfig().t("elicitationAnswerLabel"));
5309
+ if (ownName == null) {
5310
+ const named = input;
5311
+ onRelabel(() => {
5312
+ named.setAttribute("aria-label", contextConfig().t("elicitationAnswerLabel"));
5313
+ });
5314
+ }
5315
+ wrap.appendChild(input);
5316
+ input.addEventListener("input", onChange);
5317
+ const required = field.required ?? true;
5318
+ const value = () => input.value;
5319
+ return {
5320
+ el: wrap,
5321
+ getValue: () => value(),
5322
+ isComplete: () => {
5323
+ const v = value().trim();
5324
+ if (!required && v === "") return true;
5325
+ return v.length >= (field.minLength ?? 1);
5326
+ },
5327
+ focus: () => input.focus()
5328
+ };
5329
+ }
5330
+ function buildField(field, onChange, fallbackLabel, key) {
5331
+ const custom = contextConfig().getElicitationFieldRenderer()?.(field, { key, notifyChange: onChange });
5332
+ if (custom) {
5333
+ return {
5334
+ el: custom.el,
5335
+ getValue: () => custom.getValue(),
5336
+ isComplete: () => custom.isComplete(),
5337
+ focus: () => custom.focus?.()
5338
+ };
5339
+ }
5340
+ switch (field.type) {
5341
+ case "enum":
5342
+ return buildEnumField(field, onChange, fallbackLabel);
5343
+ case "boolean":
5344
+ return buildBooleanField(field, onChange, fallbackLabel);
5345
+ case "string":
5346
+ return buildStringField(field, onChange, fallbackLabel);
5347
+ }
5348
+ }
5349
+ function buildElicitationPanel(message, schema, onChange) {
5350
+ const relabels = [];
5351
+ const outer = _relabels;
5352
+ _relabels = relabels;
5353
+ try {
5354
+ const built = buildPanel(message, schema, onChange);
5355
+ return { ...built, relabel: () => {
5356
+ for (const fn of relabels) fn();
5357
+ } };
5358
+ } finally {
5359
+ _relabels = outer;
5360
+ }
5361
+ }
5362
+ function buildPanel(message, schema, onChange) {
5363
+ const panel = el$1("div", "aparte-elic-panel");
5364
+ const asked = typeof message === "function" ? message() : message;
5365
+ if (asked) panel.appendChild(el$1("p", "aparte-elic-message", asked));
5366
+ const body = el$1("div", "aparte-elic-body");
5367
+ panel.appendChild(body);
5368
+ const dismiss = el$1("div", "aparte-elic-dismiss");
5369
+ panel.appendChild(dismiss);
5370
+ if (schema.type === "object") {
5371
+ let show = function(index) {
5372
+ current = index;
5373
+ fields.forEach((f, i) => {
5374
+ f.field.el.hidden = i !== index;
5375
+ });
5376
+ syncNav();
5377
+ };
5378
+ const entries = Object.entries(schema.properties);
5379
+ const requiredKeys = new Set(schema.required ?? entries.map(([k]) => k));
5380
+ const cfg = contextConfig();
5381
+ let syncNav = () => {
5382
+ };
5383
+ const notify = () => {
5384
+ syncNav();
5385
+ onChange();
5386
+ };
5387
+ const fields = entries.map(([key, field2]) => {
5388
+ const built = buildField(field2, notify, void 0, key);
5389
+ if (!field2.title && !built.el.querySelector(".aparte-elic-title")) {
5390
+ built.el.insertBefore(el$1("p", "aparte-elic-title", key), built.el.firstChild);
5391
+ }
5392
+ body.appendChild(built.el);
5393
+ return { key, field: built, required: requiredKeys.has(key), header: field2.header };
5394
+ });
5395
+ const isComplete = () => fields.every((f) => !f.required || f.field.isComplete());
5396
+ const api = {
5397
+ el: panel,
5398
+ dismiss,
5399
+ getContent: () => Object.fromEntries(fields.map((f) => [f.key, f.field.getValue()])),
5400
+ isComplete,
5401
+ focus: () => fields[0]?.field.focus(),
5402
+ // A form of one behaves like a single field; only a real form steps.
5403
+ mode: () => "submit",
5404
+ canProceed: isComplete,
5405
+ proceed: () => {
5406
+ }
5407
+ };
5408
+ if (fields.length < 2 || cfg.getElicitationOptions().layout !== "stepped") return api;
5409
+ panel.classList.add("aparte-elic-panel--stepped");
5410
+ let current = 0;
5411
+ const steps = el$1("div", "aparte-elic-steps");
5412
+ steps.setAttribute("role", "tablist");
5413
+ const chips = fields.map((f, i) => {
5414
+ const chip = el$1("button", "aparte-elic-step");
5415
+ chip.type = "button";
5416
+ chip.setAttribute("role", "tab");
5417
+ chip.textContent = f.header?.trim() || String(i + 1);
5418
+ chip.addEventListener("click", () => {
5419
+ show(i);
5420
+ f.field.focus();
5421
+ });
5422
+ steps.appendChild(chip);
5423
+ return chip;
5424
+ });
5425
+ panel.insertBefore(steps, body);
5426
+ syncNav = () => {
5427
+ chips.forEach((chip, i) => {
5428
+ chip.setAttribute("aria-selected", String(i === current));
5429
+ chip.toggleAttribute("data-answered", fields[i].field.isComplete());
5430
+ });
5431
+ };
5432
+ show(0);
5433
+ const last = () => current === fields.length - 1;
5434
+ return {
5435
+ ...api,
5436
+ focus: () => fields[current]?.field.focus(),
5437
+ mode: () => last() ? "submit" : "advance",
5438
+ // Advancing needs only THIS question answered; submitting needs them all,
5439
+ // which is what stops the last question from accepting a form with a hole
5440
+ // in it.
5441
+ canProceed: () => last() ? isComplete() : !!fields[current]?.field.isComplete(),
5442
+ proceed: () => {
5443
+ if (last()) return;
5444
+ show(current + 1);
5445
+ fields[current]?.field.focus();
5446
+ }
5447
+ };
5448
+ }
5449
+ const field = buildField(schema, onChange, asked);
5450
+ body.appendChild(field.el);
5451
+ return {
5452
+ el: panel,
5453
+ dismiss,
5454
+ getContent: () => field.getValue(),
5455
+ isComplete: () => field.isComplete(),
5456
+ focus: () => field.focus(),
5457
+ mode: () => "submit",
5458
+ canProceed: () => field.isComplete(),
5459
+ proceed: () => {
5460
+ }
5461
+ };
5462
+ }
5463
+ function el(tag, className, text) {
5464
+ const node = document.createElement(tag);
5465
+ if (className) node.className = className;
5466
+ if (text != null) node.textContent = text;
5467
+ return node;
5468
+ }
5469
+ function buildApprovalPanel(message, options, onChange) {
5470
+ const cfg = contextConfig();
5471
+ const panel = el("div", "aparte-elic-panel aparte-approval-panel");
5472
+ const askText = () => typeof message === "function" ? message() : message;
5473
+ const ask = askText() ? el("p", "aparte-elic-message", askText()) : null;
5474
+ if (ask) panel.appendChild(ask);
5475
+ const dismiss = el("div", "aparte-elic-dismiss");
5476
+ panel.appendChild(dismiss);
5477
+ let settle = null;
5478
+ let done = false;
5479
+ const fire = (answer) => {
5480
+ if (done) return;
5481
+ done = true;
5482
+ settle?.(answer);
5483
+ };
5484
+ const row = el("div", "aparte-approval-options");
5485
+ row.setAttribute("role", "group");
5486
+ row.setAttribute("aria-label", cfg.t("approvalOptionsLabel"));
5487
+ const labelled = [];
5488
+ const textOf = (option) => typeof option.label === "function" ? option.label() : option.label;
5489
+ for (const option of options) {
5490
+ const button = el("button", `aparte-approval-option aparte-approval-option--${option.tone ?? "affirm"}`);
5491
+ button.type = "button";
5492
+ button.textContent = textOf(option);
5493
+ button.addEventListener("click", () => fire({ option: option.value }));
5494
+ row.appendChild(button);
5495
+ labelled.push({ button, option });
5496
+ }
5497
+ panel.appendChild(row);
5498
+ const instruction = el("textarea", "aparte-approval-instruction");
5499
+ instruction.rows = 1;
5500
+ instruction.placeholder = cfg.t("approvalInstructionPlaceholder");
5501
+ instruction.setAttribute("aria-label", cfg.t("approvalInstructionPlaceholder"));
5502
+ instruction.addEventListener("input", onChange);
5503
+ panel.appendChild(instruction);
5504
+ const typed = () => instruction.value.trim();
5505
+ return {
5506
+ el: panel,
5507
+ dismiss,
5508
+ onSettle: (cb) => {
5509
+ settle = cb;
5510
+ },
5511
+ getContent: () => ({ instruction: typed() }),
5512
+ isComplete: () => typed().length > 0,
5513
+ focus: () => {
5514
+ panel.querySelector(".aparte-approval-option")?.focus();
5515
+ },
5516
+ relabel: () => {
5517
+ const now = contextConfig();
5518
+ const label = now.t("approvalInstructionPlaceholder");
5519
+ instruction.placeholder = label;
5520
+ instruction.setAttribute("aria-label", label);
5521
+ row.setAttribute("aria-label", now.t("approvalOptionsLabel"));
5522
+ if (ask) ask.textContent = askText();
5523
+ for (const { button, option } of labelled) button.textContent = textOf(option);
5524
+ }
5525
+ };
5526
+ }
5527
+ function requestUserInput(request) {
5528
+ return resolveConfig(request.target ?? null).requestUserInput(request);
5529
+ }
5530
+ const DEFAULT_TOOL_HANDLER_TIMEOUT_MS = 5 * 60 * 1e3;
5531
+ function withToolTimeout(work, controller, timeoutMs) {
5532
+ let timer;
5533
+ const timedOut = new Promise((_resolve, reject) => {
5534
+ timer = setTimeout(() => {
5535
+ controller.abort();
5536
+ reject(Object.assign(new Error(`tool handler exceeded ${timeoutMs}ms`), { name: "AbortError" }));
5537
+ }, timeoutMs);
5538
+ });
5539
+ return {
5540
+ raced: Promise.race([work(controller.signal), timedOut]),
5541
+ done: () => clearTimeout(timer)
5542
+ };
5543
+ }
5544
+ class AparteClient {
5138
5545
  _boundHandler = null;
5139
5546
  _boundAbortHandler = null;
5140
5547
  _boundCompactHandler = null;
@@ -5281,61 +5688,66 @@ class AparteClient {
5281
5688
  }
5282
5689
  this._activeToolControllers.clear();
5283
5690
  }
5284
- /**
5285
- * Human-in-the-loop: wait for an `aparte-tool-decision` event matching this
5286
- * tool call (dispatched by the built-in Approve/Reject UI or an app-level
5287
- * approval surface). Resolves `{ approved, payload }` `approved` is `true`
5288
- * only on an explicit approve, and `payload` carries any arbitrary data a
5289
- * custom approval UI attached to the decision (the built-in gate sends
5290
- * none). The `signal` (an AbortController registered in
5291
- * `_activeToolControllers`) lets `abort()` cleanly resolve a pending
5292
- * approval to `{ approved: false }`there is no timeout, since a human may
5293
- * take any amount of time to decide.
5691
+ /*
5692
+ * Two docblocks stood here, both describing `_awaitToolDecision` the method
5693
+ * this replaced. Deleting a method and leaving its documentation is worse than
5694
+ * leaving the method: only the LAST comment before a declaration is its JSDoc,
5695
+ * so those two were floating prose, and one of them explained a DOM-containment
5696
+ * check as a live security property. It was the fix for a cross-chat hazard that
5697
+ * only existed because the decision travelled as a bubbling `document` event.
5698
+ * Routing through `requestUserInput` removed the hazard rather than guarding it,
5699
+ * so the guard is gone tooand a reader who found that comment would have
5700
+ * believed a containment check was still protecting them.
5294
5701
  */
5295
5702
  /**
5296
- * Await the human's Approve/Reject for one tool call.
5703
+ * Ask the human at the COMPOSER, and report what they decided.
5297
5704
  *
5298
- * `target` is not decoration: it is the scope of the consent. The listener sits
5299
- * on `document` and used to accept any `aparte-tool-decision` whose
5300
- * `detail.toolCallId` matched and that id is the tool-call id the MODEL chose.
5301
- * The built-in buttons dispatch with `bubbles: true, composed: true`, so on a
5302
- * page with two chats a click aimed at one tool could satisfy the gate awaiting
5303
- * a different tool in a different conversation. The consented action and the
5304
- * executed action came apart, which is the entire failure mode an approval gate
5305
- * exists to prevent — and the handler behind it is arbitrary consumer code.
5705
+ * The default channel, replacing a `document` listener for `aparte-tool-decision`
5706
+ * that existed only because the buttons lived in a segment renderer with no
5707
+ * reference to this client. Routing through `requestUserInput` puts the decision
5708
+ * where every other request for the user already goes, which is the whole point:
5709
+ * one queue, one panel slot, one teardown, and the same behaviour whether the
5710
+ * request came from a tool handler or from this gate.
5306
5711
  *
5307
- * The check is DOM CONTAINMENT rather than a `targetId` string comparison. A
5308
- * model can choose an id; it cannot choose where in the tree the click happened.
5309
- * It also needs no change on the dispatch side, so a consumer's own Approve
5310
- * button keeps working as long as it fires from inside its own chat.
5712
+ * The tool NAME is the question; the arguments stay in the transcript, on the pill
5713
+ * this request is anchored to. A panel capped at 50vh cannot hold a diff or a plan,
5714
+ * and the transcript is already scrollable, copyable and persisted.
5311
5715
  *
5312
- * The request half of this handshake was hardened with `targetId` for exactly
5313
- * this hazard. This is its sibling, and it was missed.
5716
+ * `aparte-tool-decision` is GONE with the buttons that dispatched it, and so is the
5717
+ * `document` listener that answered it. It existed only because a segment renderer
5718
+ * has no reference to this client; two seams that can disagree about who answers a
5719
+ * decision are one seam too many, and the ratified elicitation rule already said so
5720
+ * — "a typed presenter registered per config instance, never window events". A host
5721
+ * that wants to answer programmatically registers an `approvalResolver` (headless,
5722
+ * no DOM) or its own presenter, both strictly more capable than an id on an event.
5314
5723
  */
5315
- _awaitToolDecision(toolCallId, signal, target) {
5316
- return new Promise((resolve) => {
5317
- if (signal.aborted) {
5318
- resolve({ approved: false });
5319
- return;
5320
- }
5321
- const cleanup = () => {
5322
- document.removeEventListener("aparte-tool-decision", onDecision);
5323
- signal.removeEventListener("abort", onAbort);
5324
- };
5325
- const onDecision = (e) => {
5326
- const detail = e.detail;
5327
- if (detail?.toolCallId !== toolCallId) return;
5328
- if (target && e.target instanceof Node && e.target !== document && !target.contains(e.target)) return;
5329
- cleanup();
5330
- resolve({ approved: detail?.approved === true, payload: detail?.payload });
5331
- };
5332
- const onAbort = () => {
5333
- cleanup();
5334
- resolve({ approved: false });
5335
- };
5336
- document.addEventListener("aparte-tool-decision", onDecision);
5337
- signal.addEventListener("abort", onAbort, { once: true });
5724
+ async _askForApproval(event, signal, target) {
5725
+ const answer = await requestUserInput({
5726
+ kind: "approval",
5727
+ /*
5728
+ * A function, like the option labels below, and for the same reason: the
5729
+ * FRAME is locale text and follows a language switch while the question is
5730
+ * open. The tool's NAME is substituted into it and never translated — it is
5731
+ * wire format, the identifier the model called.
5732
+ */
5733
+ message: () => {
5734
+ const loc = this._config.getLocale();
5735
+ return (loc.approvalAsk ?? "Run {tool}?").replace("{tool}", event.name);
5736
+ },
5737
+ // Functions, not strings, same as the question: these follow a language
5738
+ // switch while the request is open, which a resolved string cannot.
5739
+ options: [
5740
+ { value: "allow", label: () => this._config.getLocale().approveTool ?? "Approve", tone: "affirm" },
5741
+ { value: "deny", label: () => this._config.getLocale().rejectTool ?? "Reject", tone: "deny" }
5742
+ ],
5743
+ signal,
5744
+ target
5338
5745
  });
5746
+ if (answer.action !== "accept") return { approved: false };
5747
+ const content = answer.content;
5748
+ const instruction = content?.instruction?.trim();
5749
+ if (instruction) return { approved: false, instruction };
5750
+ return { approved: content?.option === "allow" };
5339
5751
  }
5340
5752
  /**
5341
5753
  * Resolve the auth for a provider: `options.keyResolver` takes precedence,
@@ -5994,16 +6406,27 @@ ${content}
5994
6406
  this._activeToolControllers.delete(controller);
5995
6407
  }
5996
6408
  }
6409
+ /**
6410
+ * What the loop does after one tool call. THREE outcomes, because a boolean
6411
+ * conflated two questions that a refusal answers differently.
6412
+ *
6413
+ * - `'continue'` — run the next tool call of this turn.
6414
+ * - `'respond'` — skip this turn's remaining calls, then take another turn so the
6415
+ * model reads what happened. A human refusal: the model asked for
6416
+ * several calls and refusing one cannot license the others, but a
6417
+ * refusal it never reads is one the user has to retype as a message.
6418
+ * - `'halt'` — the run is over: an abort, a turn limit, a missing handler.
6419
+ */
5997
6420
  /**
5998
6421
  * Handle one `tool_use` stream event from {@link _streamLoop}: the built-in
5999
6422
  * `create_artifact`, per-tool renderer selection, the human-in-the-loop
6000
6423
  * approval gate, and running the registered handler (timeout / abort).
6001
6424
  * Mutates the shared `messages` / `toolCallsThisTurn` history in place and
6002
- * returns whether the agentic loop should keep going.
6425
+ * returns what the loop should do next.
6003
6426
  */
6004
6427
  async _handleToolUseEvent(event, ctx) {
6005
6428
  const { targetElement, messageId, messages, toolCallsThisTurn, precedingText, artifactProgress, turns, globalMaxTurns } = ctx;
6006
- let continueLoop = true;
6429
+ let flow = "continue";
6007
6430
  toolCallsThisTurn.push({ id: event.id, name: event.name, input: event.input });
6008
6431
  if (event.name === "create_artifact") {
6009
6432
  const input = event.input;
@@ -6029,7 +6452,7 @@ ${content}
6029
6452
  content: "Artifact created successfully.",
6030
6453
  toolCallId: event.id
6031
6454
  });
6032
- return { continueLoop: true };
6455
+ return { flow: "continue" };
6033
6456
  }
6034
6457
  const toolSeg = {
6035
6458
  id: `tool-${event.id}`,
@@ -6063,7 +6486,7 @@ ${content}
6063
6486
  if (turns >= effectiveMaxTurns) {
6064
6487
  console.warn(`[AparteClient] Tool "${event.name}" maxTurns (${effectiveMaxTurns}) reached.`);
6065
6488
  targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
6066
- return { continueLoop: false };
6489
+ return { flow: "halt" };
6067
6490
  }
6068
6491
  const handler = this._config.getToolHandler(event.name);
6069
6492
  if (handler) {
@@ -6078,14 +6501,24 @@ ${content}
6078
6501
  input: event.input
6079
6502
  });
6080
6503
  let decision;
6081
- const resolveApproval = this.options.approvalResolver ?? ((id, sig) => this._awaitToolDecision(id, sig, targetElement));
6504
+ const resolveApproval = this.options.approvalResolver ?? ((call, sig) => this._askForApproval(call, sig, targetElement));
6082
6505
  try {
6083
- decision = await resolveApproval(event.id, approvalController.signal);
6506
+ decision = await resolveApproval(event, approvalController.signal);
6507
+ } catch (err) {
6508
+ if (err?.name === "AbortError") {
6509
+ targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
6510
+ return { flow: "halt" };
6511
+ }
6512
+ throw err;
6084
6513
  } finally {
6085
6514
  this._activeToolControllers.delete(approvalController);
6086
6515
  }
6516
+ if (approvalController.signal.aborted) {
6517
+ targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
6518
+ return { flow: "halt" };
6519
+ }
6087
6520
  if (!decision.approved) {
6088
- const rejection = "Tool execution was rejected by the user.";
6521
+ const rejection = decision.instruction ? `The user rejected this tool call and said: ${decision.instruction}` : "Tool execution was rejected by the user.";
6089
6522
  targetElement.updateSegment?.(toolSeg.id, { status: "rejected", result: rejection });
6090
6523
  const existingToolCallMsg = messages.find(
6091
6524
  (m) => m.role === "tool_call" && m.toolCalls?.some((tc) => tc.id === event.id)
@@ -6099,7 +6532,7 @@ ${content}
6099
6532
  });
6100
6533
  }
6101
6534
  messages.push({ role: "tool_result", content: rejection, toolCallId: event.id });
6102
- return { continueLoop: false };
6535
+ return { flow: "respond" };
6103
6536
  }
6104
6537
  if (decision.payload && typeof decision.payload === "object" && !Array.isArray(decision.payload)) {
6105
6538
  effectiveInput = { ...event.input, ...decision.payload };
@@ -6139,7 +6572,7 @@ ${content}
6139
6572
  } catch (err) {
6140
6573
  if (err?.name === "AbortError") {
6141
6574
  targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
6142
- continueLoop = false;
6575
+ flow = "halt";
6143
6576
  } else {
6144
6577
  throw err;
6145
6578
  }
@@ -6150,9 +6583,9 @@ ${content}
6150
6583
  } else {
6151
6584
  console.warn(`[AparteClient] No handler registered for tool "${event.name}"`);
6152
6585
  targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
6153
- continueLoop = false;
6586
+ flow = "halt";
6154
6587
  }
6155
- return { continueLoop };
6588
+ return { flow };
6156
6589
  }
6157
6590
  /**
6158
6591
  * Stream loop: runs one provider.chat() call and repeats if a tool was called.
@@ -6177,6 +6610,7 @@ ${content}
6177
6610
  targetElement.addSegment?.(seg);
6178
6611
  }
6179
6612
  while (continueLoop) {
6613
+ let endTurnEarly = false;
6180
6614
  if (this._isAborted) {
6181
6615
  dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
6182
6616
  break;
@@ -6382,7 +6816,8 @@ ${content}
6382
6816
  { id: event.id, name: event.name, input: event.input },
6383
6817
  { targetElement, messageId, messages, toolCallsThisTurn, precedingText, artifactProgress, turns, globalMaxTurns }
6384
6818
  );
6385
- if (!toolResult.continueLoop) continueLoop = false;
6819
+ if (toolResult.flow !== "continue") endTurnEarly = true;
6820
+ if (toolResult.flow === "halt") continueLoop = false;
6386
6821
  break;
6387
6822
  }
6388
6823
  case "error":
@@ -6397,7 +6832,7 @@ ${content}
6397
6832
  break;
6398
6833
  }
6399
6834
  }
6400
- if (!continueLoop) break;
6835
+ if (endTurnEarly) break;
6401
6836
  }
6402
6837
  finalizeXmlArtifact(xmlCtx, { targetElement, messageId, artifactProgress, artifactXmlHint, textParser, streamingSegmentIds });
6403
6838
  const finals = textParser.finalize();
@@ -6512,7 +6947,7 @@ ${content}
6512
6947
  const tool = this._config.getTools().find((t) => t.name === name);
6513
6948
  return tool ? { maxTurns: tool.maxTurns, needsApproval: tool.needsApproval } : void 0;
6514
6949
  };
6515
- const approvalResolver = this.options.approvalResolver ?? ((id, sig) => this._awaitToolDecision(id, sig, targetElement));
6950
+ const approvalResolver = this.options.approvalResolver ?? ((call, sig) => this._askForApproval(call, sig, targetElement));
6516
6951
  const usage = await streamRunner({
6517
6952
  messageId,
6518
6953
  baseRequest,
@@ -6818,13 +7253,34 @@ class AparteMessageRepository {
6818
7253
  }
6819
7254
  }
6820
7255
  const APARTE_DEFAULT_UI_EVENTS = [
7256
+ // <aparte-composer> and its parts
6821
7257
  "aparte-send",
7258
+ "aparte-cancel",
7259
+ "aparte-composer-change",
7260
+ "aparte-composer-submit",
7261
+ "aparte-action-click",
7262
+ "aparte-attachment-preview",
7263
+ // <aparte-chat-bubble>
6822
7264
  "aparte-action",
6823
7265
  "aparte-retry",
6824
7266
  "aparte-edit",
7267
+ "aparte-feedback",
7268
+ "aparte-message-info",
6825
7269
  "aparte-branch-navigate",
6826
- "aparte-composer-change",
6827
- "aparte-path-changed"
7270
+ // <aparte-chat-viewport>
7271
+ "aparte-segment-update",
7272
+ "aparte-reset-done",
7273
+ "aparte-path-changed",
7274
+ // <aparte-conversation-list>
7275
+ "aparte-select-conversation",
7276
+ "aparte-delete-conversation",
7277
+ "aparte-archive-conversation",
7278
+ "aparte-unarchive-conversation",
7279
+ // the select primitives
7280
+ "aparte-select-change",
7281
+ "aparte-select-open",
7282
+ "aparte-select-close",
7283
+ "aparte-optgroup-toggle"
6828
7284
  ];
6829
7285
  function applyElementProps(el2, props, transformValue = (value) => value) {
6830
7286
  for (const [key, value] of Object.entries(props)) {
@@ -6847,344 +7303,75 @@ function isAwaitingReply(message) {
6847
7303
  if (message.status === "streaming" || message.status === "pending") return true;
6848
7304
  return message.status === void 0 && message.role === "assistant" && !message.content?.trim() && !message.segments?.length;
6849
7305
  }
6850
- function el(tag, className, text) {
6851
- const node = document.createElement(tag);
6852
- if (className) node.className = className;
6853
- if (text != null) node.textContent = text;
6854
- return node;
6855
- }
6856
- function fieldHeader(parent, field, skipTitle) {
6857
- let titleId;
6858
- if (field.title && field.title.trim() !== skipTitle?.trim()) {
6859
- const title = el("p", "aparte-elic-title", field.title);
6860
- titleId = `elic-title-${uuid()}`;
6861
- title.id = titleId;
6862
- parent.appendChild(title);
6863
- }
6864
- if (field.description) parent.appendChild(el("p", "aparte-elic-desc", field.description));
6865
- return titleId;
6866
- }
6867
- function labelGroup(list, opts) {
6868
- list.setAttribute("role", opts.multiple ? "group" : "radiogroup");
6869
- if (opts.titleId) list.setAttribute("aria-labelledby", opts.titleId);
6870
- else if (opts.fallbackLabel) list.setAttribute("aria-label", opts.fallbackLabel);
6871
- }
6872
- function buildEnumField(field, onChange, fallbackLabel) {
6873
- const wrap = el("div", "aparte-elic-field aparte-elic-enum");
6874
- const titleId = fieldHeader(wrap, field, fallbackLabel);
6875
- const list = el("div", "aparte-elic-options");
6876
- labelGroup(list, { multiple: field.multiple, titleId, fallbackLabel });
6877
- const name = `elic-${uuid()}`;
6878
- const type = field.multiple ? "checkbox" : "radio";
6879
- const allowOther = field.allowOther ?? contextConfig().getElicitationOptions().allowOther;
6880
- const defaults = new Set(Array.isArray(field.default) ? field.default : field.default != null ? [field.default] : []);
6881
- const buildOption = (value, label, description, recommended) => {
6882
- const row = el("label", "aparte-elic-option" + (recommended ? " aparte-elic-option--recommended" : ""));
6883
- const control = el("input", "aparte-elic-control");
6884
- control.type = type;
6885
- control.name = name;
6886
- control.value = value;
6887
- if (defaults.has(value)) control.checked = true;
6888
- const body = el("span", "aparte-elic-option-body");
6889
- body.appendChild(el("span", "aparte-elic-option-title", label));
6890
- if (description) body.appendChild(el("span", "aparte-elic-option-desc", description));
6891
- row.append(control, body);
6892
- return row;
6893
- };
6894
- for (const opt of field.options) {
6895
- list.appendChild(buildOption(opt.value, opt.label ?? opt.value, opt.description, opt.recommended));
6896
- }
6897
- let otherText = null;
6898
- if (allowOther) {
6899
- const row = el("label", "aparte-elic-option aparte-elic-option--other");
6900
- const control = el("input", "aparte-elic-control");
6901
- control.type = type;
6902
- control.name = name;
6903
- control.value = "__other__";
6904
- const body = el("span", "aparte-elic-option-body");
6905
- const t = contextConfig();
6906
- body.appendChild(el("span", "aparte-elic-option-title", t.t("elicitationOther")));
6907
- otherText = el("input", "aparte-elic-other-input");
6908
- otherText.type = "text";
6909
- otherText.placeholder = t.t("elicitationOtherPlaceholder");
6910
- otherText.style.display = "none";
6911
- otherText.setAttribute("aria-label", t.t("elicitationOtherLabel"));
6912
- body.appendChild(otherText);
6913
- row.append(control, body);
6914
- list.appendChild(row);
6915
- }
6916
- wrap.appendChild(list);
6917
- const controls = () => Array.from(list.querySelectorAll(".aparte-elic-control"));
6918
- const other = () => list.querySelector('input[value="__other__"]');
6919
- list.addEventListener("change", () => {
6920
- const o = other();
6921
- if (o && otherText) {
6922
- otherText.style.display = o.checked ? "" : "none";
6923
- if (o.checked) otherText.focus();
6924
- }
6925
- onChange();
6926
- });
6927
- otherText?.addEventListener("input", onChange);
6928
- const getValue = () => {
6929
- const otherVal = other()?.checked && otherText?.value.trim() ? otherText.value.trim() : "";
6930
- if (field.multiple) {
6931
- const vals = controls().filter((c) => c.checked && c.value !== "__other__").map((c) => c.value);
6932
- if (otherVal) vals.push(otherVal);
6933
- return vals;
6934
- }
6935
- const checked = controls().find((c) => c.checked);
6936
- if (!checked) return "";
6937
- return checked.value === "__other__" ? otherVal : checked.value;
6938
- };
6939
- return {
6940
- el: wrap,
6941
- getValue,
6942
- isComplete: () => {
6943
- const v = getValue();
6944
- return field.multiple ? v.length > 0 : v !== "";
6945
- },
6946
- focus: () => controls()[0]?.focus()
6947
- };
6948
- }
6949
- function buildBooleanField(field, onChange, fallbackLabel) {
6950
- const wrap = el("div", "aparte-elic-field aparte-elic-boolean");
6951
- const titleId = fieldHeader(wrap, field, fallbackLabel);
6952
- const list = el("div", "aparte-elic-options");
6953
- labelGroup(list, { titleId, fallbackLabel });
6954
- const name = `elic-${uuid()}`;
6955
- const mk = (val, label) => {
6956
- const row = el("label", "aparte-elic-option");
6957
- const control = el("input", "aparte-elic-control");
6958
- control.type = "radio";
6959
- control.name = name;
6960
- control.value = val;
6961
- if (field.default != null && String(field.default) === val) control.checked = true;
6962
- const body = el("span", "aparte-elic-option-body");
6963
- body.appendChild(el("span", "aparte-elic-option-title", label));
6964
- row.append(control, body);
6965
- list.appendChild(row);
6966
- };
6967
- const t = contextConfig();
6968
- mk("true", field.trueLabel ?? t.t("elicitationYes"));
6969
- mk("false", field.falseLabel ?? t.t("elicitationNo"));
6970
- wrap.appendChild(list);
6971
- list.addEventListener("change", onChange);
6972
- const checked = () => list.querySelector("input:checked");
6973
- return {
6974
- el: wrap,
6975
- getValue: () => checked()?.value === "true",
6976
- isComplete: () => checked() != null,
6977
- focus: () => list.querySelector("input")?.focus()
6978
- };
6979
- }
6980
- function buildStringField(field, onChange, fallbackLabel) {
6981
- const wrap = el("div", "aparte-elic-field aparte-elic-string");
6982
- fieldHeader(wrap, field, fallbackLabel);
6983
- const input = field.multiline ? el("textarea", "aparte-elic-text") : el("input", "aparte-elic-text");
6984
- if (!field.multiline) input.type = "text";
6985
- if (field.placeholder) input.setAttribute("placeholder", field.placeholder);
6986
- if (field.default) input.value = field.default;
6987
- if (field.maxLength != null) input.setAttribute("maxlength", String(field.maxLength));
6988
- input.setAttribute("aria-label", field.title ?? field.description ?? fallbackLabel ?? contextConfig().t("elicitationAnswerLabel"));
6989
- wrap.appendChild(input);
6990
- input.addEventListener("input", onChange);
6991
- const required = field.required ?? true;
6992
- const value = () => input.value;
6993
- return {
6994
- el: wrap,
6995
- getValue: () => value(),
6996
- isComplete: () => {
6997
- const v = value().trim();
6998
- if (!required && v === "") return true;
6999
- return v.length >= (field.minLength ?? 1);
7000
- },
7001
- focus: () => input.focus()
7002
- };
7003
- }
7004
- function buildField(field, onChange, fallbackLabel, key) {
7005
- const custom = contextConfig().getElicitationFieldRenderer()?.(field, { key, notifyChange: onChange });
7006
- if (custom) {
7007
- return {
7008
- el: custom.el,
7009
- getValue: () => custom.getValue(),
7010
- isComplete: () => custom.isComplete(),
7011
- focus: () => custom.focus?.()
7012
- };
7013
- }
7014
- switch (field.type) {
7015
- case "enum":
7016
- return buildEnumField(field, onChange, fallbackLabel);
7017
- case "boolean":
7018
- return buildBooleanField(field, onChange, fallbackLabel);
7019
- case "string":
7020
- return buildStringField(field, onChange, fallbackLabel);
7021
- }
7022
- }
7023
- function buildElicitationPanel(message, schema, onChange) {
7024
- const panel = el("div", "aparte-elic-panel");
7025
- if (message) panel.appendChild(el("p", "aparte-elic-message", message));
7026
- const body = el("div", "aparte-elic-body");
7027
- panel.appendChild(body);
7028
- const dismiss = el("div", "aparte-elic-dismiss");
7029
- panel.appendChild(dismiss);
7030
- if (schema.type === "object") {
7031
- let show = function(index) {
7032
- current = index;
7033
- fields.forEach((f, i) => {
7034
- f.field.el.hidden = i !== index;
7035
- });
7036
- syncNav();
7037
- };
7038
- const entries = Object.entries(schema.properties);
7039
- const requiredKeys = new Set(schema.required ?? entries.map(([k]) => k));
7040
- const cfg = contextConfig();
7041
- let syncNav = () => {
7042
- };
7043
- const notify = () => {
7044
- syncNav();
7045
- onChange();
7046
- };
7047
- const fields = entries.map(([key, field2]) => {
7048
- const built = buildField(field2, notify, void 0, key);
7049
- if (!field2.title && !built.el.querySelector(".aparte-elic-title")) {
7050
- built.el.insertBefore(el("p", "aparte-elic-title", key), built.el.firstChild);
7051
- }
7052
- body.appendChild(built.el);
7053
- return { key, field: built, required: requiredKeys.has(key), header: field2.header };
7054
- });
7055
- const isComplete = () => fields.every((f) => !f.required || f.field.isComplete());
7056
- const api = {
7057
- el: panel,
7058
- dismiss,
7059
- getContent: () => Object.fromEntries(fields.map((f) => [f.key, f.field.getValue()])),
7060
- isComplete,
7061
- focus: () => fields[0]?.field.focus(),
7062
- // A form of one behaves like a single field; only a real form steps.
7063
- mode: () => "submit",
7064
- canProceed: isComplete,
7065
- proceed: () => {
7066
- }
7067
- };
7068
- if (fields.length < 2 || cfg.getElicitationOptions().layout !== "stepped") return api;
7069
- panel.classList.add("aparte-elic-panel--stepped");
7070
- let current = 0;
7071
- const steps = el("div", "aparte-elic-steps");
7072
- steps.setAttribute("role", "tablist");
7073
- const chips = fields.map((f, i) => {
7074
- const chip = el("button", "aparte-elic-step");
7075
- chip.type = "button";
7076
- chip.setAttribute("role", "tab");
7077
- chip.textContent = f.header?.trim() || String(i + 1);
7078
- chip.addEventListener("click", () => {
7079
- show(i);
7080
- f.field.focus();
7081
- });
7082
- steps.appendChild(chip);
7083
- return chip;
7084
- });
7085
- panel.insertBefore(steps, body);
7086
- syncNav = () => {
7087
- chips.forEach((chip, i) => {
7088
- chip.setAttribute("aria-selected", String(i === current));
7089
- chip.toggleAttribute("data-answered", fields[i].field.isComplete());
7090
- });
7091
- };
7092
- show(0);
7093
- const last = () => current === fields.length - 1;
7094
- return {
7095
- ...api,
7096
- focus: () => fields[current]?.field.focus(),
7097
- mode: () => last() ? "submit" : "advance",
7098
- // Advancing needs only THIS question answered; submitting needs them all,
7099
- // which is what stops the last question from accepting a form with a hole
7100
- // in it.
7101
- canProceed: () => last() ? isComplete() : !!fields[current]?.field.isComplete(),
7102
- proceed: () => {
7103
- if (last()) return;
7104
- show(current + 1);
7105
- fields[current]?.field.focus();
7106
- }
7107
- };
7108
- }
7109
- const field = buildField(schema, onChange, message);
7110
- body.appendChild(field.el);
7111
- return {
7112
- el: panel,
7113
- dismiss,
7114
- getContent: () => field.getValue(),
7115
- isComplete: () => field.isComplete(),
7116
- focus: () => field.focus(),
7117
- mode: () => "submit",
7118
- canProceed: () => field.isComplete(),
7119
- proceed: () => {
7120
- }
7121
- };
7122
- }
7123
- function requestUserInput(request) {
7124
- return resolveConfig(request.target ?? null).requestUserInput(request);
7125
- }
7126
7306
  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
7307
+ resolveConfig as $,
7308
+ APARTE_CONFIG_CHANGE as A,
7309
+ contentToText as B,
7310
+ contextConfig as C,
7311
+ createAparteChatHandler as D,
7312
+ createStreamAdapter as E,
7313
+ cssEscape as F,
7314
+ declineDefaultRenderers as G,
7315
+ defaultSanitizer as H,
7316
+ deriveArtifactKind as I,
7317
+ detachConfig as J,
7318
+ escapeAttr as K,
7319
+ escapeHtml as L,
7320
+ filesToAttachments as M,
7321
+ getAllRenderers as N,
7322
+ getSegmentRenderer as O,
7323
+ installDefaultRenderersOnce as P,
7324
+ isAwaitingReply as Q,
7325
+ isFormatAdapter as R,
7326
+ isSafeUrl as S,
7327
+ isSegmentSettled as T,
7328
+ parseAparteEventStream as U,
7329
+ parseMarkdownToSegments as V,
7330
+ populateBubbleFromMessage as W,
7331
+ readableToAsyncIterable as X,
7332
+ registerDefaultRenderers as Y,
7333
+ registerSegmentRenderer as Z,
7334
+ requestUserInput as _,
7335
+ APARTE_CONVERSATION_SCHEMA_VERSION as a,
7336
+ revokeAttachmentUrls as a0,
7337
+ runWithConfig as a1,
7338
+ segmentDuration as a2,
7339
+ segmentTiming as a3,
7340
+ subscribeConfigChange as a4,
7341
+ unregisterSegmentRenderer as a5,
7342
+ uuid as a6,
7343
+ mergeSegmentUpdate as a7,
7344
+ stampSegmentActivity as a8,
7345
+ stampSegmentOnInsert as a9,
7346
+ stampSegmentOnUpdate as aa,
7347
+ renumberSegments as ab,
7348
+ openSegmentIds as ac,
7349
+ isTerminalStatus as ad,
7350
+ adoptMessageSegments as ae,
7351
+ APARTE_DEFAULT_BUBBLE_ACTIONS as b,
7352
+ APARTE_DEFAULT_HOST_HANDLERS as c,
7353
+ APARTE_DEFAULT_ICON_FALLBACKS as d,
7354
+ APARTE_DEFAULT_LOCALE as e,
7355
+ APARTE_DEFAULT_SKELETON_FALLBACKS as f,
7356
+ APARTE_DEFAULT_UI_EVENTS as g,
7357
+ APARTE_HOST_ATTR as h,
7358
+ AparteBackendTransport as i,
7359
+ AparteChatHost as j,
7360
+ AparteClient as k,
7361
+ AparteConfig as l,
7362
+ AparteConversationController as m,
7363
+ AparteConversationManager as n,
7364
+ AparteDirectTransport as o,
7365
+ AparteElicitationAbortError as p,
7366
+ AparteError as q,
7367
+ AparteErrorCode as r,
7368
+ AparteMessageRepository as s,
7369
+ AparteStreamParser as t,
7370
+ aparteGlobalConfig as u,
7371
+ applyElementProps as v,
7372
+ attachConfig as w,
7373
+ buildApprovalPanel as x,
7374
+ buildElicitationPanel as y,
7375
+ collectRendererStyles as z
7189
7376
  };
7190
- //# sourceMappingURL=index-hQQljwfX.js.map
7377
+ //# sourceMappingURL=is-awaiting-reply-Cb7H3YfN.js.map