@aparte/core 0.10.0 → 0.12.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 (85) 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 +25 -4
  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 +15 -6
  10. package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
  11. package/dist/components/composer/aparte-composer-add-attachment.d.ts +5 -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 +2 -2
  16. package/dist/components/composer/aparte-composer-input.d.ts +8 -5
  17. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  18. package/dist/components/composer/aparte-composer-send.d.ts +2 -2
  19. package/dist/components/composer/aparte-composer-toolbar.d.ts +3 -3
  20. package/dist/components/composer/aparte-composer.d.ts +81 -10
  21. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  22. package/dist/components/conversation-list/aparte-conversation-list.d.ts +19 -12
  23. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  24. package/dist/components/elicitation/aparte-elicitation.d.ts +14 -2
  25. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  26. package/dist/components/status/aparte-chat-status.d.ts +9 -0
  27. package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
  28. package/dist/components/viewport/aparte-chat-viewport.d.ts +15 -4
  29. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  30. package/dist/config/aparte-config.d.ts +72 -15
  31. package/dist/config/aparte-config.d.ts.map +1 -1
  32. package/dist/config/locale.d.ts +43 -0
  33. package/dist/config/locale.d.ts.map +1 -1
  34. package/dist/custom-elements.json +2824 -1801
  35. package/dist/elicitation/approval-panel.d.ts +42 -0
  36. package/dist/elicitation/approval-panel.d.ts.map +1 -0
  37. package/dist/elicitation/index.d.ts +4 -1
  38. package/dist/elicitation/index.d.ts.map +1 -1
  39. package/dist/elicitation/panel.d.ts +1 -1
  40. package/dist/elicitation/panel.d.ts.map +1 -1
  41. package/dist/elicitation/types.d.ts +116 -9
  42. package/dist/elicitation/types.d.ts.map +1 -1
  43. package/dist/generated/element-attributes.d.ts +177 -0
  44. package/dist/generated/element-attributes.d.ts.map +1 -0
  45. package/dist/index.css +621 -92
  46. package/dist/index.d.ts +4 -3
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +368 -255
  49. package/dist/index.js.map +1 -1
  50. package/dist/index.node.d.ts +4 -3
  51. package/dist/index.node.d.ts.map +1 -1
  52. package/dist/index.node.js +46 -44
  53. package/dist/index.node.js.map +1 -1
  54. package/dist/interop/element-props.d.ts +40 -2
  55. package/dist/interop/element-props.d.ts.map +1 -1
  56. package/dist/{index-BnKBciDG.js → is-awaiting-reply-CxWRISBC.js} +949 -957
  57. package/dist/is-awaiting-reply-CxWRISBC.js.map +1 -0
  58. package/dist/primitives/select/aparte-optgroup.d.ts +3 -0
  59. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  60. package/dist/primitives/select/aparte-option.d.ts +1 -0
  61. package/dist/primitives/select/aparte-option.d.ts.map +1 -1
  62. package/dist/primitives/select/aparte-select.d.ts +22 -18
  63. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  64. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  65. package/dist/renderers/segments/artifact/card.d.ts.map +1 -1
  66. package/dist/renderers/segments/pipeline-waiting.d.ts.map +1 -1
  67. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  68. package/dist/types/element-attributes.d.ts +42 -0
  69. package/dist/types/element-attributes.d.ts.map +1 -0
  70. package/dist/types/event-map.d.ts +1 -2
  71. package/dist/types/event-map.d.ts.map +1 -1
  72. package/dist/types/index.d.ts +3 -2
  73. package/dist/types/index.d.ts.map +1 -1
  74. package/dist/types/segments.d.ts +1 -8
  75. package/dist/types/segments.d.ts.map +1 -1
  76. package/dist/types/tools.d.ts +26 -23
  77. package/dist/types/tools.d.ts.map +1 -1
  78. package/dist/utils/chat-boundary.d.ts +22 -0
  79. package/dist/utils/chat-boundary.d.ts.map +1 -0
  80. package/dist/utils/segments.d.ts +8 -0
  81. package/dist/utils/segments.d.ts.map +1 -1
  82. package/package.json +3 -2
  83. package/dist/index-BnKBciDG.js.map +0 -1
  84. package/dist/renderers/segments/progress.d.ts +0 -3
  85. package/dist/renderers/segments/progress.d.ts.map +0 -1
@@ -75,6 +75,16 @@ const APARTE_DEFAULT_LOCALE = {
75
75
  elicitationYes: "Yes",
76
76
  elicitationNo: "No",
77
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",
78
88
  actionUpload: "Attach file",
79
89
  download: "Download",
80
90
  previewPending: "Press Preview to run this artifact.",
@@ -1024,6 +1034,23 @@ const defaultSanitizer = (html) => {
1024
1034
  sanitizeChildren(doc.body, container, doc);
1025
1035
  return container.innerHTML;
1026
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
+ }
1027
1054
  function escapeHtml(value) {
1028
1055
  let out = "";
1029
1056
  for (let i = 0; i < value.length; i++) {
@@ -1038,6 +1065,19 @@ function escapeHtml(value) {
1038
1065
  return out;
1039
1066
  }
1040
1067
  const escapeAttr = escapeHtml;
1068
+ function isChatBoundary(el2) {
1069
+ if (!el2) return false;
1070
+ const tag = el2.tagName?.toLowerCase();
1071
+ return tag === "aparte-chat" || tag === "aparte-chat-component" || el2.hasAttribute?.("data-aparte-chat") === true;
1072
+ }
1073
+ function chatBoundaryOf(el2) {
1074
+ let node = el2 ?? null;
1075
+ while (node) {
1076
+ if (isChatBoundary(node)) return node;
1077
+ node = node.parentElement;
1078
+ }
1079
+ return null;
1080
+ }
1041
1081
  const APARTE_DEFAULT_BUBBLE_ACTIONS = {
1042
1082
  copy: true,
1043
1083
  retry: false,
@@ -1050,6 +1090,8 @@ const APARTE_DEFAULT_HOST_HANDLERS = {
1050
1090
  artifactRedownload: false,
1051
1091
  artifactRehydrate: false
1052
1092
  };
1093
+ const NOOP = () => {
1094
+ };
1053
1095
  class AparteConfig {
1054
1096
  _markdownProvider;
1055
1097
  _streamingMarkdownProvider;
@@ -1101,11 +1143,46 @@ class AparteConfig {
1101
1143
  _modelPreferenceProvider;
1102
1144
  // Conversation persistence (optional, agnostic)
1103
1145
  _conversationManager;
1104
- // Human-in-the-loop: presents typed input requests (ask_user,
1105
- // tool approval, forms). Set by the <aparte-elicitation> Web Component.
1106
- _elicitationPresenter;
1146
+ /*
1147
+ * Human-in-the-loop: presents typed input requests (ask_user, tool approval,
1148
+ * forms). Registered by the `<aparte-elicitation>` Web Component.
1149
+ *
1150
+ * A STACK, not one slot, and that is the fix for two real failures. Since
1151
+ * `<aparte-elicitation>` entered `<aparte-chat>`'s default composition, two plain
1152
+ * `<aparte-chat>` elements on a page each register one on the same global config:
1153
+ *
1154
+ * - the second clobbered the first, so chat A's approval opened under chat B, and
1155
+ * answering it under B decided A's tool call;
1156
+ * - and when B unmounted it cleared the slot, leaving chat A mounted with a
1157
+ * perfectly good presenter that never re-registered — every later approval and
1158
+ * every `ask_user` in A rejected `no-presenter` for the life of the page,
1159
+ * silently, because the warning fires at most once per config.
1160
+ *
1161
+ * Each entry carries the element that registered it, so a request naming a `target`
1162
+ * is routed to the presenter in the SAME chat. `AparteElicitationRequest.target` was
1163
+ * already documented as "used to resolve WHICH instance presents"; the single slot
1164
+ * is what made that impossible.
1165
+ */
1166
+ _elicitationPresenters = [];
1107
1167
  /** Warn once per config, not once per request — a tool loop can ask repeatedly. */
1108
1168
  _warnedNoPresenter = false;
1169
+ /**
1170
+ * The tail of the queue of requests waiting for the human, or `null` when none is.
1171
+ *
1172
+ * One request reaches the presenter at a time — not because a presenter could not
1173
+ * cope, but because the COMPOSER cannot: it has one panel slot, and a second
1174
+ * request used to clobber the first's DOM. The previous protection lived in the
1175
+ * default presenter, which answered the second request `cancel` on the spot; that
1176
+ * is a refusal invented for a question nobody was ever shown, the same class of
1177
+ * lie as telling the model a stopped turn was refused. WAITING is the honest
1178
+ * behaviour.
1179
+ *
1180
+ * Here rather than in the presenter because this is the only place that sees
1181
+ * EVERY request whatever raised it — a tool handler, an app's own button, and
1182
+ * (soon) the tool-approval gate. A custom presenter registered by a consumer gets
1183
+ * the same protection for free, which it previously had none of.
1184
+ */
1185
+ _elicitationQueue = null;
1109
1186
  // Tool Registry
1110
1187
  _tools = /* @__PURE__ */ new Map();
1111
1188
  _toolRenderers = /* @__PURE__ */ new Map();
@@ -1175,7 +1252,7 @@ class AparteConfig {
1175
1252
  * trigger only for the ones you claim — see {@link AparteHostHandlersConfig}.
1176
1253
  *
1177
1254
  * @example
1178
- * aparteGlobalConfig.setHostHandlers({ attachmentPreview: true, terminalRun: true });
1255
+ * aparteGlobalConfig.setHostHandlers({ attachmentPreview: true, artifactRedownload: true });
1179
1256
  */
1180
1257
  setHostHandlers(config) {
1181
1258
  this._hostHandlers = { ...this._hostHandlers, ...config };
@@ -1811,7 +1888,7 @@ class AparteConfig {
1811
1888
  *
1812
1889
  * @example
1813
1890
  * // Custom pill for a web-search tool
1814
- * aparteGlobalConfig.registerToolRenderer('web_search', { render: (seg) => `<div class="tool-pill">Searching...</div>` });
1891
+ * aparteGlobalConfig.registerToolRenderer('web_search', { render: (seg) => `<div class="aparte-tool-label">Searching...</div>` });
1815
1892
  */
1816
1893
  registerToolRenderer(toolName, renderer) {
1817
1894
  this._toolRenderers.set(toolName, renderer);
@@ -1885,39 +1962,112 @@ class AparteConfig {
1885
1962
  * The `<aparte-elicitation>` Web Component registers itself here; an app can
1886
1963
  * override with its own framework-native presenter. Pass `null` to clear.
1887
1964
  */
1888
- setElicitationPresenter(presenter) {
1889
- this._elicitationPresenter = presenter ?? void 0;
1965
+ setElicitationPresenter(presenter, owner) {
1966
+ if (presenter === null) {
1967
+ this._elicitationPresenters = [];
1968
+ return;
1969
+ }
1970
+ this._elicitationPresenters = this._elicitationPresenters.filter((e) => e.fn !== presenter);
1971
+ this._elicitationPresenters.push(owner === void 0 ? { fn: presenter } : { fn: presenter, owner });
1972
+ }
1973
+ /**
1974
+ * Withdraw ONE presenter, named, leaving any others registered.
1975
+ *
1976
+ * This is what an unmounting `<aparte-elicitation>` needs and what
1977
+ * `setElicitationPresenter(null)` could not express: clearing the slot took every
1978
+ * other mounted chat's presenter down with it. Removing a presenter that is not
1979
+ * registered is a no-op.
1980
+ */
1981
+ removeElicitationPresenter(presenter) {
1982
+ this._elicitationPresenters = this._elicitationPresenters.filter((e) => e.fn !== presenter);
1890
1983
  }
1891
- /** The registered elicitation presenter, or undefined if none. */
1984
+ /**
1985
+ * The presenter a request with no `target` would reach — the most recently
1986
+ * registered one. Kept for the callers that ask "is anybody able to present?".
1987
+ */
1892
1988
  getElicitationPresenter() {
1893
- return this._elicitationPresenter;
1989
+ return this._elicitationPresenters[this._elicitationPresenters.length - 1]?.fn;
1990
+ }
1991
+ /**
1992
+ * The presenter that should handle THIS request.
1993
+ *
1994
+ * A `target` is an element inside the asking chat, so the presenter registered from
1995
+ * the same chat is the right one — found by comparing chat boundaries rather than by
1996
+ * `contains`, because the presenter is a sibling of the transcript, not an ancestor
1997
+ * of it. With no target, no match, or no boundary to compare, the top of the stack
1998
+ * is the answer, which is exactly the old single-slot behaviour.
1999
+ */
2000
+ _presenterFor(target) {
2001
+ const stack = this._elicitationPresenters;
2002
+ if (stack.length === 0) return void 0;
2003
+ if (target) {
2004
+ const wanted = chatBoundaryOf(target);
2005
+ if (wanted) {
2006
+ for (let i = stack.length - 1; i >= 0; i -= 1) {
2007
+ const entry = stack[i];
2008
+ if (entry.owner && chatBoundaryOf(entry.owner) === wanted) return entry.fn;
2009
+ }
2010
+ }
2011
+ }
2012
+ return stack[stack.length - 1]?.fn;
1894
2013
  }
1895
2014
  /**
1896
2015
  * Ask the user for typed input mid-run and await their response. This is the
1897
2016
  * generic primitive behind `ask_user` and tool approval — the KIND of
1898
- * question is the schema, not a bespoke tool. Resolves `accept` with the
1899
- * value, `decline` when the user declines, or `cancel` when the turn is
1900
- * cancelled.
2017
+ * question is the schema, not a bespoke tool.
2018
+ *
2019
+ * **Resolves** `accept` with the value, or `decline` when the user declines.
1901
2020
  *
1902
- * With NO presenter registered it resolves `cancel` rather than hanging and
1903
- * warns, once, because that `cancel` is otherwise a lie told quietly. The
1904
- * model reads it as "the user refused"; the user was never asked, and the
1905
- * developer sees nothing at all. The default presenter is
1906
- * `<aparte-elicitation>`, which registers itself from its `connectedCallback`
1907
- * — so it has to be IN THE DOM. A docs page of ours claimed it "installs
1908
- * itself nothing to register", which is how this failure mode was found.
2021
+ * **Rejects** it does not resolve when the request ends without an answer, with
2022
+ * an {@link AparteElicitationAbortError} whose `name` is `'AbortError'` and whose
2023
+ * `reason` is `'aborted'` (the turn was stopped, the signal fired, or another request
2024
+ * took the question away) or `'no-presenter'` (nothing was mounted to ask it). So
2025
+ * `await` it inside a `try`, or attach a `.catch()`.
2026
+ *
2027
+ * That is a change, and this block described the old behaviour for a whole release:
2028
+ * "resolves `cancel` when the turn is cancelled", and "with NO presenter registered it
2029
+ * resolves `cancel` rather than hanging". Both were false the moment `cancel` was
2030
+ * removed — and the second is the one that bites, because a developer who reads
2031
+ * "rather than hanging" writes no `.catch()` and gets an unhandled rejection. It ships
2032
+ * in the `.d.ts` and in the custom-elements manifest, which is the API surface the
2033
+ * docs site renders, so it was the most-read wrong sentence in the library.
2034
+ *
2035
+ * A value was removed rather than renamed on purpose: `cancel` was easy to handle as
2036
+ * though it were an answer, and the approval gate did exactly that — it read a stopped
2037
+ * turn as the user refusing a tool they were never shown.
2038
+ *
2039
+ * With no presenter it rejects `'no-presenter'` and warns once, because a request
2040
+ * nobody could see is a setup mistake only the developer can fix. The default
2041
+ * presenter is `<aparte-elicitation>`, which registers itself from its
2042
+ * `connectedCallback` — so it has to be IN THE DOM. A docs page of ours claimed it
2043
+ * "installs itself — nothing to register", which is how that was found.
1909
2044
  */
1910
2045
  requestUserInput(request) {
1911
- if (!this._elicitationPresenter) {
2046
+ const previous = this._elicitationQueue;
2047
+ const settled = previous === null ? this._present(request) : previous.then(
2048
+ () => this._present(request),
2049
+ () => this._present(request)
2050
+ );
2051
+ const mine = settled.then(NOOP, NOOP).then(() => {
2052
+ if (this._elicitationQueue === mine) this._elicitationQueue = null;
2053
+ });
2054
+ this._elicitationQueue = mine;
2055
+ return settled;
2056
+ }
2057
+ /** Hand ONE request to the presenter. Called only from the queue. */
2058
+ _present(request) {
2059
+ const presenter = this._presenterFor(request.target);
2060
+ if (!presenter) {
1912
2061
  if (!this._warnedNoPresenter) {
1913
2062
  this._warnedNoPresenter = true;
1914
2063
  console.warn(
1915
- "[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."
2064
+ "[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."
1916
2065
  );
1917
2066
  }
1918
- return Promise.resolve({ action: "cancel" });
2067
+ return Promise.reject(new AparteElicitationAbortError("no-presenter"));
1919
2068
  }
1920
- return this._elicitationPresenter(request);
2069
+ if (request.signal?.aborted) return Promise.reject(new AparteElicitationAbortError("aborted"));
2070
+ return presenter(request);
1921
2071
  }
1922
2072
  /**
1923
2073
  * Reset ALL configuration back to defaults — providers, registries, model
@@ -1943,7 +2093,8 @@ class AparteConfig {
1943
2093
  this._artifactPreviewBuilder = void 0;
1944
2094
  this._keyProvider = void 0;
1945
2095
  this._conversationManager = void 0;
1946
- this._elicitationPresenter = void 0;
2096
+ this._elicitationPresenters = [];
2097
+ this._elicitationQueue = null;
1947
2098
  this._locale = APARTE_DEFAULT_LOCALE;
1948
2099
  this._actions = [];
1949
2100
  this._sanitizer = defaultSanitizer;
@@ -2094,9 +2245,9 @@ function highlightMarkdownFences(contentEl) {
2094
2245
  }
2095
2246
  const textRenderer = {
2096
2247
  type: "text",
2097
- render: (segment) => `<div class="segment segment-text" data-segment-id="${escapeHtml(segment.id)}"><div class="segment-content">${contextConfig().renderMarkdown(segment.content)}</div></div>`,
2248
+ 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>`,
2098
2249
  update: (el2, segment) => {
2099
- const contentEl = el2.querySelector(".segment-content");
2250
+ const contentEl = el2.querySelector(".aparte-segment-content");
2100
2251
  if (!contentEl) return;
2101
2252
  writeStreamedMarkdown(
2102
2253
  el2,
@@ -2124,17 +2275,17 @@ const thinkingRenderer = {
2124
2275
  * new class of risk. With no Markdown provider registered it degrades to the
2125
2276
  * zero-dependency default (escape + `<br>`), which is what it used to be.
2126
2277
  */
2127
- render: (segment) => `<details class="segment segment-thinking" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed === false ? "open" : ""}><summary class="thinking-header"><span class="thinking-label">${escapeHtml(segment.label || contextConfig().t("thinking"))}</span><span class="thinking-toggle"></span></summary><div class="thinking-content">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
2278
+ 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>`,
2128
2279
  /**
2129
2280
  * The default label is the only config-derived text here — and `segment.label`
2130
2281
  * still wins, exactly as in `render`, because that string is the app's.
2131
2282
  */
2132
2283
  relabel: (el2, segment) => {
2133
- const label = el2.querySelector(".thinking-label");
2284
+ const label = el2.querySelector(".aparte-thinking-label");
2134
2285
  if (label) label.textContent = segment.label || contextConfig().t("thinking");
2135
2286
  },
2136
2287
  update: (el2, segment) => {
2137
- const contentEl = el2.querySelector(".thinking-content");
2288
+ const contentEl = el2.querySelector(".aparte-thinking-content");
2138
2289
  if (!contentEl) return;
2139
2290
  const wasAtBottom = contentEl.scrollHeight - contentEl.scrollTop - contentEl.clientHeight <= THINKING_ANCHOR_SLACK;
2140
2291
  const streaming = segment.isStreaming !== false;
@@ -2194,38 +2345,38 @@ function streamHighlight(element, paneSelector, content, lang, segId) {
2194
2345
  const codeRenderer = {
2195
2346
  type: "code",
2196
2347
  render: (segment) => `
2197
- <div class="segment segment-code" data-segment-id="${escapeHtml(segment.id)}">
2198
- <div class="code-header">
2199
- ${segment.filename ? `<span class="code-filename">${escapeHtml(segment.filename)}</span>` : `<span class="code-header-filler"></span>`}
2200
- <span class="code-language">${escapeHtml(segment.language || "")}</span>
2201
- <button class="code-copy" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}">
2348
+ <div class="aparte-segment aparte-segment-code" data-segment-id="${escapeHtml(segment.id)}">
2349
+ <div class="aparte-code-header">
2350
+ ${segment.filename ? `<span class="aparte-code-filename">${escapeHtml(segment.filename)}</span>` : `<span class="aparte-code-header-filler"></span>`}
2351
+ <span class="aparte-code-language">${escapeHtml(segment.language || "")}</span>
2352
+ <button class="aparte-code-copy" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}">
2202
2353
  ${contextConfig().getIcon("copy")}
2203
2354
  </button>
2204
2355
  </div>
2205
- <div class="code-content-wrapper">
2356
+ <div class="aparte-code-content-wrapper">
2206
2357
  <pre><code class="language-${escapeHtml(segment.language || "text")}">${escapeHtml(segment.content)}</code></pre>
2207
2358
  </div>
2208
2359
  </div>
2209
2360
  `,
2210
2361
  /** The copy button's icon and tooltip — nothing else here comes from config. */
2211
2362
  relabel: (element) => {
2212
- const copyBtn = element.querySelector(".code-copy");
2363
+ const copyBtn = element.querySelector(".aparte-code-copy");
2213
2364
  if (!copyBtn || copyBtn.dataset.copied) return;
2214
2365
  copyBtn.setAttribute("title", contextConfig().t("copy"));
2215
2366
  copyBtn.innerHTML = contextConfig().getIcon("copy");
2216
2367
  },
2217
2368
  setup: (element, segment) => {
2218
- const wrapper = element.querySelector(".code-content-wrapper");
2369
+ const wrapper = element.querySelector(".aparte-code-content-wrapper");
2219
2370
  if (wrapper) {
2220
2371
  void contextConfig().highlightCode(segment.content, segment.language || "").then((html) => {
2221
2372
  wrapper.innerHTML = html;
2222
2373
  }).catch(() => {
2223
2374
  });
2224
2375
  }
2225
- const copyBtn = element.querySelector(".code-copy");
2376
+ const copyBtn = element.querySelector(".aparte-code-copy");
2226
2377
  if (copyBtn) {
2227
2378
  copyBtn.addEventListener("click", () => {
2228
- const source = element.querySelector(".code-content-wrapper")?.textContent ?? "";
2379
+ const source = element.querySelector(".aparte-code-content-wrapper")?.textContent ?? "";
2229
2380
  void navigator.clipboard.writeText(source).catch(() => {
2230
2381
  });
2231
2382
  copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
@@ -2241,9 +2392,9 @@ const codeRenderer = {
2241
2392
  },
2242
2393
  update: (element, segment) => {
2243
2394
  if (segment.isStreaming) {
2244
- streamHighlight(element, ".code-content-wrapper", segment.content, segment.language || "", segment.id);
2395
+ streamHighlight(element, ".aparte-code-content-wrapper", segment.content, segment.language || "", segment.id);
2245
2396
  } else {
2246
- const wrapper = element.querySelector(".code-content-wrapper");
2397
+ const wrapper = element.querySelector(".aparte-code-content-wrapper");
2247
2398
  if (wrapper) {
2248
2399
  void contextConfig().highlightCode(segment.content, segment.language || "").then((html) => {
2249
2400
  wrapper.innerHTML = html;
@@ -2267,14 +2418,14 @@ const errorRenderer = {
2267
2418
  return out;
2268
2419
  }
2269
2420
  return `
2270
- <div class="segment segment-error" data-segment-id="${escapeHtml(segment.id)}">
2271
- <div class="error-icon-wrapper">
2421
+ <div class="aparte-segment aparte-segment-error" data-segment-id="${escapeHtml(segment.id)}">
2422
+ <div class="aparte-error-icon-wrapper">
2272
2423
  ${contextConfig().getIcon("error") || "⚠"}
2273
2424
  </div>
2274
- <div class="error-content">
2275
- <div class="error-title">${escapeHtml(contextConfig().t("error"))}</div>
2276
- <div class="error-message">${escapeHtml(segment.content)}</div>
2277
- ${segment.details ? `<div class="error-details">${escapeHtml(segment.details)}</div>` : ""}
2425
+ <div class="aparte-error-content">
2426
+ <div class="aparte-error-title">${escapeHtml(contextConfig().t("error"))}</div>
2427
+ <div class="aparte-error-message">${escapeHtml(segment.content)}</div>
2428
+ ${segment.details ? `<div class="aparte-error-details">${escapeHtml(segment.details)}</div>` : ""}
2278
2429
  </div>
2279
2430
  </div>
2280
2431
  `;
@@ -2289,171 +2440,198 @@ const errorRenderer = {
2289
2440
  * language it arrived in. Relabelling it would be inventing content.
2290
2441
  */
2291
2442
  relabel: (element) => {
2292
- const wrap = element.querySelector(".error-icon-wrapper");
2443
+ const wrap = element.querySelector(".aparte-error-icon-wrapper");
2293
2444
  if (wrap) wrap.innerHTML = contextConfig().getIcon("error") || "⚠";
2294
- const title = element.querySelector(".error-title");
2445
+ const title = element.querySelector(".aparte-error-title");
2295
2446
  if (title) title.textContent = contextConfig().t("error");
2296
2447
  },
2297
2448
  getStyles: () => ``
2298
2449
  };
2299
- const progressRenderer = {
2300
- type: "progress",
2301
- render: (segment) => {
2302
- const label = escapeHtml(segment.label || "Progress");
2303
- const pct = Math.round(segment.percent || 0);
2304
- 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>`;
2305
- },
2306
- getStyles: () => ``
2307
- };
2450
+ function describeInput(segment) {
2451
+ const input = segment.toolCall?.input;
2452
+ if (!input || typeof input !== "object" || Object.keys(input).length === 0) return "";
2453
+ try {
2454
+ return JSON.stringify(input, null, 2);
2455
+ } catch {
2456
+ return "";
2457
+ }
2458
+ }
2459
+ function stateBadge(segment) {
2460
+ const cfg = contextConfig();
2461
+ const status = segment.status ?? "pending";
2462
+ const word = (key) => escapeHtml(cfg.t(key));
2463
+ if (status === "awaiting-approval") return word("approvalWaiting");
2464
+ if (status === "pending") return word("toolRunning");
2465
+ if (status === "resolved") return `${cfg.getIcon("check")}${word("toolCompleted")}`;
2466
+ if (status === "rejected") return `${cfg.getIcon("close")}${word("toolRejected")}`;
2467
+ if (status === "aborted") return `${cfg.getIcon("close")}${word("toolStopped")}`;
2468
+ return "";
2469
+ }
2470
+ function setPart(element, part, text) {
2471
+ const existing = element.querySelector(`[data-part="${part}"]`);
2472
+ if (!text) {
2473
+ existing?.remove();
2474
+ return;
2475
+ }
2476
+ if (existing) {
2477
+ const code2 = existing.querySelector("code");
2478
+ if (code2 && code2.textContent !== text) code2.textContent = text;
2479
+ return;
2480
+ }
2481
+ const detail = element.querySelector(".aparte-tool-detail");
2482
+ if (!detail) return;
2483
+ const cfg = contextConfig();
2484
+ const wrap = document.createElement("div");
2485
+ wrap.className = "aparte-tool-part";
2486
+ wrap.dataset["part"] = part;
2487
+ const label = document.createElement("span");
2488
+ label.className = "aparte-tool-part-label";
2489
+ label.textContent = cfg.t(part === "input" ? "toolInput" : "toolOutput");
2490
+ const body = document.createElement("div");
2491
+ body.className = "aparte-tool-part-body";
2492
+ const pre = document.createElement("pre");
2493
+ const code = document.createElement("code");
2494
+ code.textContent = text;
2495
+ pre.appendChild(code);
2496
+ body.appendChild(pre);
2497
+ wrap.append(label, body);
2498
+ if (part === "input") detail.prepend(wrap);
2499
+ else detail.appendChild(wrap);
2500
+ }
2501
+ function highlightParts(element) {
2502
+ const cfg = contextConfig();
2503
+ for (const body of element.querySelectorAll(".aparte-tool-part-body")) {
2504
+ const text = body.querySelector("code")?.textContent ?? "";
2505
+ if (!text) continue;
2506
+ void cfg.highlightCode(text, "json").then((html) => {
2507
+ if ((body.querySelector("code")?.textContent ?? "") !== text) return;
2508
+ body.innerHTML = html;
2509
+ }).catch(() => {
2510
+ });
2511
+ }
2512
+ }
2513
+ function pillMarkup(segment, name, status) {
2514
+ const badge = stateBadge(segment);
2515
+ return `
2516
+ <span class="aparte-tool-label">
2517
+ <span class="aparte-tool-icon">${contextConfig().getIcon("tool")}</span>
2518
+ <span class="aparte-tool-name">${escapeHtml(name)}</span>
2519
+ </span>
2520
+ <span class="aparte-tool-spinner" aria-hidden="true"${status === "pending" ? "" : " hidden"}></span>
2521
+ <span class="aparte-tool-state">${badge}</span>`;
2522
+ }
2308
2523
  const toolCallRenderer = {
2309
2524
  type: "tool_call",
2310
2525
  render: (segment) => {
2311
2526
  const name = segment.toolCall?.name ?? "tool";
2312
2527
  const status = segment.status ?? "pending";
2313
2528
  const toolCallId = segment.toolCall?.id ?? "";
2314
- if (status === "awaiting-approval") {
2315
- const loc = contextConfig().getLocale();
2316
- const approve = loc.approveTool ?? "Approve";
2317
- const reject = loc.rejectTool ?? "Reject";
2318
- return `
2319
- <div class="segment segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="awaiting-approval" data-tool-call-id="${escapeAttr(toolCallId)}">
2320
- <span class="tool-pill">
2321
- <span class="tool-pill-icon">${contextConfig().getIcon("tool")}</span>
2322
- <span class="tool-pill-name">${escapeHtml(name)}</span>
2323
- </span>
2324
- <span class="tool-approval" role="group" aria-label="${escapeAttr(name)}">
2325
- <button type="button" class="tool-approve-btn" data-tool-decision="approve" aria-label="${escapeAttr(approve)}">${escapeHtml(approve)}</button>
2326
- <button type="button" class="tool-reject-btn" data-tool-decision="reject" aria-label="${escapeAttr(reject)}">${escapeHtml(reject)}</button>
2327
- </span>
2328
- </div>
2329
- `;
2330
- }
2331
2529
  const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
2332
2530
  if (customRenderer) {
2333
2531
  const out = customRenderer.render(segment);
2334
2532
  if (out) return out;
2335
2533
  }
2336
- const statusIcon = status === "resolved" ? contextConfig().getIcon("check") : status === "aborted" || status === "rejected" ? contextConfig().getIcon("close") : "";
2337
- const spinner = status === "pending" ? `<span class="tool-pill-spinner" aria-hidden="true"></span>` : "";
2338
- return `
2339
- <div class="segment segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}">
2340
- <span class="tool-pill">
2341
- <span class="tool-pill-icon">${contextConfig().getIcon("tool")}</span>
2342
- <span class="tool-pill-name">${escapeHtml(name)}</span>
2343
- ${spinner}
2344
- ${statusIcon ? `<span class="tool-pill-status">${statusIcon}</span>` : ""}
2345
- </span>
2534
+ const pill = pillMarkup(segment, name, status);
2535
+ const input = describeInput(segment);
2536
+ const result = segment.result;
2537
+ if (!input && !result) {
2538
+ return `
2539
+ <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}
2346
2540
  </div>
2347
2541
  `;
2542
+ }
2543
+ const cfg = contextConfig();
2544
+ return `
2545
+ <details class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}">
2546
+ <summary class="aparte-tool-summary"><span class="aparte-tool-toggle"></span>${pill}</summary>
2547
+ <div class="aparte-tool-detail">
2548
+ ${input ? `
2549
+ <div class="aparte-tool-part" data-part="input">
2550
+ <span class="aparte-tool-part-label">${escapeHtml(cfg.t("toolInput"))}</span>
2551
+ <div class="aparte-tool-part-body"><pre><code>${escapeHtml(input)}</code></pre></div>
2552
+ </div>` : ""}
2553
+ ${result ? `
2554
+ <div class="aparte-tool-part" data-part="output">
2555
+ <span class="aparte-tool-part-label">${escapeHtml(cfg.t("toolOutput"))}</span>
2556
+ <div class="aparte-tool-part-body"><pre><code>${escapeHtml(result)}</code></pre></div>
2557
+ </div>` : ""}
2558
+ </div>
2559
+ </details>
2560
+ `;
2348
2561
  },
2349
2562
  /**
2350
- * The pill's glyphs and — when the turn is paused for a person the two labels
2351
- * on the approval gate. Those are the highest-stakes strings in the library: a
2352
- * reader deciding whether to let a tool run should not be reading them in a
2353
- * language they did not choose.
2563
+ * The pill's glyphs, and the label it wears while a person is deciding.
2564
+ *
2565
+ * The two highest-stakes strings in the library the approval labels are no
2566
+ * longer here: they are on the panel, which relabels itself. This still matters
2567
+ * though, because the request the pill describes stays open for as long as
2568
+ * somebody takes to decide, which is exactly long enough for a language switch.
2354
2569
  *
2355
- * No child node is added or removed, so the buttons keep their listeners and,
2356
- * critically, their FOCUS — this is the one control a keyboard user may be
2357
- * sitting on when a config change fires.
2570
+ * No child node is added or removed, per the relabel contract.
2358
2571
  */
2359
2572
  relabel: (element, segment) => {
2360
2573
  const cfg = contextConfig();
2361
- const icon = element.querySelector(".tool-pill-icon");
2574
+ const icon = element.querySelector(".aparte-tool-icon");
2362
2575
  if (icon) icon.innerHTML = cfg.getIcon("tool");
2363
- const statusEl = element.querySelector(".tool-pill-status");
2364
- if (statusEl) {
2365
- const s = segment.status;
2366
- statusEl.innerHTML = s === "resolved" ? cfg.getIcon("check") : s === "aborted" || s === "rejected" ? cfg.getIcon("close") : "";
2367
- }
2368
- const loc = cfg.getLocale();
2369
- const approve = element.querySelector(".tool-approve-btn");
2370
- if (approve) {
2371
- const label = loc.approveTool ?? "Approve";
2372
- approve.textContent = label;
2373
- approve.setAttribute("aria-label", label);
2374
- }
2375
- const reject = element.querySelector(".tool-reject-btn");
2376
- if (reject) {
2377
- const label = loc.rejectTool ?? "Reject";
2378
- reject.textContent = label;
2379
- reject.setAttribute("aria-label", label);
2380
- }
2576
+ const statusEl = element.querySelector(".aparte-tool-state");
2577
+ if (statusEl) statusEl.innerHTML = stateBadge(segment);
2381
2578
  },
2382
- setup: (element, segment) => {
2383
- if (segment.status === "awaiting-approval") {
2384
- const toolCallId = segment.toolCall?.id;
2385
- if (!toolCallId) return;
2386
- const host = element.closest("[data-aparte-host], aparte-chat, aparte-chat-viewport");
2387
- const decide = (approved) => element.dispatchEvent(new CustomEvent("aparte-tool-decision", {
2388
- bubbles: true,
2389
- composed: true,
2390
- detail: { toolCallId, approved, targetId: host?.id || void 0 }
2391
- }));
2392
- element.querySelector('[data-tool-decision="approve"]')?.addEventListener("click", () => decide(true));
2393
- element.querySelector('[data-tool-decision="reject"]')?.addEventListener("click", () => decide(false));
2579
+ /**
2580
+ * Patch in place. NEVER re-render, and never touch `open`.
2581
+ *
2582
+ * Without an `update` the bubble replaces the element wholesale — and a tool call
2583
+ * changes status several times per turn, so a disclosure the reader opened would
2584
+ * slam shut under them every time. The same rule the reasoning block has, for the
2585
+ * same reason: `collapsed` is honoured only when an update explicitly carries it.
2586
+ *
2587
+ * The one case that cannot be patched is a change of ELEMENT: a call that had
2588
+ * neither arguments nor a result rendered as a plain pill, and gaining a result
2589
+ * makes it a `<details>`. That falls back to a rebuild, which costs nothing because
2590
+ * there was no disclosure to lose.
2591
+ */
2592
+ update: (element, segment) => {
2593
+ const status = segment.status ?? "pending";
2594
+ const input = describeInput(segment);
2595
+ const wantsDetail = !!input || !!segment.result;
2596
+ const custom = contextConfig().getToolRenderer(segment.toolCall?.name);
2597
+ if (custom || wantsDetail !== (element.tagName === "DETAILS")) {
2598
+ const out = toolCallRenderer.render(segment);
2599
+ const tpl = document.createElement("template");
2600
+ tpl.innerHTML = typeof out === "string" ? out.trim() : "";
2601
+ const rebuilt = typeof out === "string" ? tpl.content.firstElementChild : out;
2602
+ if (rebuilt) {
2603
+ element.replaceWith(rebuilt);
2604
+ toolCallRenderer.setup?.(rebuilt, segment);
2605
+ }
2394
2606
  return;
2395
2607
  }
2608
+ element.setAttribute("data-status", status);
2609
+ const badge = element.querySelector(".aparte-tool-state");
2610
+ if (badge) badge.innerHTML = stateBadge(segment);
2611
+ element.querySelector(".aparte-tool-spinner")?.toggleAttribute("hidden", status !== "pending");
2612
+ setPart(element, "input", input);
2613
+ setPart(element, "output", segment.result ?? "");
2614
+ highlightParts(element);
2615
+ },
2616
+ setup: (element, segment) => {
2617
+ highlightParts(element);
2396
2618
  const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
2397
2619
  customRenderer?.setup?.(element, segment);
2398
2620
  },
2399
- getStyles: () => `
2400
- .segment-tool-call { display: flex; padding: 2px 0; }
2401
- .tool-pill {
2402
- display: inline-flex;
2403
- align-items: center;
2404
- gap: 5px;
2405
- padding: 3px 10px 3px 7px;
2406
- border-radius: 99px;
2407
- font-size: 0.78rem;
2408
- font-weight: 500;
2409
- border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
2410
- background: var(--aparte-surface, #f8f8f8);
2411
- color: var(--aparte-text-secondary, rgba(0,0,0,0.55));
2412
- user-select: none;
2413
- }
2414
- [data-status="resolved"] .tool-pill {
2415
- border-color: var(--aparte-success-border, rgba(34,197,94,0.3));
2416
- background: var(--aparte-success-surface, rgba(34,197,94,0.06));
2417
- color: var(--aparte-success, rgb(21,128,61));
2418
- }
2419
- [data-status="aborted"] .tool-pill,
2420
- [data-status="rejected"] .tool-pill {
2421
- border-color: var(--aparte-error-border, rgba(239,68,68,0.3));
2422
- background: var(--aparte-error-surface, rgba(239,68,68,0.06));
2423
- color: var(--aparte-error, rgb(185,28,28));
2424
- }
2425
- .tool-approval { display: inline-flex; gap: 6px; margin-left: 8px; vertical-align: middle; }
2426
- .tool-approve-btn, .tool-reject-btn {
2427
- font: inherit; font-size: 0.78rem; font-weight: 600; line-height: 1;
2428
- padding: 4px 12px; border-radius: 99px; cursor: pointer;
2429
- border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
2430
- background: var(--aparte-surface, #f8f8f8);
2431
- }
2432
- .tool-approve-btn { color: var(--aparte-success, rgb(21,128,61)); border-color: var(--aparte-success-border, rgba(34,197,94,0.4)); }
2433
- .tool-reject-btn { color: var(--aparte-error, rgb(185,28,28)); border-color: var(--aparte-error-border, rgba(239,68,68,0.4)); }
2434
- .tool-approve-btn:hover { background: var(--aparte-success-surface, rgba(34,197,94,0.1)); }
2435
- .tool-reject-btn:hover { background: var(--aparte-error-surface, rgba(239,68,68,0.1)); }
2436
- .tool-pill-spinner {
2437
- width: 10px; height: 10px;
2438
- border: 1.5px solid currentColor;
2439
- border-top-color: transparent;
2440
- border-radius: 50%;
2441
- display: inline-block;
2442
- animation: tool-spin 0.7s linear infinite;
2443
- }
2444
- .tool-pill-status { font-size: 0.75rem; }
2445
- @keyframes tool-spin { to { transform: rotate(360deg); } }
2446
- `
2621
+ // CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
2622
+ // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
2623
+ // has no other way onto the page.
2624
+ getStyles: () => ""
2447
2625
  };
2448
2626
  const waitingLabel = () => contextConfig().t("generating");
2449
2627
  const pipelineWaitingRenderer = {
2450
2628
  type: "pipeline-waiting",
2451
2629
  render: (segment) => {
2452
2630
  return `
2453
- <div class="segment segment-pipeline-waiting" data-segment-id="${escapeHtml(segment.id)}" aria-label="${escapeAttr(waitingLabel())}" role="status">
2454
- <span class="pw-dot"></span>
2455
- <span class="pw-dot"></span>
2456
- <span class="pw-dot"></span>
2631
+ <div class="aparte-segment aparte-segment-pipeline-waiting" data-segment-id="${escapeHtml(segment.id)}" aria-label="${escapeAttr(waitingLabel())}" role="status">
2632
+ <span class="aparte-pw-dot"></span>
2633
+ <span class="aparte-pw-dot"></span>
2634
+ <span class="aparte-pw-dot"></span>
2457
2635
  </div>`;
2458
2636
  },
2459
2637
  update: () => {
@@ -2478,30 +2656,10 @@ const pipelineWaitingRenderer = {
2478
2656
  });
2479
2657
  observer.observe(parent, { childList: true });
2480
2658
  },
2481
- getStyles: () => `
2482
- .segment-pipeline-waiting {
2483
- display: flex;
2484
- align-items: center;
2485
- gap: 5px;
2486
- padding: 6px 2px;
2487
- min-height: 28px;
2488
- }
2489
- .pw-dot {
2490
- display: inline-block;
2491
- width: 8px;
2492
- height: 8px;
2493
- border-radius: 50%;
2494
- background: var(--aparte-text-muted, #6b7280);
2495
- opacity: 0.3;
2496
- animation: pw-pulse 1.2s ease-in-out infinite;
2497
- }
2498
- .pw-dot:nth-child(2) { animation-delay: 0.2s; }
2499
- .pw-dot:nth-child(3) { animation-delay: 0.4s; }
2500
- @keyframes pw-pulse {
2501
- 0%, 80%, 100% { opacity: 0.3; transform: scale(0.85); }
2502
- 40% { opacity: 1; transform: scale(1.1); }
2503
- }
2504
- `
2659
+ // CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
2660
+ // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
2661
+ // has no other way onto the page.
2662
+ getStyles: () => ""
2505
2663
  };
2506
2664
  function stripCodeFences(content) {
2507
2665
  let s = content;
@@ -2704,7 +2862,7 @@ function renderBinaryFileArtifact(segment, kind) {
2704
2862
  if (cached && !isStreaming) {
2705
2863
  const previewBody = cached.previewHtml ? contextConfig().sanitizeHtml(cached.previewHtml) : `<div class="aparte-art-file__preview-empty">Preview not available for ${escapeHtml(kind)} yet</div>`;
2706
2864
  return `
2707
- <div class="segment segment-artifact-file"
2865
+ <div class="aparte-segment aparte-segment-artifact-file"
2708
2866
  data-segment-id="${escapeHtml(segment.id)}"
2709
2867
  data-artifact-type="${escapeHtml(kind)}"
2710
2868
  data-state="ready">
@@ -2730,7 +2888,7 @@ function renderBinaryFileArtifact(segment, kind) {
2730
2888
  const cleanContent = stripCodeFences(segment.content || "");
2731
2889
  const subText = isStreaming ? contextConfig().t("generating") : contextConfig().t("rebuildingPreview");
2732
2890
  return `
2733
- <div class="segment segment-artifact-file"
2891
+ <div class="aparte-segment aparte-segment-artifact-file"
2734
2892
  data-segment-id="${escapeHtml(segment.id)}"
2735
2893
  data-artifact-type="${escapeHtml(kind)}"
2736
2894
  data-state="${isStreaming ? "streaming" : "compiling"}">
@@ -2925,7 +3083,7 @@ const artifactRenderer = {
2925
3083
  const isBinary = BINARY_FILE_KINDS.has(kind);
2926
3084
  const cleanContent = stripCodeFences(segment.content || "");
2927
3085
  return `
2928
- <div class="segment segment-artifact-card"
3086
+ <div class="aparte-segment aparte-segment-artifact-card"
2929
3087
  data-segment-id="${escapeHtml(segment.id)}"
2930
3088
  data-artifact-type="${escapeHtml(kind)}"
2931
3089
  data-streaming="${isStreaming ? "true" : "false"}"
@@ -2953,7 +3111,7 @@ const artifactRenderer = {
2953
3111
  </nav>
2954
3112
  <div class="aparte-art-card__body">
2955
3113
  <div class="aparte-art-card__pane" data-pane="code">
2956
- <div class="code-content-wrapper">
3114
+ <div class="aparte-code-content-wrapper">
2957
3115
  <pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>
2958
3116
  </div>
2959
3117
  </div>
@@ -3004,7 +3162,7 @@ const artifactRenderer = {
3004
3162
  setupBinaryFileArtifact(element, segment, kind);
3005
3163
  return;
3006
3164
  }
3007
- const wrapper = element.querySelector(".code-content-wrapper");
3165
+ const wrapper = element.querySelector(".aparte-code-content-wrapper");
3008
3166
  if (wrapper) {
3009
3167
  const displayLang = languageForKind(kind);
3010
3168
  const cleanContent = stripCodeFences(segment.content || "");
@@ -3079,13 +3237,13 @@ const artifactRenderer = {
3079
3237
  const cleanContent = stripCodeFences(segment.content || "");
3080
3238
  if (isStreaming) {
3081
3239
  const segId = element.getAttribute("data-segment-id") ?? segment.id;
3082
- streamHighlight(element, ".code-content-wrapper", cleanContent, languageForKind(kind), segId);
3240
+ streamHighlight(element, ".aparte-code-content-wrapper", cleanContent, languageForKind(kind), segId);
3083
3241
  } else {
3084
- const codeEl = element.querySelector(".code-content-wrapper code");
3242
+ const codeEl = element.querySelector(".aparte-code-content-wrapper code");
3085
3243
  if (codeEl) {
3086
3244
  codeEl.textContent = cleanContent;
3087
3245
  } else {
3088
- const wrapper = element.querySelector(".code-content-wrapper");
3246
+ const wrapper = element.querySelector(".aparte-code-content-wrapper");
3089
3247
  if (wrapper) {
3090
3248
  const displayLang = languageForKind(kind);
3091
3249
  wrapper.innerHTML = `<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>`;
@@ -3095,7 +3253,7 @@ const artifactRenderer = {
3095
3253
  if (wasStreaming && !isStreaming) {
3096
3254
  element.setAttribute("data-streaming", "false");
3097
3255
  element.querySelector(".aparte-art-card__pulse")?.remove();
3098
- const wrapper = element.querySelector(".code-content-wrapper");
3256
+ const wrapper = element.querySelector(".aparte-code-content-wrapper");
3099
3257
  if (wrapper) {
3100
3258
  const displayLang = languageForKind(kind);
3101
3259
  void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
@@ -3108,296 +3266,10 @@ const artifactRenderer = {
3108
3266
  });
3109
3267
  }
3110
3268
  },
3111
- getStyles: () => `
3112
- .segment-artifact-card {
3113
- display: flex; flex-direction: column;
3114
- margin: 8px 0;
3115
- border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
3116
- border-radius: 12px;
3117
- background: var(--aparte-surface, #fff);
3118
- overflow: hidden;
3119
- font: inherit;
3120
- color: var(--aparte-text, inherit);
3121
- }
3122
- .aparte-art-card__header {
3123
- display: flex; align-items: center; justify-content: space-between;
3124
- padding: 8px 10px;
3125
- border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
3126
- background: var(--aparte-surface-2, rgba(0,0,0,0.02));
3127
- min-height: 36px;
3128
- }
3129
- .aparte-art-card__title-block { display: flex; align-items: center; gap: 8px; min-width: 0; }
3130
- .aparte-art-card__kind {
3131
- font-size: 0.7rem; font-weight: 600;
3132
- text-transform: uppercase; letter-spacing: 0.04em;
3133
- padding: 2px 6px; border-radius: 4px;
3134
- background: var(--aparte-surface, #fff);
3135
- border: 1px solid var(--aparte-border, rgba(0,0,0,0.1));
3136
- color: var(--aparte-text-muted, rgba(0,0,0,0.6));
3137
- }
3138
- .aparte-art-card__title {
3139
- font-size: 0.85rem; font-weight: 500;
3140
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
3141
- }
3142
- .aparte-art-card__pulse {
3143
- width: 8px; height: 8px; border-radius: 50%;
3144
- background: var(--aparte-accent, #3b82f6);
3145
- animation: aparte-art-card-pulse 1.2s ease-in-out infinite;
3146
- }
3147
- @keyframes aparte-art-card-pulse {
3148
- 0%, 100% { opacity: 0.35; transform: scale(0.85); }
3149
- 50% { opacity: 1; transform: scale(1.1); }
3150
- }
3151
- .aparte-art-card__actions { display: flex; gap: 4px; }
3152
- .aparte-art-card__btn {
3153
- display: inline-flex; align-items: center; justify-content: center;
3154
- width: 28px; height: 28px;
3155
- background: transparent;
3156
- border: 1px solid transparent;
3157
- border-radius: 6px;
3158
- color: var(--aparte-text-muted, rgba(0,0,0,0.55));
3159
- cursor: pointer;
3160
- transition: background 0.12s, border-color 0.12s, color 0.12s;
3161
- }
3162
- .aparte-art-card__btn:hover:not(:disabled) {
3163
- background: var(--aparte-surface-hover, rgba(0,0,0,0.04));
3164
- border-color: var(--aparte-border, rgba(0,0,0,0.1));
3165
- color: var(--aparte-text, inherit);
3166
- }
3167
- .aparte-art-card__btn:disabled { opacity: 0.4; cursor: not-allowed; }
3168
- .aparte-art-card__tabs {
3169
- /* justify-content and the padding are DECLARED, not left to a default.
3170
- Core is light DOM on purpose - no shadow root, no ::part(), any selector
3171
- reaches in - and the corollary is that a component must state what its
3172
- layout depends on, because an undeclared property has nothing to override
3173
- a host rule with. A consuming page with a bare nav selector setting
3174
- justify-content: space-between and padding-top (this library's own docs
3175
- site had exactly that) otherwise pushes these two tabs to opposite ends of
3176
- the card and pads the row out. No backticks in here: this block is a JS
3177
- template literal, and one of them ended it.
3178
-
3179
- flex-end, and Code first in the DOM. The card OPENS on Code - mounting the
3180
- preview would execute model-authored code with no gesture (ratified
3181
- decision #8) - and a selected tab sitting second reads backwards. Right,
3182
- because the header above puts the artifact's identity on the left and its
3183
- copy/download buttons on the right, so this keeps every control in one
3184
- column. DOM order is also keyboard order, so the tab a reader lands on
3185
- first is the one already showing. */
3186
- display: flex; justify-content: flex-end; align-items: stretch; gap: 2px;
3187
- padding: 4px 8px 0;
3188
- border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
3189
- background: var(--aparte-surface-2, rgba(0,0,0,0.02));
3190
- }
3191
- .aparte-art-card__tabs button {
3192
- padding: 6px 12px;
3193
- font-size: 0.78rem;
3194
- background: transparent;
3195
- border: 1px solid transparent;
3196
- border-bottom: none;
3197
- border-radius: 6px 6px 0 0;
3198
- color: var(--aparte-text-muted, rgba(0,0,0,0.6));
3199
- cursor: pointer;
3200
- transition: background 0.12s, color 0.12s;
3201
- }
3202
- .aparte-art-card__tabs button[aria-selected="true"] {
3203
- background: var(--aparte-surface, #fff);
3204
- border-color: var(--aparte-border, rgba(0,0,0,0.08));
3205
- color: var(--aparte-text, inherit);
3206
- font-weight: 500;
3207
- }
3208
- .aparte-art-card__tabs button:disabled { opacity: 0.4; cursor: not-allowed; }
3209
- /* The card's heights, as variables with ONE owner each.
3210
- They were four hardcoded numbers in the only part of this card that did not
3211
- use a variable - everything else here already reads var(--aparte-code-bg) and
3212
- friends - and two of them had to agree while a third contradicted a fourth:
3213
- the code pane repeated the body's 600px, and the "press Preview" placeholder
3214
- was 120px tall inside a body whose min-height said 80, so that minimum applied
3215
- to nothing.
3216
- The frame stays a FIXED height rather than an aspect ratio, which is what
3217
- embeds of arbitrary HTML actually do - CodeSandbox documents 500px, StackBlitz
3218
- takes a height parameter - because a frame with an opaque origin cannot be
3219
- measured and a 16/10 ratio on a wide card is enormous. The vh cap is the part
3220
- that was missing: a fixed 480px should not own a phone screen.
3221
- Each default lives in its read, as var(--x, literal), the way every other
3222
- value in this file already does - not in a declaration block on top of the
3223
- fallbacks, which would be two owners of one number again. It also means the
3224
- docs' CSS-variable generator finds them: its sweep looks for reads that
3225
- carry a fallback, so a read without one is a public knob nobody documents. */
3226
- .aparte-art-card__body {
3227
- position: relative;
3228
- /* The placeholder is the tallest thing this box can hold while empty, so it
3229
- IS the minimum - one number instead of two that disagreed. */
3230
- min-height: var(--aparte-artifact-pending-height, 120px);
3231
- max-height: var(--aparte-artifact-body-max, 600px);
3232
- overflow: hidden;
3233
- }
3234
- .aparte-art-card__pane { display: none; height: 100%; }
3235
- .segment-artifact-card[data-tab="code"] .aparte-art-card__pane[data-pane="code"] { display: block; }
3236
- .segment-artifact-card[data-tab="preview"] .aparte-art-card__pane[data-pane="preview"] { display: block; }
3237
- .aparte-art-card__pane[data-pane="code"] {
3238
- /* The body already caps this; repeating the number was the second owner. */
3239
- max-height: var(--aparte-artifact-body-max, 600px); overflow: auto;
3240
- }
3241
- .aparte-art-card__pane[data-pane="code"] pre {
3242
- margin: 0; padding: 12px;
3243
- font-size: 0.82rem;
3244
- background: var(--aparte-code-bg, #fafafa);
3245
- }
3246
- .aparte-art-card__frame {
3247
- display: block;
3248
- width: 100%;
3249
- height: min(var(--aparte-artifact-frame-height, 480px), var(--aparte-artifact-frame-max, 70vh));
3250
- border: 0;
3251
- background: #fff;
3252
- }
3253
- .aparte-art-card__pending {
3254
- display: flex; align-items: center; justify-content: center;
3255
- height: var(--aparte-artifact-pending-height, 120px);
3256
- color: var(--aparte-text-muted, rgba(0,0,0,0.5));
3257
- font-size: 0.85rem;
3258
- font-style: italic;
3259
- }
3260
- /* ── Binary file artifact (xlsx/pdf/docx) ──────────────────── */
3261
- .segment-artifact-file {
3262
- display: flex; flex-direction: column;
3263
- margin: 8px 0;
3264
- border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
3265
- border-radius: 12px;
3266
- background: var(--aparte-surface, #fff);
3267
- overflow: hidden;
3268
- color: var(--aparte-text, inherit);
3269
- font: inherit;
3270
- }
3271
- .aparte-art-file__card {
3272
- display: flex; align-items: center; gap: 12px;
3273
- padding: 12px 14px;
3274
- background: var(--aparte-surface-2, rgba(0,0,0,0.02));
3275
- border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
3276
- }
3277
- .aparte-art-file__body {
3278
- position: relative;
3279
- }
3280
- .aparte-art-file__code-pane {
3281
- max-height: var(--aparte-artifact-file-code-max, 360px); overflow: auto;
3282
- background: var(--aparte-code-bg, #fafafa);
3283
- }
3284
- .aparte-art-file__code-pane pre {
3285
- margin: 0; padding: 12px;
3286
- font-size: 0.82rem;
3287
- font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
3288
- }
3289
- .aparte-art-file__preview-pane {
3290
- max-height: var(--aparte-artifact-file-preview-max, 460px);
3291
- overflow: auto;
3292
- background: #fff;
3293
- /* Preview is a document view — force light scheme regardless of
3294
- the app theme, with a dark text colour so cells stay readable. */
3295
- color: #1f2937;
3296
- }
3297
- .aparte-art-file__icon {
3298
- width: 40px; height: 40px;
3299
- border-radius: 8px;
3300
- display: flex; align-items: center; justify-content: center;
3301
- background: linear-gradient(135deg, #1d6f42, #0f5132);
3302
- color: #fff;
3303
- font-weight: 700;
3304
- font-size: 0.78rem;
3305
- letter-spacing: 0.04em;
3306
- flex-shrink: 0;
3307
- }
3308
- .aparte-art-file__icon[data-kind="pdf"] { background: linear-gradient(135deg, #c0392b, #7d1f17); }
3309
- .aparte-art-file__icon[data-kind="docx"] { background: linear-gradient(135deg, #1e5288, #0f3060); }
3310
- .aparte-art-file__meta { flex: 1 1 auto; min-width: 0; }
3311
- .aparte-art-file__meta-name {
3312
- font-weight: 600; font-size: 0.92rem;
3313
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
3314
- }
3315
- .aparte-art-file__meta-sub {
3316
- font-size: 0.78rem;
3317
- color: var(--aparte-text-muted, rgba(0,0,0,0.55));
3318
- }
3319
- .aparte-art-file__actions { display: flex; gap: 6px; flex-shrink: 0; }
3320
- .aparte-art-file__btn {
3321
- border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
3322
- background: var(--aparte-surface, #fff);
3323
- color: var(--aparte-text, inherit);
3324
- padding: 6px 10px;
3325
- border-radius: 6px;
3326
- font-size: 0.8rem;
3327
- cursor: pointer;
3328
- transition: background 0.12s ease;
3329
- }
3330
- .aparte-art-file__btn:hover { background: var(--aparte-surface-2, rgba(0,0,0,0.05)); }
3331
- .aparte-art-file__btn--primary {
3332
- background: var(--aparte-primary, #6366f1);
3333
- color: #fff;
3334
- border-color: var(--aparte-primary, #6366f1);
3335
- }
3336
- .aparte-art-file__btn--primary:hover { filter: brightness(1.07); }
3337
- /* Hide the download button until the sandbox has produced the buffer.
3338
- Avoids confusing the user with a disabled-but-styled-primary button
3339
- during streaming / compiling. */
3340
- .segment-artifact-file:not([data-state="ready"]) .aparte-art-file__btn[data-action="download"] {
3341
- display: none;
3342
- }
3343
- .aparte-art-file__preview-pane table {
3344
- border-collapse: collapse;
3345
- width: 100%;
3346
- font-size: 0.82rem;
3347
- font-family: inherit;
3348
- }
3349
- .aparte-art-file__preview-pane th,
3350
- .aparte-art-file__preview-pane td {
3351
- border: 1px solid rgba(0,0,0,0.10);
3352
- padding: 6px 10px;
3353
- text-align: left;
3354
- white-space: nowrap;
3355
- color: #1f2937;
3356
- background: #fff;
3357
- }
3358
- .aparte-art-file__preview-pane tr:nth-child(odd) td { background: #f9fafb; }
3359
- .aparte-art-file__preview-pane tr:first-child td {
3360
- background: #f3f4f6;
3361
- font-weight: 600;
3362
- position: sticky; top: 0;
3363
- color: #111827;
3364
- }
3365
- .aparte-art-file__preview-empty {
3366
- padding: 20px; text-align: center;
3367
- color: var(--aparte-text-muted, rgba(0,0,0,0.5));
3368
- font-size: 0.85rem; font-style: italic;
3369
- }
3370
- .segment-artifact-file[data-state="error"] .aparte-art-file__icon {
3371
- background: linear-gradient(135deg, #c0392b, #7d1f17);
3372
- }
3373
- .aparte-art-file__error {
3374
- padding: 16px 18px;
3375
- background: rgba(239, 68, 68, 0.05);
3376
- border-top: 1px solid rgba(239, 68, 68, 0.2);
3377
- color: var(--aparte-text, inherit);
3378
- }
3379
- .aparte-art-file__error-title {
3380
- font-weight: 600;
3381
- font-size: 0.9rem;
3382
- margin-bottom: 6px;
3383
- color: #b91c1c;
3384
- }
3385
- .aparte-art-file__error-msg {
3386
- font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
3387
- font-size: 0.78rem;
3388
- padding: 6px 10px;
3389
- background: rgba(0,0,0,0.04);
3390
- border-radius: 4px;
3391
- margin-bottom: 8px;
3392
- color: #7f1d1d;
3393
- word-break: break-word;
3394
- }
3395
- .aparte-art-file__error-hint {
3396
- font-size: 0.78rem;
3397
- color: var(--aparte-text-muted, rgba(0,0,0,0.55));
3398
- font-style: italic;
3399
- }
3400
- `
3269
+ // CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
3270
+ // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
3271
+ // has no other way onto the page.
3272
+ getStyles: () => ""
3401
3273
  };
3402
3274
  function languageForKind(kind) {
3403
3275
  if (kind === "react") return "jsx";
@@ -3548,7 +3420,6 @@ const DEFAULT_RENDERERS = [
3548
3420
  thinkingRenderer,
3549
3421
  codeRenderer,
3550
3422
  errorRenderer,
3551
- progressRenderer,
3552
3423
  toolCallRenderer,
3553
3424
  artifactRenderer,
3554
3425
  pipelineWaitingRenderer
@@ -3575,7 +3446,8 @@ function adoptSegment(segments, segment, messageId) {
3575
3446
  ...segment,
3576
3447
  messageId,
3577
3448
  index: segments.length,
3578
- isStreaming: false
3449
+ isStreaming: false,
3450
+ ...segment.type === "tool_call" && segment.status === "awaiting-approval" ? { status: "aborted" } : {}
3579
3451
  };
3580
3452
  }
3581
3453
  const RESERVED = /* @__PURE__ */ new Set(["id", "type", "messageId", "index"]);
@@ -5362,38 +5234,415 @@ function readableToAsyncIterable(stream, signal) {
5362
5234
  }
5363
5235
  };
5364
5236
  }
5365
- const DEFAULT_TOOL_HANDLER_TIMEOUT_MS = 5 * 60 * 1e3;
5366
- function withToolTimeout(work, controller, timeoutMs) {
5367
- let timer;
5368
- const timedOut = new Promise((_resolve, reject) => {
5369
- timer = setTimeout(() => {
5370
- controller.abort();
5371
- reject(Object.assign(new Error(`tool handler exceeded ${timeoutMs}ms`), { name: "AbortError" }));
5372
- }, timeoutMs);
5237
+ let _relabels = null;
5238
+ function onRelabel(fn) {
5239
+ _relabels?.push(fn);
5240
+ }
5241
+ function el$1(tag, className, text) {
5242
+ const node = document.createElement(tag);
5243
+ if (className) node.className = className;
5244
+ if (text != null) node.textContent = text;
5245
+ return node;
5246
+ }
5247
+ function fieldHeader(parent, field, skipTitle) {
5248
+ let titleId;
5249
+ if (field.title && field.title.trim() !== skipTitle?.trim()) {
5250
+ const title = el$1("p", "aparte-elic-title", field.title);
5251
+ titleId = `elic-title-${uuid()}`;
5252
+ title.id = titleId;
5253
+ parent.appendChild(title);
5254
+ }
5255
+ if (field.description) parent.appendChild(el$1("p", "aparte-elic-desc", field.description));
5256
+ return titleId;
5257
+ }
5258
+ function labelGroup(list, opts) {
5259
+ list.setAttribute("role", opts.multiple ? "group" : "radiogroup");
5260
+ if (opts.titleId) list.setAttribute("aria-labelledby", opts.titleId);
5261
+ else if (opts.fallbackLabel) list.setAttribute("aria-label", opts.fallbackLabel);
5262
+ }
5263
+ function buildEnumField(field, onChange, fallbackLabel) {
5264
+ const wrap = el$1("div", "aparte-elic-field aparte-elic-enum");
5265
+ const titleId = fieldHeader(wrap, field, fallbackLabel);
5266
+ const list = el$1("div", "aparte-elic-options");
5267
+ labelGroup(list, { multiple: field.multiple, titleId, fallbackLabel });
5268
+ const name = `elic-${uuid()}`;
5269
+ const type = field.multiple ? "checkbox" : "radio";
5270
+ const allowOther = field.allowOther ?? contextConfig().getElicitationOptions().allowOther;
5271
+ const defaults = new Set(Array.isArray(field.default) ? field.default : field.default != null ? [field.default] : []);
5272
+ const buildOption = (value, label, description, recommended) => {
5273
+ const row = el$1("label", "aparte-elic-option" + (recommended ? " aparte-elic-option--recommended" : ""));
5274
+ const control = el$1("input", "aparte-elic-control");
5275
+ control.type = type;
5276
+ control.name = name;
5277
+ control.value = value;
5278
+ if (defaults.has(value)) control.checked = true;
5279
+ const body = el$1("span", "aparte-elic-option-body");
5280
+ body.appendChild(el$1("span", "aparte-elic-option-title", label));
5281
+ if (description) body.appendChild(el$1("span", "aparte-elic-option-desc", description));
5282
+ row.append(control, body);
5283
+ return row;
5284
+ };
5285
+ for (const opt of field.options) {
5286
+ list.appendChild(buildOption(opt.value, opt.label ?? opt.value, opt.description, opt.recommended));
5287
+ }
5288
+ let otherText = null;
5289
+ if (allowOther) {
5290
+ const row = el$1("label", "aparte-elic-option aparte-elic-option--other");
5291
+ const control = el$1("input", "aparte-elic-control");
5292
+ control.type = type;
5293
+ control.name = name;
5294
+ control.value = "__other__";
5295
+ const body = el$1("span", "aparte-elic-option-body");
5296
+ const t = contextConfig();
5297
+ const otherTitle = el$1("span", "aparte-elic-option-title", t.t("elicitationOther"));
5298
+ body.appendChild(otherTitle);
5299
+ otherText = el$1("input", "aparte-elic-other-input");
5300
+ otherText.type = "text";
5301
+ otherText.placeholder = t.t("elicitationOtherPlaceholder");
5302
+ otherText.style.display = "none";
5303
+ otherText.setAttribute("aria-label", t.t("elicitationOtherLabel"));
5304
+ const otherInput = otherText;
5305
+ onRelabel(() => {
5306
+ const now = contextConfig();
5307
+ otherTitle.textContent = now.t("elicitationOther");
5308
+ otherInput.placeholder = now.t("elicitationOtherPlaceholder");
5309
+ otherInput.setAttribute("aria-label", now.t("elicitationOtherLabel"));
5310
+ });
5311
+ body.appendChild(otherText);
5312
+ row.append(control, body);
5313
+ list.appendChild(row);
5314
+ }
5315
+ wrap.appendChild(list);
5316
+ const controls = () => Array.from(list.querySelectorAll(".aparte-elic-control"));
5317
+ const other = () => list.querySelector('input[value="__other__"]');
5318
+ list.addEventListener("change", () => {
5319
+ const o = other();
5320
+ if (o && otherText) {
5321
+ otherText.style.display = o.checked ? "" : "none";
5322
+ if (o.checked) otherText.focus();
5323
+ }
5324
+ onChange();
5373
5325
  });
5326
+ otherText?.addEventListener("input", onChange);
5327
+ const getValue = () => {
5328
+ const otherVal = other()?.checked && otherText?.value.trim() ? otherText.value.trim() : "";
5329
+ if (field.multiple) {
5330
+ const vals = controls().filter((c) => c.checked && c.value !== "__other__").map((c) => c.value);
5331
+ if (otherVal) vals.push(otherVal);
5332
+ return vals;
5333
+ }
5334
+ const checked = controls().find((c) => c.checked);
5335
+ if (!checked) return "";
5336
+ return checked.value === "__other__" ? otherVal : checked.value;
5337
+ };
5374
5338
  return {
5375
- raced: Promise.race([work(controller.signal), timedOut]),
5376
- done: () => clearTimeout(timer)
5339
+ el: wrap,
5340
+ getValue,
5341
+ isComplete: () => {
5342
+ const v = getValue();
5343
+ return field.multiple ? v.length > 0 : v !== "";
5344
+ },
5345
+ focus: () => controls()[0]?.focus()
5377
5346
  };
5378
5347
  }
5379
- class AparteClient {
5380
- _boundHandler = null;
5381
- _boundAbortHandler = null;
5382
- _boundCompactHandler = null;
5383
- _boundRetryHandler = null;
5384
- _boundEditHandler = null;
5385
- _activeToolControllers = /* @__PURE__ */ new Set();
5386
- _isAborted = false;
5387
- /**
5388
- * Does this window event belong to this client?
5389
- *
5390
- * One rule for all five handlers, because it used to be four near-copies and
5391
- * one omission: `aparte-compact` had no guard at all, so in the two-client
5392
- * layout the JSDoc documents, a single compact event made BOTH clients run —
5393
- * two paid summarisation calls against whichever chat the DOM scan found
5394
- * first, and a global reset that wiped the other conversation.
5395
- *
5396
- * A scoped client also answers only events ADDRESSED to it. The old guard let
5348
+ function buildBooleanField(field, onChange, fallbackLabel) {
5349
+ const wrap = el$1("div", "aparte-elic-field aparte-elic-boolean");
5350
+ const titleId = fieldHeader(wrap, field, fallbackLabel);
5351
+ const list = el$1("div", "aparte-elic-options");
5352
+ labelGroup(list, { titleId, fallbackLabel });
5353
+ const name = `elic-${uuid()}`;
5354
+ const mk = (val, label, defaulted) => {
5355
+ const row = el$1("label", "aparte-elic-option");
5356
+ const control = el$1("input", "aparte-elic-control");
5357
+ control.type = "radio";
5358
+ control.name = name;
5359
+ control.value = val;
5360
+ if (field.default != null && String(field.default) === val) control.checked = true;
5361
+ const body = el$1("span", "aparte-elic-option-body");
5362
+ const title = el$1("span", "aparte-elic-option-title", label);
5363
+ if (defaulted) onRelabel(() => {
5364
+ title.textContent = contextConfig().t(defaulted);
5365
+ });
5366
+ body.appendChild(title);
5367
+ row.append(control, body);
5368
+ list.appendChild(row);
5369
+ };
5370
+ const t = contextConfig();
5371
+ mk("true", field.trueLabel ?? t.t("elicitationYes"), field.trueLabel ? void 0 : "elicitationYes");
5372
+ mk("false", field.falseLabel ?? t.t("elicitationNo"), field.falseLabel ? void 0 : "elicitationNo");
5373
+ wrap.appendChild(list);
5374
+ list.addEventListener("change", onChange);
5375
+ const checked = () => list.querySelector("input:checked");
5376
+ return {
5377
+ el: wrap,
5378
+ getValue: () => checked()?.value === "true",
5379
+ isComplete: () => checked() != null,
5380
+ focus: () => list.querySelector("input")?.focus()
5381
+ };
5382
+ }
5383
+ function buildStringField(field, onChange, fallbackLabel) {
5384
+ const wrap = el$1("div", "aparte-elic-field aparte-elic-string");
5385
+ fieldHeader(wrap, field, fallbackLabel);
5386
+ const input = field.multiline ? el$1("textarea", "aparte-elic-text") : el$1("input", "aparte-elic-text");
5387
+ if (!field.multiline) input.type = "text";
5388
+ if (field.placeholder) input.setAttribute("placeholder", field.placeholder);
5389
+ if (field.default) input.value = field.default;
5390
+ if (field.maxLength != null) input.setAttribute("maxlength", String(field.maxLength));
5391
+ const ownName = field.title || field.description || fallbackLabel || void 0;
5392
+ input.setAttribute("aria-label", ownName ?? contextConfig().t("elicitationAnswerLabel"));
5393
+ if (ownName == null) {
5394
+ const named = input;
5395
+ onRelabel(() => {
5396
+ named.setAttribute("aria-label", contextConfig().t("elicitationAnswerLabel"));
5397
+ });
5398
+ }
5399
+ wrap.appendChild(input);
5400
+ input.addEventListener("input", onChange);
5401
+ const required = field.required ?? true;
5402
+ const value = () => input.value;
5403
+ return {
5404
+ el: wrap,
5405
+ getValue: () => value(),
5406
+ isComplete: () => {
5407
+ const v = value().trim();
5408
+ if (!required && v === "") return true;
5409
+ return v.length >= (field.minLength ?? 1);
5410
+ },
5411
+ focus: () => input.focus()
5412
+ };
5413
+ }
5414
+ function buildField(field, onChange, fallbackLabel, key) {
5415
+ const custom = contextConfig().getElicitationFieldRenderer()?.(field, { key, notifyChange: onChange });
5416
+ if (custom) {
5417
+ return {
5418
+ el: custom.el,
5419
+ getValue: () => custom.getValue(),
5420
+ isComplete: () => custom.isComplete(),
5421
+ focus: () => custom.focus?.()
5422
+ };
5423
+ }
5424
+ switch (field.type) {
5425
+ case "enum":
5426
+ return buildEnumField(field, onChange, fallbackLabel);
5427
+ case "boolean":
5428
+ return buildBooleanField(field, onChange, fallbackLabel);
5429
+ case "string":
5430
+ return buildStringField(field, onChange, fallbackLabel);
5431
+ }
5432
+ }
5433
+ function buildElicitationPanel(message, schema, onChange) {
5434
+ const relabels = [];
5435
+ const outer = _relabels;
5436
+ _relabels = relabels;
5437
+ try {
5438
+ const built = buildPanel(message, schema, onChange);
5439
+ return { ...built, relabel: () => {
5440
+ for (const fn of relabels) fn();
5441
+ } };
5442
+ } finally {
5443
+ _relabels = outer;
5444
+ }
5445
+ }
5446
+ function buildPanel(message, schema, onChange) {
5447
+ const panel = el$1("div", "aparte-elic-panel");
5448
+ const asked = typeof message === "function" ? message() : message;
5449
+ if (asked) panel.appendChild(el$1("p", "aparte-elic-message", asked));
5450
+ const body = el$1("div", "aparte-elic-body");
5451
+ panel.appendChild(body);
5452
+ const dismiss = el$1("div", "aparte-elic-dismiss");
5453
+ panel.appendChild(dismiss);
5454
+ if (schema.type === "object") {
5455
+ let show = function(index) {
5456
+ current = index;
5457
+ fields.forEach((f, i) => {
5458
+ f.field.el.hidden = i !== index;
5459
+ });
5460
+ syncNav();
5461
+ };
5462
+ const entries = Object.entries(schema.properties);
5463
+ const requiredKeys = new Set(schema.required ?? entries.map(([k]) => k));
5464
+ const cfg = contextConfig();
5465
+ let syncNav = () => {
5466
+ };
5467
+ const notify = () => {
5468
+ syncNav();
5469
+ onChange();
5470
+ };
5471
+ const fields = entries.map(([key, field2]) => {
5472
+ const built = buildField(field2, notify, void 0, key);
5473
+ if (!field2.title && !built.el.querySelector(".aparte-elic-title")) {
5474
+ built.el.insertBefore(el$1("p", "aparte-elic-title", key), built.el.firstChild);
5475
+ }
5476
+ body.appendChild(built.el);
5477
+ return { key, field: built, required: requiredKeys.has(key), header: field2.header };
5478
+ });
5479
+ const isComplete = () => fields.every((f) => !f.required || f.field.isComplete());
5480
+ const api = {
5481
+ el: panel,
5482
+ dismiss,
5483
+ getContent: () => Object.fromEntries(fields.map((f) => [f.key, f.field.getValue()])),
5484
+ isComplete,
5485
+ focus: () => fields[0]?.field.focus(),
5486
+ // A form of one behaves like a single field; only a real form steps.
5487
+ mode: () => "submit",
5488
+ canProceed: isComplete,
5489
+ proceed: () => {
5490
+ }
5491
+ };
5492
+ if (fields.length < 2 || cfg.getElicitationOptions().layout !== "stepped") return api;
5493
+ panel.classList.add("aparte-elic-panel--stepped");
5494
+ let current = 0;
5495
+ const steps = el$1("div", "aparte-elic-steps");
5496
+ steps.setAttribute("role", "tablist");
5497
+ const chips = fields.map((f, i) => {
5498
+ const chip = el$1("button", "aparte-elic-step");
5499
+ chip.type = "button";
5500
+ chip.setAttribute("role", "tab");
5501
+ chip.textContent = f.header?.trim() || String(i + 1);
5502
+ chip.addEventListener("click", () => {
5503
+ show(i);
5504
+ f.field.focus();
5505
+ });
5506
+ steps.appendChild(chip);
5507
+ return chip;
5508
+ });
5509
+ panel.insertBefore(steps, body);
5510
+ syncNav = () => {
5511
+ chips.forEach((chip, i) => {
5512
+ chip.setAttribute("aria-selected", String(i === current));
5513
+ chip.toggleAttribute("data-answered", fields[i].field.isComplete());
5514
+ });
5515
+ };
5516
+ show(0);
5517
+ const last = () => current === fields.length - 1;
5518
+ return {
5519
+ ...api,
5520
+ focus: () => fields[current]?.field.focus(),
5521
+ mode: () => last() ? "submit" : "advance",
5522
+ // Advancing needs only THIS question answered; submitting needs them all,
5523
+ // which is what stops the last question from accepting a form with a hole
5524
+ // in it.
5525
+ canProceed: () => last() ? isComplete() : !!fields[current]?.field.isComplete(),
5526
+ proceed: () => {
5527
+ if (last()) return;
5528
+ show(current + 1);
5529
+ fields[current]?.field.focus();
5530
+ }
5531
+ };
5532
+ }
5533
+ const field = buildField(schema, onChange, asked);
5534
+ body.appendChild(field.el);
5535
+ return {
5536
+ el: panel,
5537
+ dismiss,
5538
+ getContent: () => field.getValue(),
5539
+ isComplete: () => field.isComplete(),
5540
+ focus: () => field.focus(),
5541
+ mode: () => "submit",
5542
+ canProceed: () => field.isComplete(),
5543
+ proceed: () => {
5544
+ }
5545
+ };
5546
+ }
5547
+ function el(tag, className, text) {
5548
+ const node = document.createElement(tag);
5549
+ if (className) node.className = className;
5550
+ if (text != null) node.textContent = text;
5551
+ return node;
5552
+ }
5553
+ function buildApprovalPanel(message, options, onChange) {
5554
+ const cfg = contextConfig();
5555
+ const panel = el("div", "aparte-elic-panel aparte-approval-panel");
5556
+ const askText = () => typeof message === "function" ? message() : message;
5557
+ const ask = askText() ? el("p", "aparte-elic-message", askText()) : null;
5558
+ if (ask) panel.appendChild(ask);
5559
+ const dismiss = el("div", "aparte-elic-dismiss");
5560
+ panel.appendChild(dismiss);
5561
+ let settle = null;
5562
+ let done = false;
5563
+ const fire = (answer) => {
5564
+ if (done) return;
5565
+ done = true;
5566
+ settle?.(answer);
5567
+ };
5568
+ const row = el("div", "aparte-approval-options");
5569
+ row.setAttribute("role", "group");
5570
+ row.setAttribute("aria-label", cfg.t("approvalOptionsLabel"));
5571
+ const labelled = [];
5572
+ const textOf = (option) => typeof option.label === "function" ? option.label() : option.label;
5573
+ for (const option of options) {
5574
+ const button = el("button", `aparte-approval-option aparte-approval-option--${option.tone ?? "affirm"}`);
5575
+ button.type = "button";
5576
+ button.textContent = textOf(option);
5577
+ button.addEventListener("click", () => fire({ option: option.value }));
5578
+ row.appendChild(button);
5579
+ labelled.push({ button, option });
5580
+ }
5581
+ panel.appendChild(row);
5582
+ const instruction = el("textarea", "aparte-approval-instruction");
5583
+ instruction.rows = 1;
5584
+ instruction.placeholder = cfg.t("approvalInstructionPlaceholder");
5585
+ instruction.setAttribute("aria-label", cfg.t("approvalInstructionPlaceholder"));
5586
+ instruction.addEventListener("input", onChange);
5587
+ panel.appendChild(instruction);
5588
+ const typed = () => instruction.value.trim();
5589
+ return {
5590
+ el: panel,
5591
+ dismiss,
5592
+ onSettle: (cb) => {
5593
+ settle = cb;
5594
+ },
5595
+ getContent: () => ({ instruction: typed() }),
5596
+ isComplete: () => typed().length > 0,
5597
+ focus: () => {
5598
+ panel.querySelector(".aparte-approval-option")?.focus();
5599
+ },
5600
+ relabel: () => {
5601
+ const now = contextConfig();
5602
+ const label = now.t("approvalInstructionPlaceholder");
5603
+ instruction.placeholder = label;
5604
+ instruction.setAttribute("aria-label", label);
5605
+ row.setAttribute("aria-label", now.t("approvalOptionsLabel"));
5606
+ if (ask) ask.textContent = askText();
5607
+ for (const { button, option } of labelled) button.textContent = textOf(option);
5608
+ }
5609
+ };
5610
+ }
5611
+ function requestUserInput(request) {
5612
+ return resolveConfig(request.target ?? null).requestUserInput(request);
5613
+ }
5614
+ const DEFAULT_TOOL_HANDLER_TIMEOUT_MS = 5 * 60 * 1e3;
5615
+ function withToolTimeout(work, controller, timeoutMs) {
5616
+ let timer;
5617
+ const timedOut = new Promise((_resolve, reject) => {
5618
+ timer = setTimeout(() => {
5619
+ controller.abort();
5620
+ reject(Object.assign(new Error(`tool handler exceeded ${timeoutMs}ms`), { name: "AbortError" }));
5621
+ }, timeoutMs);
5622
+ });
5623
+ return {
5624
+ raced: Promise.race([work(controller.signal), timedOut]),
5625
+ done: () => clearTimeout(timer)
5626
+ };
5627
+ }
5628
+ class AparteClient {
5629
+ _boundHandler = null;
5630
+ _boundAbortHandler = null;
5631
+ _boundCompactHandler = null;
5632
+ _boundRetryHandler = null;
5633
+ _boundEditHandler = null;
5634
+ _activeToolControllers = /* @__PURE__ */ new Set();
5635
+ _isAborted = false;
5636
+ /**
5637
+ * Does this window event belong to this client?
5638
+ *
5639
+ * One rule for all five handlers, because it used to be four near-copies and
5640
+ * one omission: `aparte-compact` had no guard at all, so in the two-client
5641
+ * layout the JSDoc documents, a single compact event made BOTH clients run —
5642
+ * two paid summarisation calls against whichever chat the DOM scan found
5643
+ * first, and a global reset that wiped the other conversation.
5644
+ *
5645
+ * A scoped client also answers only events ADDRESSED to it. The old guard let
5397
5646
  * an untargeted event through to every scoped client, which turned one
5398
5647
  * broadcast into an action on every chat on the page.
5399
5648
  */
@@ -5523,61 +5772,66 @@ class AparteClient {
5523
5772
  }
5524
5773
  this._activeToolControllers.clear();
5525
5774
  }
5526
- /**
5527
- * Human-in-the-loop: wait for an `aparte-tool-decision` event matching this
5528
- * tool call (dispatched by the built-in Approve/Reject UI or an app-level
5529
- * approval surface). Resolves `{ approved, payload }` `approved` is `true`
5530
- * only on an explicit approve, and `payload` carries any arbitrary data a
5531
- * custom approval UI attached to the decision (the built-in gate sends
5532
- * none). The `signal` (an AbortController registered in
5533
- * `_activeToolControllers`) lets `abort()` cleanly resolve a pending
5534
- * approval to `{ approved: false }`there is no timeout, since a human may
5535
- * take any amount of time to decide.
5775
+ /*
5776
+ * Two docblocks stood here, both describing `_awaitToolDecision` the method
5777
+ * this replaced. Deleting a method and leaving its documentation is worse than
5778
+ * leaving the method: only the LAST comment before a declaration is its JSDoc,
5779
+ * so those two were floating prose, and one of them explained a DOM-containment
5780
+ * check as a live security property. It was the fix for a cross-chat hazard that
5781
+ * only existed because the decision travelled as a bubbling `document` event.
5782
+ * Routing through `requestUserInput` removed the hazard rather than guarding it,
5783
+ * so the guard is gone tooand a reader who found that comment would have
5784
+ * believed a containment check was still protecting them.
5536
5785
  */
5537
5786
  /**
5538
- * Await the human's Approve/Reject for one tool call.
5787
+ * Ask the human at the COMPOSER, and report what they decided.
5539
5788
  *
5540
- * `target` is not decoration: it is the scope of the consent. The listener sits
5541
- * on `document` and used to accept any `aparte-tool-decision` whose
5542
- * `detail.toolCallId` matched and that id is the tool-call id the MODEL chose.
5543
- * The built-in buttons dispatch with `bubbles: true, composed: true`, so on a
5544
- * page with two chats a click aimed at one tool could satisfy the gate awaiting
5545
- * a different tool in a different conversation. The consented action and the
5546
- * executed action came apart, which is the entire failure mode an approval gate
5547
- * exists to prevent — and the handler behind it is arbitrary consumer code.
5789
+ * The default channel, replacing a `document` listener for `aparte-tool-decision`
5790
+ * that existed only because the buttons lived in a segment renderer with no
5791
+ * reference to this client. Routing through `requestUserInput` puts the decision
5792
+ * where every other request for the user already goes, which is the whole point:
5793
+ * one queue, one panel slot, one teardown, and the same behaviour whether the
5794
+ * request came from a tool handler or from this gate.
5548
5795
  *
5549
- * The check is DOM CONTAINMENT rather than a `targetId` string comparison. A
5550
- * model can choose an id; it cannot choose where in the tree the click happened.
5551
- * It also needs no change on the dispatch side, so a consumer's own Approve
5552
- * button keeps working as long as it fires from inside its own chat.
5796
+ * The tool NAME is the question; the arguments stay in the transcript, on the pill
5797
+ * this request is anchored to. A panel capped at 50vh cannot hold a diff or a plan,
5798
+ * and the transcript is already scrollable, copyable and persisted.
5553
5799
  *
5554
- * The request half of this handshake was hardened with `targetId` for exactly
5555
- * this hazard. This is its sibling, and it was missed.
5800
+ * `aparte-tool-decision` is GONE with the buttons that dispatched it, and so is the
5801
+ * `document` listener that answered it. It existed only because a segment renderer
5802
+ * has no reference to this client; two seams that can disagree about who answers a
5803
+ * decision are one seam too many, and the ratified elicitation rule already said so
5804
+ * — "a typed presenter registered per config instance, never window events". A host
5805
+ * that wants to answer programmatically registers an `approvalResolver` (headless,
5806
+ * no DOM) or its own presenter, both strictly more capable than an id on an event.
5556
5807
  */
5557
- _awaitToolDecision(toolCallId, signal, target) {
5558
- return new Promise((resolve) => {
5559
- if (signal.aborted) {
5560
- resolve({ approved: false });
5561
- return;
5562
- }
5563
- const cleanup = () => {
5564
- document.removeEventListener("aparte-tool-decision", onDecision);
5565
- signal.removeEventListener("abort", onAbort);
5566
- };
5567
- const onDecision = (e) => {
5568
- const detail = e.detail;
5569
- if (detail?.toolCallId !== toolCallId) return;
5570
- if (target && e.target instanceof Node && e.target !== document && !target.contains(e.target)) return;
5571
- cleanup();
5572
- resolve({ approved: detail?.approved === true, payload: detail?.payload });
5573
- };
5574
- const onAbort = () => {
5575
- cleanup();
5576
- resolve({ approved: false });
5577
- };
5578
- document.addEventListener("aparte-tool-decision", onDecision);
5579
- signal.addEventListener("abort", onAbort, { once: true });
5808
+ async _askForApproval(event, signal, target) {
5809
+ const answer = await requestUserInput({
5810
+ kind: "approval",
5811
+ /*
5812
+ * A function, like the option labels below, and for the same reason: the
5813
+ * FRAME is locale text and follows a language switch while the question is
5814
+ * open. The tool's NAME is substituted into it and never translated — it is
5815
+ * wire format, the identifier the model called.
5816
+ */
5817
+ message: () => {
5818
+ const loc = this._config.getLocale();
5819
+ return (loc.approvalAsk ?? "Run {tool}?").replace("{tool}", event.name);
5820
+ },
5821
+ // Functions, not strings, same as the question: these follow a language
5822
+ // switch while the request is open, which a resolved string cannot.
5823
+ options: [
5824
+ { value: "allow", label: () => this._config.getLocale().approveTool ?? "Approve", tone: "affirm" },
5825
+ { value: "deny", label: () => this._config.getLocale().rejectTool ?? "Reject", tone: "deny" }
5826
+ ],
5827
+ signal,
5828
+ target
5580
5829
  });
5830
+ if (answer.action !== "accept") return { approved: false };
5831
+ const content = answer.content;
5832
+ const instruction = content?.instruction?.trim();
5833
+ if (instruction) return { approved: false, instruction };
5834
+ return { approved: content?.option === "allow" };
5581
5835
  }
5582
5836
  /**
5583
5837
  * Resolve the auth for a provider: `options.keyResolver` takes precedence,
@@ -6236,16 +6490,27 @@ ${content}
6236
6490
  this._activeToolControllers.delete(controller);
6237
6491
  }
6238
6492
  }
6493
+ /**
6494
+ * What the loop does after one tool call. THREE outcomes, because a boolean
6495
+ * conflated two questions that a refusal answers differently.
6496
+ *
6497
+ * - `'continue'` — run the next tool call of this turn.
6498
+ * - `'respond'` — skip this turn's remaining calls, then take another turn so the
6499
+ * model reads what happened. A human refusal: the model asked for
6500
+ * several calls and refusing one cannot license the others, but a
6501
+ * refusal it never reads is one the user has to retype as a message.
6502
+ * - `'halt'` — the run is over: an abort, a turn limit, a missing handler.
6503
+ */
6239
6504
  /**
6240
6505
  * Handle one `tool_use` stream event from {@link _streamLoop}: the built-in
6241
6506
  * `create_artifact`, per-tool renderer selection, the human-in-the-loop
6242
6507
  * approval gate, and running the registered handler (timeout / abort).
6243
6508
  * Mutates the shared `messages` / `toolCallsThisTurn` history in place and
6244
- * returns whether the agentic loop should keep going.
6509
+ * returns what the loop should do next.
6245
6510
  */
6246
6511
  async _handleToolUseEvent(event, ctx) {
6247
6512
  const { targetElement, messageId, messages, toolCallsThisTurn, precedingText, artifactProgress, turns, globalMaxTurns } = ctx;
6248
- let continueLoop = true;
6513
+ let flow = "continue";
6249
6514
  toolCallsThisTurn.push({ id: event.id, name: event.name, input: event.input });
6250
6515
  if (event.name === "create_artifact") {
6251
6516
  const input = event.input;
@@ -6271,7 +6536,7 @@ ${content}
6271
6536
  content: "Artifact created successfully.",
6272
6537
  toolCallId: event.id
6273
6538
  });
6274
- return { continueLoop: true };
6539
+ return { flow: "continue" };
6275
6540
  }
6276
6541
  const toolSeg = {
6277
6542
  id: `tool-${event.id}`,
@@ -6305,7 +6570,7 @@ ${content}
6305
6570
  if (turns >= effectiveMaxTurns) {
6306
6571
  console.warn(`[AparteClient] Tool "${event.name}" maxTurns (${effectiveMaxTurns}) reached.`);
6307
6572
  targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
6308
- return { continueLoop: false };
6573
+ return { flow: "halt" };
6309
6574
  }
6310
6575
  const handler = this._config.getToolHandler(event.name);
6311
6576
  if (handler) {
@@ -6320,14 +6585,24 @@ ${content}
6320
6585
  input: event.input
6321
6586
  });
6322
6587
  let decision;
6323
- const resolveApproval = this.options.approvalResolver ?? ((id, sig) => this._awaitToolDecision(id, sig, targetElement));
6588
+ const resolveApproval = this.options.approvalResolver ?? ((call, sig) => this._askForApproval(call, sig, targetElement));
6324
6589
  try {
6325
- decision = await resolveApproval(event.id, approvalController.signal);
6590
+ decision = await resolveApproval(event, approvalController.signal);
6591
+ } catch (err) {
6592
+ if (err?.name === "AbortError") {
6593
+ targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
6594
+ return { flow: "halt" };
6595
+ }
6596
+ throw err;
6326
6597
  } finally {
6327
6598
  this._activeToolControllers.delete(approvalController);
6328
6599
  }
6600
+ if (approvalController.signal.aborted) {
6601
+ targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
6602
+ return { flow: "halt" };
6603
+ }
6329
6604
  if (!decision.approved) {
6330
- const rejection = "Tool execution was rejected by the user.";
6605
+ const rejection = decision.instruction ? `The user rejected this tool call and said: ${decision.instruction}` : "Tool execution was rejected by the user.";
6331
6606
  targetElement.updateSegment?.(toolSeg.id, { status: "rejected", result: rejection });
6332
6607
  const existingToolCallMsg = messages.find(
6333
6608
  (m) => m.role === "tool_call" && m.toolCalls?.some((tc) => tc.id === event.id)
@@ -6341,7 +6616,7 @@ ${content}
6341
6616
  });
6342
6617
  }
6343
6618
  messages.push({ role: "tool_result", content: rejection, toolCallId: event.id });
6344
- return { continueLoop: false };
6619
+ return { flow: "respond" };
6345
6620
  }
6346
6621
  if (decision.payload && typeof decision.payload === "object" && !Array.isArray(decision.payload)) {
6347
6622
  effectiveInput = { ...event.input, ...decision.payload };
@@ -6381,7 +6656,7 @@ ${content}
6381
6656
  } catch (err) {
6382
6657
  if (err?.name === "AbortError") {
6383
6658
  targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
6384
- continueLoop = false;
6659
+ flow = "halt";
6385
6660
  } else {
6386
6661
  throw err;
6387
6662
  }
@@ -6392,9 +6667,9 @@ ${content}
6392
6667
  } else {
6393
6668
  console.warn(`[AparteClient] No handler registered for tool "${event.name}"`);
6394
6669
  targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
6395
- continueLoop = false;
6670
+ flow = "halt";
6396
6671
  }
6397
- return { continueLoop };
6672
+ return { flow };
6398
6673
  }
6399
6674
  /**
6400
6675
  * Stream loop: runs one provider.chat() call and repeats if a tool was called.
@@ -6419,6 +6694,7 @@ ${content}
6419
6694
  targetElement.addSegment?.(seg);
6420
6695
  }
6421
6696
  while (continueLoop) {
6697
+ let endTurnEarly = false;
6422
6698
  if (this._isAborted) {
6423
6699
  dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
6424
6700
  break;
@@ -6624,7 +6900,8 @@ ${content}
6624
6900
  { id: event.id, name: event.name, input: event.input },
6625
6901
  { targetElement, messageId, messages, toolCallsThisTurn, precedingText, artifactProgress, turns, globalMaxTurns }
6626
6902
  );
6627
- if (!toolResult.continueLoop) continueLoop = false;
6903
+ if (toolResult.flow !== "continue") endTurnEarly = true;
6904
+ if (toolResult.flow === "halt") continueLoop = false;
6628
6905
  break;
6629
6906
  }
6630
6907
  case "error":
@@ -6639,7 +6916,7 @@ ${content}
6639
6916
  break;
6640
6917
  }
6641
6918
  }
6642
- if (!continueLoop) break;
6919
+ if (endTurnEarly) break;
6643
6920
  }
6644
6921
  finalizeXmlArtifact(xmlCtx, { targetElement, messageId, artifactProgress, artifactXmlHint, textParser, streamingSegmentIds });
6645
6922
  const finals = textParser.finalize();
@@ -6754,7 +7031,7 @@ ${content}
6754
7031
  const tool = this._config.getTools().find((t) => t.name === name);
6755
7032
  return tool ? { maxTurns: tool.maxTurns, needsApproval: tool.needsApproval } : void 0;
6756
7033
  };
6757
- const approvalResolver = this.options.approvalResolver ?? ((id, sig) => this._awaitToolDecision(id, sig, targetElement));
7034
+ const approvalResolver = this.options.approvalResolver ?? ((call, sig) => this._askForApproval(call, sig, targetElement));
6758
7035
  const usage = await streamRunner({
6759
7036
  messageId,
6760
7037
  baseRequest,
@@ -7060,22 +7337,47 @@ class AparteMessageRepository {
7060
7337
  }
7061
7338
  }
7062
7339
  const APARTE_DEFAULT_UI_EVENTS = [
7340
+ // <aparte-composer> and its parts
7063
7341
  "aparte-send",
7342
+ "aparte-cancel",
7343
+ "aparte-composer-change",
7344
+ "aparte-composer-submit",
7345
+ "aparte-action-click",
7346
+ "aparte-attachment-preview",
7347
+ // <aparte-chat-bubble>
7064
7348
  "aparte-action",
7065
7349
  "aparte-retry",
7066
7350
  "aparte-edit",
7351
+ "aparte-feedback",
7352
+ "aparte-message-info",
7067
7353
  "aparte-branch-navigate",
7068
- "aparte-composer-change",
7069
- "aparte-path-changed"
7354
+ // <aparte-chat-viewport>
7355
+ "aparte-segment-update",
7356
+ "aparte-reset-done",
7357
+ "aparte-path-changed",
7358
+ // <aparte-conversation-list>
7359
+ "aparte-select-conversation",
7360
+ "aparte-delete-conversation",
7361
+ "aparte-archive-conversation",
7362
+ "aparte-unarchive-conversation",
7363
+ // the select primitives
7364
+ "aparte-select-change",
7365
+ "aparte-select-open",
7366
+ "aparte-select-close",
7367
+ "aparte-optgroup-toggle"
7070
7368
  ];
7071
7369
  function applyElementProps(el2, props, transformValue = (value) => value) {
7072
7370
  for (const [key, value] of Object.entries(props)) {
7073
7371
  if (key.startsWith("--")) {
7074
- el2.style.setProperty(key, String(value));
7372
+ if (value === null || value === void 0) el2.style.removeProperty(key);
7373
+ else if (typeof value === "object" || typeof value === "function") el2.style.removeProperty(key);
7374
+ else el2.style.setProperty(key, String(value));
7075
7375
  } else if (key.startsWith("on")) {
7076
7376
  el2.removeAttribute(key);
7077
7377
  } else if (value === null || value === void 0 || value === false) {
7078
7378
  el2.removeAttribute(key);
7379
+ } else if (typeof value === "number" && Number.isNaN(value)) {
7380
+ el2.removeAttribute(key);
7079
7381
  } else if (value === true) {
7080
7382
  el2.setAttribute(key, "");
7081
7383
  } else if (typeof value === "object" || typeof value === "function") {
@@ -7089,361 +7391,51 @@ function isAwaitingReply(message) {
7089
7391
  if (message.status === "streaming" || message.status === "pending") return true;
7090
7392
  return message.status === void 0 && message.role === "assistant" && !message.content?.trim() && !message.segments?.length;
7091
7393
  }
7092
- let _relabels = null;
7093
- function onRelabel(fn) {
7094
- _relabels?.push(fn);
7095
- }
7096
- function el(tag, className, text) {
7097
- const node = document.createElement(tag);
7098
- if (className) node.className = className;
7099
- if (text != null) node.textContent = text;
7100
- return node;
7101
- }
7102
- function fieldHeader(parent, field, skipTitle) {
7103
- let titleId;
7104
- if (field.title && field.title.trim() !== skipTitle?.trim()) {
7105
- const title = el("p", "aparte-elic-title", field.title);
7106
- titleId = `elic-title-${uuid()}`;
7107
- title.id = titleId;
7108
- parent.appendChild(title);
7109
- }
7110
- if (field.description) parent.appendChild(el("p", "aparte-elic-desc", field.description));
7111
- return titleId;
7112
- }
7113
- function labelGroup(list, opts) {
7114
- list.setAttribute("role", opts.multiple ? "group" : "radiogroup");
7115
- if (opts.titleId) list.setAttribute("aria-labelledby", opts.titleId);
7116
- else if (opts.fallbackLabel) list.setAttribute("aria-label", opts.fallbackLabel);
7117
- }
7118
- function buildEnumField(field, onChange, fallbackLabel) {
7119
- const wrap = el("div", "aparte-elic-field aparte-elic-enum");
7120
- const titleId = fieldHeader(wrap, field, fallbackLabel);
7121
- const list = el("div", "aparte-elic-options");
7122
- labelGroup(list, { multiple: field.multiple, titleId, fallbackLabel });
7123
- const name = `elic-${uuid()}`;
7124
- const type = field.multiple ? "checkbox" : "radio";
7125
- const allowOther = field.allowOther ?? contextConfig().getElicitationOptions().allowOther;
7126
- const defaults = new Set(Array.isArray(field.default) ? field.default : field.default != null ? [field.default] : []);
7127
- const buildOption = (value, label, description, recommended) => {
7128
- const row = el("label", "aparte-elic-option" + (recommended ? " aparte-elic-option--recommended" : ""));
7129
- const control = el("input", "aparte-elic-control");
7130
- control.type = type;
7131
- control.name = name;
7132
- control.value = value;
7133
- if (defaults.has(value)) control.checked = true;
7134
- const body = el("span", "aparte-elic-option-body");
7135
- body.appendChild(el("span", "aparte-elic-option-title", label));
7136
- if (description) body.appendChild(el("span", "aparte-elic-option-desc", description));
7137
- row.append(control, body);
7138
- return row;
7139
- };
7140
- for (const opt of field.options) {
7141
- list.appendChild(buildOption(opt.value, opt.label ?? opt.value, opt.description, opt.recommended));
7142
- }
7143
- let otherText = null;
7144
- if (allowOther) {
7145
- const row = el("label", "aparte-elic-option aparte-elic-option--other");
7146
- const control = el("input", "aparte-elic-control");
7147
- control.type = type;
7148
- control.name = name;
7149
- control.value = "__other__";
7150
- const body = el("span", "aparte-elic-option-body");
7151
- const t = contextConfig();
7152
- const otherTitle = el("span", "aparte-elic-option-title", t.t("elicitationOther"));
7153
- body.appendChild(otherTitle);
7154
- otherText = el("input", "aparte-elic-other-input");
7155
- otherText.type = "text";
7156
- otherText.placeholder = t.t("elicitationOtherPlaceholder");
7157
- otherText.style.display = "none";
7158
- otherText.setAttribute("aria-label", t.t("elicitationOtherLabel"));
7159
- const otherInput = otherText;
7160
- onRelabel(() => {
7161
- const now = contextConfig();
7162
- otherTitle.textContent = now.t("elicitationOther");
7163
- otherInput.placeholder = now.t("elicitationOtherPlaceholder");
7164
- otherInput.setAttribute("aria-label", now.t("elicitationOtherLabel"));
7165
- });
7166
- body.appendChild(otherText);
7167
- row.append(control, body);
7168
- list.appendChild(row);
7169
- }
7170
- wrap.appendChild(list);
7171
- const controls = () => Array.from(list.querySelectorAll(".aparte-elic-control"));
7172
- const other = () => list.querySelector('input[value="__other__"]');
7173
- list.addEventListener("change", () => {
7174
- const o = other();
7175
- if (o && otherText) {
7176
- otherText.style.display = o.checked ? "" : "none";
7177
- if (o.checked) otherText.focus();
7178
- }
7179
- onChange();
7180
- });
7181
- otherText?.addEventListener("input", onChange);
7182
- const getValue = () => {
7183
- const otherVal = other()?.checked && otherText?.value.trim() ? otherText.value.trim() : "";
7184
- if (field.multiple) {
7185
- const vals = controls().filter((c) => c.checked && c.value !== "__other__").map((c) => c.value);
7186
- if (otherVal) vals.push(otherVal);
7187
- return vals;
7188
- }
7189
- const checked = controls().find((c) => c.checked);
7190
- if (!checked) return "";
7191
- return checked.value === "__other__" ? otherVal : checked.value;
7192
- };
7193
- return {
7194
- el: wrap,
7195
- getValue,
7196
- isComplete: () => {
7197
- const v = getValue();
7198
- return field.multiple ? v.length > 0 : v !== "";
7199
- },
7200
- focus: () => controls()[0]?.focus()
7201
- };
7202
- }
7203
- function buildBooleanField(field, onChange, fallbackLabel) {
7204
- const wrap = el("div", "aparte-elic-field aparte-elic-boolean");
7205
- const titleId = fieldHeader(wrap, field, fallbackLabel);
7206
- const list = el("div", "aparte-elic-options");
7207
- labelGroup(list, { titleId, fallbackLabel });
7208
- const name = `elic-${uuid()}`;
7209
- const mk = (val, label, defaulted) => {
7210
- const row = el("label", "aparte-elic-option");
7211
- const control = el("input", "aparte-elic-control");
7212
- control.type = "radio";
7213
- control.name = name;
7214
- control.value = val;
7215
- if (field.default != null && String(field.default) === val) control.checked = true;
7216
- const body = el("span", "aparte-elic-option-body");
7217
- const title = el("span", "aparte-elic-option-title", label);
7218
- if (defaulted) onRelabel(() => {
7219
- title.textContent = contextConfig().t(defaulted);
7220
- });
7221
- body.appendChild(title);
7222
- row.append(control, body);
7223
- list.appendChild(row);
7224
- };
7225
- const t = contextConfig();
7226
- mk("true", field.trueLabel ?? t.t("elicitationYes"), field.trueLabel ? void 0 : "elicitationYes");
7227
- mk("false", field.falseLabel ?? t.t("elicitationNo"), field.falseLabel ? void 0 : "elicitationNo");
7228
- wrap.appendChild(list);
7229
- list.addEventListener("change", onChange);
7230
- const checked = () => list.querySelector("input:checked");
7231
- return {
7232
- el: wrap,
7233
- getValue: () => checked()?.value === "true",
7234
- isComplete: () => checked() != null,
7235
- focus: () => list.querySelector("input")?.focus()
7236
- };
7237
- }
7238
- function buildStringField(field, onChange, fallbackLabel) {
7239
- const wrap = el("div", "aparte-elic-field aparte-elic-string");
7240
- fieldHeader(wrap, field, fallbackLabel);
7241
- const input = field.multiline ? el("textarea", "aparte-elic-text") : el("input", "aparte-elic-text");
7242
- if (!field.multiline) input.type = "text";
7243
- if (field.placeholder) input.setAttribute("placeholder", field.placeholder);
7244
- if (field.default) input.value = field.default;
7245
- if (field.maxLength != null) input.setAttribute("maxlength", String(field.maxLength));
7246
- const ownName = field.title || field.description || fallbackLabel || void 0;
7247
- input.setAttribute("aria-label", ownName ?? contextConfig().t("elicitationAnswerLabel"));
7248
- if (ownName == null) {
7249
- const named = input;
7250
- onRelabel(() => {
7251
- named.setAttribute("aria-label", contextConfig().t("elicitationAnswerLabel"));
7252
- });
7253
- }
7254
- wrap.appendChild(input);
7255
- input.addEventListener("input", onChange);
7256
- const required = field.required ?? true;
7257
- const value = () => input.value;
7258
- return {
7259
- el: wrap,
7260
- getValue: () => value(),
7261
- isComplete: () => {
7262
- const v = value().trim();
7263
- if (!required && v === "") return true;
7264
- return v.length >= (field.minLength ?? 1);
7265
- },
7266
- focus: () => input.focus()
7267
- };
7268
- }
7269
- function buildField(field, onChange, fallbackLabel, key) {
7270
- const custom = contextConfig().getElicitationFieldRenderer()?.(field, { key, notifyChange: onChange });
7271
- if (custom) {
7272
- return {
7273
- el: custom.el,
7274
- getValue: () => custom.getValue(),
7275
- isComplete: () => custom.isComplete(),
7276
- focus: () => custom.focus?.()
7277
- };
7278
- }
7279
- switch (field.type) {
7280
- case "enum":
7281
- return buildEnumField(field, onChange, fallbackLabel);
7282
- case "boolean":
7283
- return buildBooleanField(field, onChange, fallbackLabel);
7284
- case "string":
7285
- return buildStringField(field, onChange, fallbackLabel);
7286
- }
7287
- }
7288
- function buildElicitationPanel(message, schema, onChange) {
7289
- const relabels = [];
7290
- const outer = _relabels;
7291
- _relabels = relabels;
7292
- try {
7293
- const built = buildPanel(message, schema, onChange);
7294
- return { ...built, relabel: () => {
7295
- for (const fn of relabels) fn();
7296
- } };
7297
- } finally {
7298
- _relabels = outer;
7299
- }
7300
- }
7301
- function buildPanel(message, schema, onChange) {
7302
- const panel = el("div", "aparte-elic-panel");
7303
- if (message) panel.appendChild(el("p", "aparte-elic-message", message));
7304
- const body = el("div", "aparte-elic-body");
7305
- panel.appendChild(body);
7306
- const dismiss = el("div", "aparte-elic-dismiss");
7307
- panel.appendChild(dismiss);
7308
- if (schema.type === "object") {
7309
- let show = function(index) {
7310
- current = index;
7311
- fields.forEach((f, i) => {
7312
- f.field.el.hidden = i !== index;
7313
- });
7314
- syncNav();
7315
- };
7316
- const entries = Object.entries(schema.properties);
7317
- const requiredKeys = new Set(schema.required ?? entries.map(([k]) => k));
7318
- const cfg = contextConfig();
7319
- let syncNav = () => {
7320
- };
7321
- const notify = () => {
7322
- syncNav();
7323
- onChange();
7324
- };
7325
- const fields = entries.map(([key, field2]) => {
7326
- const built = buildField(field2, notify, void 0, key);
7327
- if (!field2.title && !built.el.querySelector(".aparte-elic-title")) {
7328
- built.el.insertBefore(el("p", "aparte-elic-title", key), built.el.firstChild);
7329
- }
7330
- body.appendChild(built.el);
7331
- return { key, field: built, required: requiredKeys.has(key), header: field2.header };
7332
- });
7333
- const isComplete = () => fields.every((f) => !f.required || f.field.isComplete());
7334
- const api = {
7335
- el: panel,
7336
- dismiss,
7337
- getContent: () => Object.fromEntries(fields.map((f) => [f.key, f.field.getValue()])),
7338
- isComplete,
7339
- focus: () => fields[0]?.field.focus(),
7340
- // A form of one behaves like a single field; only a real form steps.
7341
- mode: () => "submit",
7342
- canProceed: isComplete,
7343
- proceed: () => {
7344
- }
7345
- };
7346
- if (fields.length < 2 || cfg.getElicitationOptions().layout !== "stepped") return api;
7347
- panel.classList.add("aparte-elic-panel--stepped");
7348
- let current = 0;
7349
- const steps = el("div", "aparte-elic-steps");
7350
- steps.setAttribute("role", "tablist");
7351
- const chips = fields.map((f, i) => {
7352
- const chip = el("button", "aparte-elic-step");
7353
- chip.type = "button";
7354
- chip.setAttribute("role", "tab");
7355
- chip.textContent = f.header?.trim() || String(i + 1);
7356
- chip.addEventListener("click", () => {
7357
- show(i);
7358
- f.field.focus();
7359
- });
7360
- steps.appendChild(chip);
7361
- return chip;
7362
- });
7363
- panel.insertBefore(steps, body);
7364
- syncNav = () => {
7365
- chips.forEach((chip, i) => {
7366
- chip.setAttribute("aria-selected", String(i === current));
7367
- chip.toggleAttribute("data-answered", fields[i].field.isComplete());
7368
- });
7369
- };
7370
- show(0);
7371
- const last = () => current === fields.length - 1;
7372
- return {
7373
- ...api,
7374
- focus: () => fields[current]?.field.focus(),
7375
- mode: () => last() ? "submit" : "advance",
7376
- // Advancing needs only THIS question answered; submitting needs them all,
7377
- // which is what stops the last question from accepting a form with a hole
7378
- // in it.
7379
- canProceed: () => last() ? isComplete() : !!fields[current]?.field.isComplete(),
7380
- proceed: () => {
7381
- if (last()) return;
7382
- show(current + 1);
7383
- fields[current]?.field.focus();
7384
- }
7385
- };
7386
- }
7387
- const field = buildField(schema, onChange, message);
7388
- body.appendChild(field.el);
7389
- return {
7390
- el: panel,
7391
- dismiss,
7392
- getContent: () => field.getValue(),
7393
- isComplete: () => field.isComplete(),
7394
- focus: () => field.focus(),
7395
- mode: () => "submit",
7396
- canProceed: () => field.isComplete(),
7397
- proceed: () => {
7398
- }
7399
- };
7400
- }
7401
- function requestUserInput(request) {
7402
- return resolveConfig(request.target ?? null).requestUserInput(request);
7403
- }
7404
7394
  export {
7405
- runWithConfig as $,
7395
+ resolveConfig as $,
7406
7396
  APARTE_CONFIG_CHANGE as A,
7407
- createAparteChatHandler as B,
7408
- createStreamAdapter as C,
7409
- cssEscape as D,
7410
- declineDefaultRenderers as E,
7411
- defaultSanitizer as F,
7412
- deriveArtifactKind as G,
7413
- detachConfig as H,
7414
- escapeAttr as I,
7415
- escapeHtml as J,
7416
- filesToAttachments as K,
7417
- getAllRenderers as L,
7418
- getSegmentRenderer as M,
7419
- installDefaultRenderersOnce as N,
7420
- isAwaitingReply as O,
7421
- isFormatAdapter as P,
7422
- isSafeUrl as Q,
7423
- isSegmentSettled as R,
7424
- parseAparteEventStream as S,
7425
- parseMarkdownToSegments as T,
7426
- populateBubbleFromMessage as U,
7427
- readableToAsyncIterable as V,
7428
- registerDefaultRenderers as W,
7429
- registerSegmentRenderer as X,
7430
- requestUserInput as Y,
7431
- resolveConfig as Z,
7432
- revokeAttachmentUrls as _,
7397
+ contentToText as B,
7398
+ contextConfig as C,
7399
+ createAparteChatHandler as D,
7400
+ createStreamAdapter as E,
7401
+ cssEscape as F,
7402
+ declineDefaultRenderers as G,
7403
+ defaultSanitizer as H,
7404
+ deriveArtifactKind as I,
7405
+ detachConfig as J,
7406
+ escapeAttr as K,
7407
+ escapeHtml as L,
7408
+ filesToAttachments as M,
7409
+ getAllRenderers as N,
7410
+ getSegmentRenderer as O,
7411
+ installDefaultRenderersOnce as P,
7412
+ isAwaitingReply as Q,
7413
+ isFormatAdapter as R,
7414
+ isSafeUrl as S,
7415
+ isSegmentSettled as T,
7416
+ parseAparteEventStream as U,
7417
+ parseMarkdownToSegments as V,
7418
+ populateBubbleFromMessage as W,
7419
+ readableToAsyncIterable as X,
7420
+ registerDefaultRenderers as Y,
7421
+ registerSegmentRenderer as Z,
7422
+ requestUserInput as _,
7433
7423
  APARTE_CONVERSATION_SCHEMA_VERSION as a,
7434
- segmentDuration as a0,
7435
- segmentTiming as a1,
7436
- subscribeConfigChange as a2,
7437
- unregisterSegmentRenderer as a3,
7438
- uuid as a4,
7439
- mergeSegmentUpdate as a5,
7440
- stampSegmentActivity as a6,
7441
- stampSegmentOnInsert as a7,
7442
- stampSegmentOnUpdate as a8,
7443
- renumberSegments as a9,
7444
- openSegmentIds as aa,
7445
- isTerminalStatus as ab,
7446
- adoptMessageSegments as ac,
7424
+ revokeAttachmentUrls as a0,
7425
+ runWithConfig as a1,
7426
+ segmentDuration as a2,
7427
+ segmentTiming as a3,
7428
+ subscribeConfigChange as a4,
7429
+ unregisterSegmentRenderer as a5,
7430
+ uuid as a6,
7431
+ mergeSegmentUpdate as a7,
7432
+ stampSegmentActivity as a8,
7433
+ stampSegmentOnInsert as a9,
7434
+ stampSegmentOnUpdate as aa,
7435
+ renumberSegments as ab,
7436
+ openSegmentIds as ac,
7437
+ isTerminalStatus as ad,
7438
+ adoptMessageSegments as ae,
7447
7439
  APARTE_DEFAULT_BUBBLE_ACTIONS as b,
7448
7440
  APARTE_DEFAULT_HOST_HANDLERS as c,
7449
7441
  APARTE_DEFAULT_ICON_FALLBACKS as d,
@@ -7458,16 +7450,16 @@ export {
7458
7450
  AparteConversationController as m,
7459
7451
  AparteConversationManager as n,
7460
7452
  AparteDirectTransport as o,
7461
- AparteError as p,
7462
- AparteErrorCode as q,
7463
- AparteMessageRepository as r,
7464
- AparteStreamParser as s,
7465
- aparteGlobalConfig as t,
7466
- applyElementProps as u,
7467
- attachConfig as v,
7468
- buildElicitationPanel as w,
7469
- collectRendererStyles as x,
7470
- contentToText as y,
7471
- contextConfig as z
7453
+ AparteElicitationAbortError as p,
7454
+ AparteError as q,
7455
+ AparteErrorCode as r,
7456
+ AparteMessageRepository as s,
7457
+ AparteStreamParser as t,
7458
+ aparteGlobalConfig as u,
7459
+ applyElementProps as v,
7460
+ attachConfig as w,
7461
+ buildApprovalPanel as x,
7462
+ buildElicitationPanel as y,
7463
+ collectRendererStyles as z
7472
7464
  };
7473
- //# sourceMappingURL=index-BnKBciDG.js.map
7465
+ //# sourceMappingURL=is-awaiting-reply-CxWRISBC.js.map