@aparte/core 0.16.11 → 0.17.1

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 (86) hide show
  1. package/README.md +11 -4
  2. package/dist/client/aparte-client.d.ts +42 -3
  3. package/dist/client/aparte-client.d.ts.map +1 -1
  4. package/dist/components/bubble/aparte-chat-bubble.d.ts +22 -0
  5. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  6. package/dist/components/chat/aparte-chat.d.ts +55 -14
  7. package/dist/components/chat/aparte-chat.d.ts.map +1 -1
  8. package/dist/components/composer/aparte-composer-action.d.ts +5 -3
  9. package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
  10. package/dist/components/composer/aparte-composer-toolbar.d.ts +5 -2
  11. package/dist/components/composer/aparte-composer-toolbar.d.ts.map +1 -1
  12. package/dist/components/composer/aparte-composer.d.ts +27 -0
  13. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  14. package/dist/components/context/aparte-context.d.ts +8 -0
  15. package/dist/components/context/aparte-context.d.ts.map +1 -1
  16. package/dist/components/conversation-list/aparte-conversation-list.d.ts +82 -17
  17. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  18. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -1
  19. package/dist/components/sidebar/aparte-sidebar.d.ts.map +1 -1
  20. package/dist/components/split/aparte-split.d.ts.map +1 -1
  21. package/dist/components/suggestions/aparte-suggestions.d.ts +37 -3
  22. package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -1
  23. package/dist/components/viewport/aparte-chat-viewport.d.ts +55 -1
  24. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  25. package/dist/config/action-provider.d.ts +14 -14
  26. package/dist/config/action-provider.d.ts.map +1 -1
  27. package/dist/config/aparte-config.d.ts +109 -14
  28. package/dist/config/aparte-config.d.ts.map +1 -1
  29. package/dist/config/bubble-shell-renderer.d.ts +50 -12
  30. package/dist/config/bubble-shell-renderer.d.ts.map +1 -1
  31. package/dist/config/locale.d.ts +33 -1
  32. package/dist/config/locale.d.ts.map +1 -1
  33. package/dist/config/sanitize.d.ts.map +1 -1
  34. package/dist/conversations/conversation-controller.d.ts +14 -0
  35. package/dist/conversations/conversation-controller.d.ts.map +1 -1
  36. package/dist/conversations/conversation-manager.d.ts +57 -19
  37. package/dist/conversations/conversation-manager.d.ts.map +1 -1
  38. package/dist/conversations/types.d.ts +18 -11
  39. package/dist/conversations/types.d.ts.map +1 -1
  40. package/dist/custom-elements.json +12007 -11027
  41. package/dist/{dialog-triggers-cYGTlfZf.js → dialog-triggers-CJWw0P3l.js} +652 -161
  42. package/dist/dialog-triggers-CJWw0P3l.js.map +1 -0
  43. package/dist/generated/element-attributes.d.ts +15 -9
  44. package/dist/generated/element-attributes.d.ts.map +1 -1
  45. package/dist/host/aparte-chat-host.d.ts +30 -0
  46. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  47. package/dist/index.css +1 -8393
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +644 -111
  51. package/dist/index.js.map +1 -1
  52. package/dist/index.node.d.ts +1 -1
  53. package/dist/index.node.d.ts.map +1 -1
  54. package/dist/index.node.js +1 -1
  55. package/dist/index.node.js.map +1 -1
  56. package/dist/interop/element-props.d.ts +1 -1
  57. package/dist/primitives/select/aparte-optgroup.d.ts +1 -0
  58. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  59. package/dist/primitives/select/aparte-select.d.ts +12 -1
  60. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  61. package/dist/renderers/markdown-stream.d.ts.map +1 -1
  62. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  63. package/dist/runtime/message-repository.d.ts +16 -3
  64. package/dist/runtime/message-repository.d.ts.map +1 -1
  65. package/dist/types/chat.d.ts +41 -46
  66. package/dist/types/chat.d.ts.map +1 -1
  67. package/dist/types/event-map.d.ts +7 -7
  68. package/dist/types/event-map.d.ts.map +1 -1
  69. package/dist/types/events.d.ts +7 -6
  70. package/dist/types/events.d.ts.map +1 -1
  71. package/dist/types/imperative-api.d.ts +16 -0
  72. package/dist/types/imperative-api.d.ts.map +1 -1
  73. package/dist/types/index.d.ts +3 -3
  74. package/dist/types/index.d.ts.map +1 -1
  75. package/dist/types/models.d.ts +9 -0
  76. package/dist/types/models.d.ts.map +1 -1
  77. package/dist/types/segments.d.ts +56 -13
  78. package/dist/types/segments.d.ts.map +1 -1
  79. package/dist/types/tools.d.ts +14 -0
  80. package/dist/types/tools.d.ts.map +1 -1
  81. package/dist/utils/event-target.d.ts +31 -0
  82. package/dist/utils/event-target.d.ts.map +1 -0
  83. package/dist/utils/transcript.d.ts +13 -0
  84. package/dist/utils/transcript.d.ts.map +1 -0
  85. package/package.json +2 -2
  86. package/dist/dialog-triggers-cYGTlfZf.js.map +0 -1
@@ -147,6 +147,8 @@ const APARTE_DEFAULT_LOCALE = {
147
147
  rejectTool: "Reject",
148
148
  messageInfo: "Details",
149
149
  newChat: "New Chat",
150
+ loadingConversations: "Loading conversations",
151
+ loadingConversation: "Loading the conversation",
150
152
  deleteConversation: "Delete",
151
153
  archiveConversation: "Archive",
152
154
  unarchiveConversation: "Unarchive",
@@ -165,8 +167,7 @@ const APARTE_DEFAULT_LOCALE = {
165
167
  scrollRailLabel: "Conversation outline",
166
168
  sidebarLabel: "Conversations",
167
169
  splitHandleLabel: "Resize the panes",
168
- transcript: "Transcript",
169
- direction: "ltr"
170
+ transcript: "Transcript"
170
171
  };
171
172
  function isFormatAdapter(p) {
172
173
  return typeof p.buildRequest === "function" && typeof p.parseStream === "function" && typeof p.defaultEndpoint === "string" && (typeof p.authHeaders === "function" || typeof p.authQuery === "function");
@@ -982,7 +983,7 @@ const ALLOWED_TAGS = /* @__PURE__ */ new Set([
982
983
  "picture",
983
984
  "source",
984
985
  "input",
985
- // GFM task-list checkboxes only (attributes locked down below)
986
+ // GFM task-list checkboxes only — `copyAttributes` refuses every other `type`
986
987
  "abbr",
987
988
  "details",
988
989
  "summary",
@@ -1086,6 +1087,7 @@ function copyAttributes(src, dest, tag) {
1086
1087
  if (name.startsWith("on") || name.startsWith("formaction")) continue;
1087
1088
  if (!name.startsWith("data-") && !GLOBAL_ATTRS.has(name) && !(extra && extra.has(name))) continue;
1088
1089
  if (URL_ATTRS.has(name) && !isSafeUrl(value, tag)) continue;
1090
+ if (tag === "input" && name === "type" && value.toLowerCase() !== "checkbox") continue;
1089
1091
  if (name === "srcset" && !isSafeSrcset(value, tag)) continue;
1090
1092
  if (name === "style") {
1091
1093
  const scrubbed = scrubStyle(value);
@@ -1233,6 +1235,11 @@ class AparteConfig {
1233
1235
  _iconProvider;
1234
1236
  _avatarProvider;
1235
1237
  _keyProvider;
1238
+ /**
1239
+ * Sources added with `registerKeyProvider` — consulted before `_keyProvider`,
1240
+ * newest first (see `getKey`).
1241
+ */
1242
+ _extraKeyProviders = /* @__PURE__ */ new Set();
1236
1243
  _locale = APARTE_DEFAULT_LOCALE;
1237
1244
  _actions = [];
1238
1245
  _listeners = /* @__PURE__ */ new Set();
@@ -1307,10 +1314,11 @@ class AparteConfig {
1307
1314
  * (soon) the tool-approval gate. A custom presenter registered by a consumer gets
1308
1315
  * the same protection for free, which it previously had none of.
1309
1316
  */
1310
- _elicitationQueue = null;
1317
+ _elicitationQueue = /* @__PURE__ */ new Map();
1311
1318
  // Tool Registry
1312
1319
  _tools = /* @__PURE__ */ new Map();
1313
1320
  _toolRenderers = /* @__PURE__ */ new Map();
1321
+ _toolSummaries = /* @__PURE__ */ new Map();
1314
1322
  /** Tagged blocks the stream parser recognises in the prose — see {@link registerStreamBlock}. */
1315
1323
  _streamBlocks = /* @__PURE__ */ new Map();
1316
1324
  _segmentDefaults = /* @__PURE__ */ new Map();
@@ -1323,9 +1331,10 @@ class AparteConfig {
1323
1331
  // Provider Setters (Dependency Injection)
1324
1332
  // ─────────────────────────────────────────────────────────────────────────
1325
1333
  /**
1326
- * Register a custom action button. `zones` places it in the composer toolbar
1327
- * and/or the message (bubble) toolbar. Re-registering the same id overwrites
1328
- * it. Notifies mounted elements so they re-render.
1334
+ * Register a custom action button. `zones` places it in the message (bubble)
1335
+ * toolbar. Re-registering the same id overwrites it. Notifies mounted elements
1336
+ * so they re-render. A button in the composer is an `<aparte-composer-action>`
1337
+ * element you write in your markup, not a registry entry.
1329
1338
  */
1330
1339
  registerAction(action) {
1331
1340
  const existing = this._actions.findIndex((a) => a.id === action.id);
@@ -1347,17 +1356,6 @@ class AparteConfig {
1347
1356
  this._actions = this._actions.filter((a) => a.id !== id);
1348
1357
  if (this._actions.length !== before) this._notify();
1349
1358
  }
1350
- /**
1351
- * Show or hide a composer action button by id.
1352
- * Triggers a config update so all mounted composer elements react immediately.
1353
- */
1354
- setActionHidden(id, hidden) {
1355
- const action = this._actions.find((a) => a.id === id);
1356
- if (action) {
1357
- action.composer = { ...action.composer, hidden };
1358
- this._notify();
1359
- }
1360
- }
1361
1359
  /**
1362
1360
  * Configure which action buttons appear in message bubbles.
1363
1361
  * Unset keys keep their defaults — see {@link APARTE_DEFAULT_BUBBLE_ACTIONS}: `copy`
@@ -1418,8 +1416,11 @@ class AparteConfig {
1418
1416
  * Set an incremental (streaming) Markdown renderer provider. Optional —
1419
1417
  * when set, the chat bubble uses it to render the assistant message
1420
1418
  * token-by-token DURING streaming (incremental parse + DOM append, O(n)),
1421
- * instead of re-parsing the whole string on every token. The one-shot
1422
- * `setMarkdownProvider` is still used for finished / re-rendered messages.
1419
+ * instead of re-parsing the whole string on every token. When the turn ends,
1420
+ * a registered one-shot `setMarkdownProvider` re-renders the finished message
1421
+ * at full fidelity; with no one-shot provider registered, what the streaming
1422
+ * renderer drew is kept (sanitised), rather than replaced by the built-in
1423
+ * escape-and-line-break fallback.
1423
1424
  */
1424
1425
  setStreamingMarkdownProvider(fn) {
1425
1426
  this._streamingMarkdownProvider = fn;
@@ -1676,11 +1677,57 @@ class AparteConfig {
1676
1677
  this._keyProvider = provider;
1677
1678
  }
1678
1679
  /**
1679
- * Get API key for a provider
1680
+ * Add a key source, without replacing the one `setKeyProvider` holds. Returns a
1681
+ * teardown.
1682
+ *
1683
+ * This is how `AparteClient({ keyResolver })` — the channel the docs teach as
1684
+ * primary — reaches everything that needs a key rather than the chat alone. It
1685
+ * used to reach the chat ONLY, so the model selector, whose one data path is
1686
+ * `refreshProviderModels`, showed an empty list on every cloud provider while
1687
+ * the chat worked: `fetchModels` returns `[]` without a key, so there was
1688
+ * nothing to see and nothing said.
1689
+ *
1690
+ * The direction matters (decision #9): the client registers a source ON the
1691
+ * config, so the capability is never hostage to the client — a page that only
1692
+ * ever calls `setKeyProvider` is unchanged, and a page that constructs no
1693
+ * client at all still lists models.
1694
+ *
1695
+ * Every call is its own source, even when the same function is handed in
1696
+ * twice: two panes sharing one module-level resolver are two mounts, and the
1697
+ * first one to unmount must not take the second one's key with it. A `Set`
1698
+ * keyed by the function itself made them one entry, which either teardown
1699
+ * removed.
1700
+ */
1701
+ registerKeyProvider(provider) {
1702
+ const entry = (providerId) => provider(providerId);
1703
+ this._extraKeyProviders.add(entry);
1704
+ return () => {
1705
+ this._extraKeyProviders.delete(entry);
1706
+ };
1707
+ }
1708
+ /**
1709
+ * The credentials for a provider: a key, or the `{ apiKey, endpoint }` record
1710
+ * the transport and `fetchModels` both read.
1711
+ *
1712
+ * ONE resolution order, so the chat and the model list cannot disagree about
1713
+ * what an endpoint is: a registered resolver (`AparteClient`'s `keyResolver`)
1714
+ * first — which is the precedence the client has always documented — then
1715
+ * `setKeyProvider`.
1716
+ *
1717
+ * Registered sources answer NEWEST first. A page that mounts a second chat, or
1718
+ * remounts one to swap its options — the swap the wrappers document — leaves
1719
+ * more than one source here, and insertion order made the first resolver the
1720
+ * page ever registered answer for every later one, so the key could not be
1721
+ * changed. The snapshot is deliberate too: a source torn down while an earlier
1722
+ * one is being awaited must not shift the iteration underneath it.
1680
1723
  */
1681
1724
  async getKey(providerId) {
1725
+ for (const provider of [...this._extraKeyProviders].reverse()) {
1726
+ const resolved = await provider(providerId);
1727
+ if (resolved) return resolved;
1728
+ }
1682
1729
  if (this._keyProvider) {
1683
- return await this._keyProvider(providerId);
1730
+ return await this._keyProvider(providerId) || void 0;
1684
1731
  }
1685
1732
  return void 0;
1686
1733
  }
@@ -1693,8 +1740,8 @@ class AparteConfig {
1693
1740
  const provider = this._aiProviders.get(providerId);
1694
1741
  if (!provider || !provider.fetchModels) return [];
1695
1742
  try {
1696
- const apiKey = await this.getKey(providerId);
1697
- const models = await provider.fetchModels(apiKey);
1743
+ const auth = await this.getKey(providerId);
1744
+ const models = await provider.fetchModels(auth);
1698
1745
  this._fetchedModels.set(providerId, models);
1699
1746
  return models;
1700
1747
  } catch (error) {
@@ -2064,6 +2111,13 @@ class AparteConfig {
2064
2111
  * Controls what appears in the chat bubble when the AI calls this tool.
2065
2112
  * Use this instead of the generic `tool_call` segment renderer for tool-specific UI.
2066
2113
  *
2114
+ * A renderer that needs a popover (a `<aparte-select>`, a menu) cannot open one
2115
+ * inside the row: the bubble is `content-visibility: auto`, which makes it the
2116
+ * containing block for `position: fixed` descendants and clips a popover to its
2117
+ * own box. Mount it on `document.body` and position it from the trigger's rect
2118
+ * instead — see [Custom tool
2119
+ * renderer](https://apartejs.dev/guides/tools/#custom-tool-renderer).
2120
+ *
2067
2121
  * @example
2068
2122
  * // Hide the segment entirely (UI-only tool like ask_user)
2069
2123
  * aparteGlobalConfig.registerToolRenderer('ask_user', { render: () => '' });
@@ -2121,6 +2175,37 @@ class AparteConfig {
2121
2175
  getToolRenderer(toolName) {
2122
2176
  return this._toolRenderers.get(toolName);
2123
2177
  }
2178
+ /**
2179
+ * Say what a call targets, in the default row, after the tool's name.
2180
+ *
2181
+ * The default row names the tool and keeps its arguments behind a disclosure, so a
2182
+ * reader saw that a file was read, not which one. Replacing the row with
2183
+ * `registerToolRenderer` for that one line meant re-implementing its six states,
2184
+ * the spinner, the disclosure and the relabel contract. This keeps all of them and
2185
+ * adds the line. A tool with its own renderer draws its own row, so its summary is
2186
+ * never read.
2187
+ *
2188
+ * The text is written as text, never parsed as HTML. It is read when the row
2189
+ * renders, on every update to the call (its arguments may still be arriving), and
2190
+ * on every config change — so registering one after a row is on screen shows up on
2191
+ * that row's next update.
2192
+ *
2193
+ * @example
2194
+ * // read_file skills.json
2195
+ * aparteGlobalConfig.registerToolSummary('read_file', (segment) =>
2196
+ * String(segment.toolCall.input['path'] ?? ''));
2197
+ */
2198
+ registerToolSummary(toolName, summary) {
2199
+ this._toolSummaries.set(toolName, summary);
2200
+ }
2201
+ /** Unregister a per-tool summary. */
2202
+ unregisterToolSummary(toolName) {
2203
+ this._toolSummaries.delete(toolName);
2204
+ }
2205
+ /** Get the summary for a specific tool name. Returns undefined if none registered. */
2206
+ getToolSummary(toolName) {
2207
+ return this._toolSummaries.get(toolName);
2208
+ }
2124
2209
  /**
2125
2210
  * Teach the stream parser a tagged block: `<tag attr="…">…</tag>` in the model's
2126
2211
  * prose becomes the segment `toSegment` builds, streamed delta by delta.
@@ -2257,17 +2342,32 @@ class AparteConfig {
2257
2342
  * "installs itself — nothing to register", which is how that was found.
2258
2343
  */
2259
2344
  requestUserInput(request) {
2260
- const previous = this._elicitationQueue;
2261
- const settled = previous === null ? this._present(request) : previous.then(
2345
+ const queue = this._elicitationQueueKey(request);
2346
+ const previous = this._elicitationQueue.get(queue);
2347
+ const settled = previous === void 0 ? this._present(request) : previous.then(
2262
2348
  () => this._present(request),
2263
2349
  () => this._present(request)
2264
2350
  );
2265
2351
  const mine = settled.then(NOOP, NOOP).then(() => {
2266
- if (this._elicitationQueue === mine) this._elicitationQueue = null;
2352
+ if (this._elicitationQueue.get(queue) === mine) this._elicitationQueue.delete(queue);
2267
2353
  });
2268
- this._elicitationQueue = mine;
2354
+ this._elicitationQueue.set(queue, mine);
2269
2355
  return settled;
2270
2356
  }
2357
+ /**
2358
+ * Which queue a request waits in: its own chat's.
2359
+ *
2360
+ * The wait exists so a second question cannot take the panel away from the one on
2361
+ * screen — a property of ONE panel, and `_present` routes by target, so a chat is
2362
+ * the right unit. It used to be one queue for the whole config: two chats on the
2363
+ * global config shared it, and an approval waiting for an answer in the first (which
2364
+ * is all an approval does) held the second's question behind it for ever, with
2365
+ * nothing on screen to answer. Requests that name no chat keep sharing the `null`
2366
+ * queue, because they all reach the same presenter — the top of the stack.
2367
+ */
2368
+ _elicitationQueueKey(request) {
2369
+ return chatBoundaryOf(request.target);
2370
+ }
2271
2371
  /** Hand ONE request to the presenter. Called only from the queue. */
2272
2372
  _present(request) {
2273
2373
  const presenter = this._presenterFor(request.target);
@@ -2306,7 +2406,7 @@ class AparteConfig {
2306
2406
  this._keyProvider = void 0;
2307
2407
  this._conversationManager = void 0;
2308
2408
  this._elicitationPresenters = [];
2309
- this._elicitationQueue = null;
2409
+ this._elicitationQueue.clear();
2310
2410
  this._locale = APARTE_DEFAULT_LOCALE;
2311
2411
  this._actions = [];
2312
2412
  this._sanitizer = defaultSanitizer;
@@ -2314,6 +2414,7 @@ class AparteConfig {
2314
2414
  this._fetchedModels.clear();
2315
2415
  this._tools.clear();
2316
2416
  this._toolRenderers.clear();
2417
+ this._toolSummaries.clear();
2317
2418
  this._streamBlocks.clear();
2318
2419
  this._modelConfig = {};
2319
2420
  this._requireModelSelection = false;
@@ -2399,9 +2500,15 @@ function subscribeConfigChange(el2, handler) {
2399
2500
  window.addEventListener(APARTE_CONFIG_CHANGE, listener);
2400
2501
  return () => window.removeEventListener(APARTE_CONFIG_CHANGE, listener);
2401
2502
  }
2503
+ const settledFromParser = /* @__PURE__ */ new WeakMap();
2504
+ const ONE_SHOT_PROBE = "<i>a</i>";
2505
+ function hasOneShotMarkdownProvider() {
2506
+ return contextConfig().renderMarkdown(ONE_SHOT_PROBE) !== escapeHtml(ONE_SHOT_PROBE);
2507
+ }
2402
2508
  function writeStreamedMarkdown(host, contentEl, content, streaming) {
2403
2509
  if (streaming) {
2404
2510
  if (host._aparteSmd === void 0) {
2511
+ settledFromParser.delete(contentEl);
2405
2512
  const renderer = contextConfig().createStreamingMarkdownRenderer(contentEl);
2406
2513
  if (renderer) {
2407
2514
  contentEl.textContent = "";
@@ -2420,9 +2527,23 @@ function writeStreamedMarkdown(host, contentEl, content, streaming) {
2420
2527
  return;
2421
2528
  }
2422
2529
  } else if (host._aparteSmd) {
2423
- host._aparteSmd.renderer.end();
2530
+ const smd = host._aparteSmd;
2531
+ const tail = content.slice(smd.written);
2532
+ if (tail) {
2533
+ smd.renderer.write(tail);
2534
+ smd.written = content.length;
2535
+ }
2536
+ smd.renderer.end();
2424
2537
  host._aparteSmd = void 0;
2538
+ if (!hasOneShotMarkdownProvider()) {
2539
+ contentEl.innerHTML = contextConfig().sanitizeHtml(contentEl.innerHTML);
2540
+ settledFromParser.set(contentEl, content);
2541
+ return;
2542
+ }
2543
+ } else if (settledFromParser.get(contentEl) === content && !hasOneShotMarkdownProvider()) {
2544
+ return;
2425
2545
  }
2546
+ settledFromParser.delete(contentEl);
2426
2547
  contentEl.innerHTML = contextConfig().renderMarkdown(content);
2427
2548
  }
2428
2549
  function highlightMarkdownFences(contentEl) {
@@ -2761,12 +2882,31 @@ function highlightParts(element) {
2761
2882
  });
2762
2883
  }
2763
2884
  }
2885
+ function toolTarget(segment) {
2886
+ const summary = contextConfig().getToolSummary(segment.toolCall?.name ?? "");
2887
+ if (!summary) return "";
2888
+ try {
2889
+ const text = summary(segment);
2890
+ return typeof text === "string" ? text.trim() : "";
2891
+ } catch {
2892
+ return "";
2893
+ }
2894
+ }
2895
+ function setTarget(element, segment) {
2896
+ const el2 = element.querySelector(".aparte-tool-target");
2897
+ if (!el2) return;
2898
+ const text = toolTarget(segment);
2899
+ if (el2.textContent !== text) el2.textContent = text;
2900
+ el2.toggleAttribute("hidden", !text);
2901
+ }
2764
2902
  function pillMarkup(segment, name, status) {
2765
2903
  const badge = stateBadge(segment);
2904
+ const target = toolTarget(segment);
2766
2905
  return `
2767
2906
  <span class="aparte-tool-label">
2768
2907
  <span class="aparte-tool-icon">${contextConfig().getIcon("tool")}</span>
2769
2908
  <span class="aparte-tool-name">${escapeHtml(name)}</span>
2909
+ <span class="aparte-tool-target"${target ? "" : " hidden"}>${escapeHtml(target)}</span>
2770
2910
  </span>
2771
2911
  <span class="aparte-spinner aparte-tool-spinner" aria-hidden="true"${status === "pending" ? "" : " hidden"}></span>
2772
2912
  <span class="aparte-tool-state">${badge}</span>`;
@@ -2831,6 +2971,7 @@ const toolCallRenderer = {
2831
2971
  }
2832
2972
  const icon = element.querySelector(".aparte-tool-icon");
2833
2973
  if (icon) icon.innerHTML = cfg.getIcon("tool");
2974
+ setTarget(element, segment);
2834
2975
  const statusEl = element.querySelector(".aparte-tool-state");
2835
2976
  if (statusEl) statusEl.innerHTML = stateBadge(segment);
2836
2977
  },
@@ -2871,6 +3012,7 @@ const toolCallRenderer = {
2871
3012
  const badge = element.querySelector(".aparte-tool-state");
2872
3013
  if (badge) badge.innerHTML = stateBadge(segment);
2873
3014
  element.querySelector(".aparte-tool-spinner")?.toggleAttribute("hidden", status !== "pending");
3015
+ setTarget(element, segment);
2874
3016
  setPart(element, "input", input);
2875
3017
  setPart(element, "output", segment.result ?? "");
2876
3018
  highlightParts(element);
@@ -3115,6 +3257,7 @@ function populateBubbleFromMessage(bubble, message, siblingInfo) {
3115
3257
  }
3116
3258
  }
3117
3259
  const APARTE_CONVERSATION_SCHEMA_VERSION = 2;
3260
+ const APARTE_DERIVED_SEGMENTS = Symbol.for("aparte.derivedSegments");
3118
3261
  function applyRetention(messages, tree, maxMessages) {
3119
3262
  if (maxMessages <= 0 || messages.length <= maxMessages) return { messages, tree };
3120
3263
  const trimmedFlat = messages.slice(messages.length - maxMessages);
@@ -3144,6 +3287,20 @@ function applyRetention(messages, tree, maxMessages) {
3144
3287
  };
3145
3288
  return { messages: trimmedFlat, tree: trimmedTree };
3146
3289
  }
3290
+ function withoutDerived(message) {
3291
+ if (!message[APARTE_DERIVED_SEGMENTS]) return message;
3292
+ const { segments: _derived, ...rest } = message;
3293
+ return rest;
3294
+ }
3295
+ function withoutDerivedSegments(messages) {
3296
+ return messages.map(withoutDerived);
3297
+ }
3298
+ function withoutDerivedTree(tree) {
3299
+ return {
3300
+ ...tree,
3301
+ messages: tree.messages.map((entry) => ({ ...entry, message: withoutDerived(entry.message) }))
3302
+ };
3303
+ }
3147
3304
  class AparteConversationManager {
3148
3305
  _adapter;
3149
3306
  _conversations = [];
@@ -3152,6 +3309,14 @@ class AparteConversationManager {
3152
3309
  _initialized = false;
3153
3310
  _retention = null;
3154
3311
  _titleProvider = null;
3312
+ /** Ids whose messages are in memory. Every id, after a loadAll(); only fetched ones after loadMeta(). */
3313
+ _loaded = /* @__PURE__ */ new Set();
3314
+ /** One fetch per id at a time: a second ensureFull(id) while the first is in flight joins it. */
3315
+ _fetching = /* @__PURE__ */ new Map();
3316
+ /** One append per id at a time: the next addMessage(id) reads what the previous one wrote. */
3317
+ _appending = /* @__PURE__ */ new Map();
3318
+ /** The list on its way: a conversation created meanwhile waits for it, or the list would replace it. */
3319
+ _initPromise = null;
3155
3320
  constructor(adapter, options) {
3156
3321
  this._adapter = adapter;
3157
3322
  this._retention = options?.retention ?? null;
@@ -3171,11 +3336,86 @@ class AparteConversationManager {
3171
3336
  return this._titleProvider;
3172
3337
  }
3173
3338
  // ─── Initialisation ────────────────────────────────────────────────────
3174
- /** Load all conversations from the adapter. Call once at app startup. */
3339
+ /**
3340
+ * Load all conversations from the adapter. Call once at app startup.
3341
+ *
3342
+ * A later call re-reads the list from the store: rows it no longer lists are
3343
+ * dropped, and a conversation whose messages are already in memory keeps them —
3344
+ * so the one on screen goes on being persisted.
3345
+ */
3175
3346
  async init() {
3176
- this._conversations = await this._adapter.loadAll();
3177
- this._initialized = true;
3178
- this._notify();
3347
+ const load = (async () => {
3348
+ const split = !!(this._adapter.loadMeta && this._adapter.loadFull);
3349
+ if (this._adapter.loadMeta && !this._adapter.loadFull) {
3350
+ console.warn("[AparteConversationManager] the adapter lists through loadMeta() but has no loadFull(): everything is loaded through loadAll() instead.");
3351
+ }
3352
+ const held = new Map(this._conversations.filter((c) => this._loaded.has(c.id)).map((c) => [c.id, c]));
3353
+ this._loaded.clear();
3354
+ const rows = split ? (await this._adapter.loadMeta()).map((m) => {
3355
+ const kept = held.get(m.id);
3356
+ return kept ? { ...m, messages: kept.messages, ...kept.tree ? { tree: kept.tree } : {} } : { ...m, messages: [] };
3357
+ }) : (await this._adapter.loadAll()).map((c) => ({ ...c, messages: c.messages ?? [] }));
3358
+ const seen = /* @__PURE__ */ new Set();
3359
+ const unique = rows.filter((c) => !seen.has(c.id) && (seen.add(c.id), true));
3360
+ if (unique.length !== rows.length) {
3361
+ console.warn(`[AparteConversationManager] ${rows.length - unique.length} repeated id(s) in the list; the first row wins.`);
3362
+ }
3363
+ this._conversations = unique;
3364
+ for (const c of unique) if (!split || held.has(c.id)) this._loaded.add(c.id);
3365
+ this._initialized = true;
3366
+ this._notify();
3367
+ })();
3368
+ this._initPromise = load;
3369
+ try {
3370
+ await load;
3371
+ } finally {
3372
+ if (this._initPromise === load) this._initPromise = null;
3373
+ }
3374
+ }
3375
+ /** Whether a conversation's messages are in memory (always, without split storage). */
3376
+ isLoaded(id) {
3377
+ return this._loaded.has(id);
3378
+ }
3379
+ /**
3380
+ * Fetch a conversation's messages through the adapter's loadFull(), the first time
3381
+ * they are needed; later calls resolve at once. The row keeps its place and its meta;
3382
+ * the messages (and the tree) come from the full record, and a message written to
3383
+ * the row while the fetch ran goes after them. Resolves `false` when the store has no
3384
+ * such record (or answers with another id) — the row is then NOT loaded, and says so,
3385
+ * rather than counting as an empty conversation.
3386
+ */
3387
+ async ensureFull(id) {
3388
+ if (this._loaded.has(id)) return true;
3389
+ if (!this._adapter.loadFull) return false;
3390
+ const inFlight = this._fetching.get(id);
3391
+ if (inFlight) return inFlight;
3392
+ const fetch2 = (async () => {
3393
+ const full = await this._adapter.loadFull(id);
3394
+ const row = this._find(id);
3395
+ if (!row || !full) return false;
3396
+ if (full.id !== id) {
3397
+ console.warn(`[AparteConversationManager] loadFull('${id}') answered with '${full.id}': ignored.`);
3398
+ return false;
3399
+ }
3400
+ this._replace({ ...row, ...full, id, messages: full.messages ?? [] });
3401
+ this._loaded.add(id);
3402
+ this._notify();
3403
+ return true;
3404
+ })().finally(() => this._fetching.delete(id));
3405
+ this._fetching.set(id, fetch2);
3406
+ return fetch2;
3407
+ }
3408
+ /**
3409
+ * The row with its messages in memory — fetched first when they are not, so a
3410
+ * metadata change never writes a row back without them. `null` when the id is
3411
+ * unknown or the store has no record to fetch.
3412
+ */
3413
+ async _full(id) {
3414
+ if (!this._loaded.has(id) && this._adapter.loadFull) {
3415
+ const loaded = await this.ensureFull(id);
3416
+ if (!loaded) return null;
3417
+ }
3418
+ return this._find(id) ?? null;
3179
3419
  }
3180
3420
  /**
3181
3421
  * Whether `init()` has completed at least once. Consumers use this to
@@ -3207,8 +3447,12 @@ class AparteConversationManager {
3207
3447
  return this._conversations.filter((c) => !!c.archivedAt).sort((a, b) => b.updatedAt - a.updatedAt);
3208
3448
  }
3209
3449
  // ─── Mutations ──────────────────────────────────────────────────────────
3210
- /** Create a new empty conversation, persist it, and make it active. */
3211
- async createNew(title = "New Chat") {
3450
+ /**
3451
+ * Create a new empty conversation, persist it, and make it active. Untitled by
3452
+ * default: the list shows its locale's `newChat` word for an empty title, so the
3453
+ * store never carries an English string a French UI would then display.
3454
+ */
3455
+ async createNew(title = "") {
3212
3456
  const now = Date.now();
3213
3457
  const conv = {
3214
3458
  id: uuid(),
@@ -3218,9 +3462,16 @@ class AparteConversationManager {
3218
3462
  messages: [],
3219
3463
  schemaVersion: APARTE_CONVERSATION_SCHEMA_VERSION
3220
3464
  };
3221
- this._conversations = [conv, ...this._conversations];
3222
3465
  this._activeId = conv.id;
3223
- await this._adapter.save(conv);
3466
+ this._loaded.add(conv.id);
3467
+ if (this._initPromise) await this._initPromise.catch(() => {
3468
+ });
3469
+ this._conversations = [conv, ...this._conversations];
3470
+ try {
3471
+ await this._adapter.save(conv);
3472
+ } catch (err) {
3473
+ console.warn("[AparteConversationManager] save() failed for a new conversation; it stays in memory and the next write retries.", err);
3474
+ }
3224
3475
  this._notify();
3225
3476
  return conv;
3226
3477
  }
@@ -3235,17 +3486,41 @@ class AparteConversationManager {
3235
3486
  this._activeId = null;
3236
3487
  this._notify();
3237
3488
  }
3238
- /** Append a message to a conversation. Auto-generates title from first user message. */
3489
+ /**
3490
+ * Append a message to a conversation. Auto-generates title from first user message.
3491
+ *
3492
+ * One append at a time per conversation: two sends that overlap (a suggestion and a
3493
+ * quick type, coalesced into one new conversation) both suspend before reading the
3494
+ * record — for the messages, then for the title provider — so both used to read a
3495
+ * conversation with no user message in it yet, and the second one's text became the
3496
+ * title.
3497
+ */
3239
3498
  async addMessage(convId, msg) {
3240
- const conv = this._find(convId);
3499
+ const previous = this._appending.get(convId);
3500
+ const run = previous ? previous.then(() => this._append(convId, msg)) : this._append(convId, msg);
3501
+ const tail = run.catch(() => {
3502
+ });
3503
+ this._appending.set(convId, tail);
3504
+ void tail.then(() => {
3505
+ if (this._appending.get(convId) === tail) this._appending.delete(convId);
3506
+ });
3507
+ return run;
3508
+ }
3509
+ async _append(convId, msg) {
3510
+ const conv = await this._full(convId);
3241
3511
  if (!conv) return;
3242
3512
  const isFirstUserMsg = msg.role === "user" && conv.messages.every((m) => m.role !== "user");
3513
+ const title = isFirstUserMsg ? await this._title(msg) : null;
3514
+ const fresh = this._find(convId);
3515
+ if (!fresh) return;
3516
+ const present = fresh.messages.some((m) => m.id === msg.id);
3243
3517
  const updated = {
3244
- ...conv,
3245
- messages: [...conv.messages, msg],
3518
+ ...fresh,
3519
+ messages: present ? fresh.messages : [...fresh.messages, msg],
3246
3520
  updatedAt: Date.now(),
3247
- title: isFirstUserMsg ? await this._title(msg) : conv.title
3521
+ title: title ?? fresh.title
3248
3522
  };
3523
+ if (isFirstUserMsg) updated.autoTitle = true;
3249
3524
  this._replace(updated);
3250
3525
  await this._adapter.save(updated);
3251
3526
  this._notify();
@@ -3258,20 +3533,44 @@ class AparteConversationManager {
3258
3533
  async updateMessages(convId, messages, tree) {
3259
3534
  const conv = this._find(convId);
3260
3535
  if (!conv) return;
3536
+ if (!this._loaded.has(convId)) {
3537
+ console.warn(`[AparteConversationManager] updateMessages('${convId}') before its messages were loaded: ignored.`);
3538
+ return;
3539
+ }
3540
+ const firstUser = messages.find((m) => m.role === "user");
3261
3541
  if (this._retention) {
3262
3542
  ({ messages, tree } = applyRetention(messages, tree, this._retention.maxMessages));
3263
3543
  }
3544
+ const storedMessages = withoutDerivedSegments(messages);
3545
+ const storedTree = tree === void 0 ? void 0 : withoutDerivedTree(tree);
3264
3546
  let contentChanged = true;
3547
+ let treeChanged = tree !== void 0;
3265
3548
  try {
3266
- contentChanged = JSON.stringify(messages) !== JSON.stringify(conv.messages);
3549
+ contentChanged = JSON.stringify(storedMessages) !== JSON.stringify(withoutDerivedSegments(conv.messages));
3550
+ if (storedTree !== void 0) treeChanged = JSON.stringify(storedTree) !== JSON.stringify(conv.tree);
3267
3551
  } catch {
3268
3552
  }
3553
+ let title;
3554
+ if (conv.autoTitle) {
3555
+ const before = conv.messages.find((m) => m.role === "user");
3556
+ if (firstUser && (firstUser.content ?? "") !== (before?.content ?? "")) {
3557
+ title = await this._title(firstUser);
3558
+ }
3559
+ }
3560
+ if (!contentChanged && !treeChanged && title === void 0) return;
3561
+ const fresh = this._find(convId);
3562
+ if (!fresh) return;
3269
3563
  const updated = {
3270
- ...conv,
3271
- messages,
3272
- updatedAt: contentChanged ? Date.now() : conv.updatedAt
3564
+ ...fresh,
3565
+ // What is stored is what the caller sent: segments a viewport DERIVED on
3566
+ // display are not the app's content. Stored, they would fill the record with
3567
+ // core-generated ids and freeze the parse as it stood the day it was saved —
3568
+ // a block grammar registered later would never apply to that message.
3569
+ messages: storedMessages,
3570
+ updatedAt: contentChanged ? Date.now() : fresh.updatedAt
3273
3571
  };
3274
- if (tree !== void 0) updated.tree = tree;
3572
+ if (storedTree !== void 0) updated.tree = storedTree;
3573
+ if (title !== void 0) updated.title = title;
3275
3574
  this._replace(updated);
3276
3575
  await this._adapter.save(updated);
3277
3576
  this._notify();
@@ -3279,35 +3578,41 @@ class AparteConversationManager {
3279
3578
  /** Permanently delete a conversation. */
3280
3579
  async delete(id) {
3281
3580
  this._conversations = this._conversations.filter((c) => c.id !== id);
3581
+ this._loaded.delete(id);
3282
3582
  if (this._activeId === id) this._activeId = null;
3283
3583
  await this._adapter.delete(id);
3284
3584
  this._notify();
3285
3585
  }
3286
3586
  /** Archive a conversation (soft-delete). */
3287
3587
  async archive(id) {
3288
- const conv = this._find(id);
3289
- if (!conv) return;
3290
- const updated = { ...conv, archivedAt: Date.now() };
3291
- this._replace(updated);
3292
- if (this._adapter.archive) {
3293
- await this._adapter.archive(id);
3294
- } else {
3295
- await this._adapter.save(updated);
3296
- }
3588
+ if (!await this._meta(id, (conv) => ({ ...conv, archivedAt: Date.now() }), this._adapter.archive)) return;
3297
3589
  if (this._activeId === id) this._activeId = null;
3298
3590
  this._notify();
3299
3591
  }
3592
+ /**
3593
+ * A metadata change, through the adapter's own hook when it has one (the row is
3594
+ * updated in memory, nothing else travels), else through `save()` — with the
3595
+ * messages fetched first, so a row the list came with (`messages: []`) is never
3596
+ * written back over the stored ones.
3597
+ */
3598
+ async _meta(id, change, hook) {
3599
+ if (hook) {
3600
+ const conv2 = this._find(id);
3601
+ if (!conv2) return false;
3602
+ this._replace(change(conv2));
3603
+ await hook.call(this._adapter, id);
3604
+ return true;
3605
+ }
3606
+ const conv = await this._full(id);
3607
+ if (!conv) return false;
3608
+ const updated = change(conv);
3609
+ this._replace(updated);
3610
+ await this._adapter.save(updated);
3611
+ return true;
3612
+ }
3300
3613
  /** Restore an archived conversation. */
3301
3614
  async unarchive(id) {
3302
- const conv = this._find(id);
3303
- if (!conv) return;
3304
- const updated = { ...conv, archivedAt: void 0 };
3305
- this._replace(updated);
3306
- if (this._adapter.unarchive) {
3307
- await this._adapter.unarchive(id);
3308
- } else {
3309
- await this._adapter.save(updated);
3310
- }
3615
+ if (!await this._meta(id, (conv) => ({ ...conv, archivedAt: void 0 }), this._adapter.unarchive)) return;
3311
3616
  this._notify();
3312
3617
  }
3313
3618
  /**
@@ -3317,28 +3622,12 @@ class AparteConversationManager {
3317
3622
  * does not gets the whole record through `save()`.
3318
3623
  */
3319
3624
  async pin(id) {
3320
- const conv = this._find(id);
3321
- if (!conv) return;
3322
- const updated = { ...conv, pinnedAt: Date.now() };
3323
- this._replace(updated);
3324
- if (this._adapter.pin) {
3325
- await this._adapter.pin(id);
3326
- } else {
3327
- await this._adapter.save(updated);
3328
- }
3625
+ if (!await this._meta(id, (conv) => ({ ...conv, pinnedAt: Date.now() }), this._adapter.pin)) return;
3329
3626
  this._notify();
3330
3627
  }
3331
3628
  /** Unpin a conversation. Metadata only — see `pin()`. */
3332
3629
  async unpin(id) {
3333
- const conv = this._find(id);
3334
- if (!conv) return;
3335
- const updated = { ...conv, pinnedAt: void 0 };
3336
- this._replace(updated);
3337
- if (this._adapter.unpin) {
3338
- await this._adapter.unpin(id);
3339
- } else {
3340
- await this._adapter.save(updated);
3341
- }
3630
+ if (!await this._meta(id, (conv) => ({ ...conv, pinnedAt: void 0 }), this._adapter.unpin)) return;
3342
3631
  this._notify();
3343
3632
  }
3344
3633
  /** Update the title manually. The full input is preserved — UI surfaces
@@ -3346,12 +3635,17 @@ class AparteConversationManager {
3346
3635
  * (`min-w-0` + `truncate`). Auto-titles produced internally by
3347
3636
  * `_autoTitle()` remain capped at a sensible length on input. */
3348
3637
  async updateTitle(id, title) {
3349
- const conv = this._find(id);
3350
- if (!conv) return;
3351
- const updated = { ...conv, title: title.trim(), updatedAt: Date.now() };
3352
- this._replace(updated);
3353
- await this._adapter.save(updated);
3354
- this._notify();
3638
+ if (!this._find(id)) {
3639
+ console.warn(`[AparteConversationManager] updateTitle('${id}'): unknown conversation.`);
3640
+ return;
3641
+ }
3642
+ const next = title.trim();
3643
+ const done = await this._meta(
3644
+ id,
3645
+ (conv) => ({ ...conv, title: next, autoTitle: false }),
3646
+ this._adapter.rename ? (convId) => this._adapter.rename(convId, next) : void 0
3647
+ );
3648
+ if (done) this._notify();
3355
3649
  }
3356
3650
  // ─── Observer ───────────────────────────────────────────────────────────
3357
3651
  subscribe(listener) {
@@ -3378,8 +3672,9 @@ class AparteConversationManager {
3378
3672
  });
3379
3673
  }
3380
3674
  /**
3381
- * The title of a conversation, decided once, on its first user message: the
3382
- * provider's answer when one is registered and answers, else the default.
3675
+ * The title of a conversation, decided on its first user message — and again if
3676
+ * that message is edited while the title is still the manager's: the provider's
3677
+ * answer when one is registered and answers, else the default.
3383
3678
  */
3384
3679
  async _title(msg) {
3385
3680
  const fallback = this._autoTitle(msg);
@@ -3393,7 +3688,7 @@ class AparteConversationManager {
3393
3688
  }
3394
3689
  }
3395
3690
  _autoTitle(msg) {
3396
- return (msg.content ?? "").trim() || "New Chat";
3691
+ return (msg.content ?? "").trim();
3397
3692
  }
3398
3693
  }
3399
3694
  function filesToAttachments(files) {
@@ -3432,6 +3727,8 @@ class AparteConversationController {
3432
3727
  _subscribedTo = null;
3433
3728
  /** Between bind() and unbind(): the only time a manager subscription may be installed. */
3434
3729
  _bound = false;
3730
+ /** The binding shows the wait this controller asked for — see `_showWait`. */
3731
+ _waitShown = false;
3435
3732
  /**
3436
3733
  * React to an external mutation of the manager (another component deleting or
3437
3734
  * archiving the active conversation). Without this the binding would keep
@@ -3452,6 +3749,7 @@ class AparteConversationController {
3452
3749
  }
3453
3750
  this._activeId = null;
3454
3751
  this._binding.clearMessages();
3752
+ this._showWait(false);
3455
3753
  }
3456
3754
  };
3457
3755
  constructor(binding, options = {}) {
@@ -3558,7 +3856,7 @@ class AparteConversationController {
3558
3856
  void this.setConversationId(evt.detail?.id ?? null);
3559
3857
  };
3560
3858
  if (typeof window !== "undefined") {
3561
- window.addEventListener("aparte-select-conversation", this._onSelectConversation);
3859
+ window.addEventListener("aparte-conversation-select", this._onSelectConversation);
3562
3860
  }
3563
3861
  this._bound = true;
3564
3862
  this._manager();
@@ -3599,7 +3897,7 @@ class AparteConversationController {
3599
3897
  this._onMessageAborted = null;
3600
3898
  }
3601
3899
  if (this._onSelectConversation && typeof window !== "undefined") {
3602
- window.removeEventListener("aparte-select-conversation", this._onSelectConversation);
3900
+ window.removeEventListener("aparte-conversation-select", this._onSelectConversation);
3603
3901
  this._onSelectConversation = null;
3604
3902
  }
3605
3903
  if (this._unsubscribeManager) {
@@ -3645,6 +3943,7 @@ class AparteConversationController {
3645
3943
  this._activeId = null;
3646
3944
  manager?.clearActive();
3647
3945
  if (isSwitch) this._binding.clearMessages();
3946
+ this._showWait(false);
3648
3947
  return;
3649
3948
  }
3650
3949
  const conv = manager?.conversations.find((c) => c.id === id);
@@ -3660,29 +3959,68 @@ class AparteConversationController {
3660
3959
  console.warn("[ConversationController] id still unknown after hydration — clearing. id:", id);
3661
3960
  manager.clearActive();
3662
3961
  this._binding.clearMessages();
3962
+ this._showWait(false);
3663
3963
  }
3664
3964
  });
3665
3965
  this._stopHydrationRetry = stop;
3966
+ this._showWait(false);
3666
3967
  return;
3667
3968
  }
3668
3969
  console.warn("[ConversationController] unknown id or no manager — clearing. manager:", !!manager, "conv:", !!conv);
3669
3970
  this._activeId = null;
3670
3971
  manager?.clearActive();
3671
3972
  this._binding.clearMessages();
3973
+ this._showWait(false);
3672
3974
  return;
3673
3975
  }
3674
3976
  this._activeId = id;
3675
3977
  manager.select(id);
3978
+ let record = conv;
3979
+ const fetching = !manager.isLoaded(id);
3980
+ if (fetching) {
3981
+ this._showWait(true);
3982
+ this._binding.clearMessages();
3983
+ let loaded = false;
3984
+ try {
3985
+ loaded = await manager.ensureFull(id);
3986
+ } catch (err) {
3987
+ console.warn("[ConversationController] the conversation could not be loaded:", id, err);
3988
+ }
3989
+ if (this._activeId !== id) return;
3990
+ if (!loaded) {
3991
+ if (manager.conversations.some((c) => c.id === id)) {
3992
+ console.warn("[ConversationController] no messages came back for", id, "— leaving it.");
3993
+ }
3994
+ this._activeId = null;
3995
+ manager.clearActive();
3996
+ this._binding.clearMessages();
3997
+ this._showWait(false);
3998
+ return;
3999
+ }
4000
+ record = manager.conversations.find((c) => c.id === id) ?? conv;
4001
+ }
3676
4002
  this._isLoadingConversation = true;
3677
4003
  try {
3678
- this._binding.setMessages([...conv.messages]);
3679
- if (conv.tree && this._binding.importTree) {
3680
- this._binding.importTree(conv.tree);
4004
+ this._binding.setMessages([...record.messages ?? []]);
4005
+ if (record.tree && this._binding.importTree) {
4006
+ this._binding.importTree(record.tree);
3681
4007
  }
3682
4008
  } finally {
3683
4009
  this._isLoadingConversation = false;
4010
+ this._showWait(false);
3684
4011
  }
3685
4012
  }
4013
+ /**
4014
+ * The binding's wait, owned here: whoever runs `setConversationId` last clears it
4015
+ * — a loaded conversation or a new chat that replaces a fetch, a delete of the
4016
+ * one being fetched — so a wait shown for one call never outlives the call that
4017
+ * superseded it.
4018
+ */
4019
+ _showWait(on) {
4020
+ if (this._waitShown === on) return;
4021
+ this._waitShown = on;
4022
+ this._binding.setLoading?.(on);
4023
+ }
3686
4024
  // ─────────────────────────────────────────────────────────────────────
3687
4025
  // Internal
3688
4026
  // ─────────────────────────────────────────────────────────────────────
@@ -3700,6 +4038,7 @@ class AparteConversationController {
3700
4038
  if (!this._activeId) return;
3701
4039
  const manager = this._manager();
3702
4040
  if (!manager) return;
4041
+ if (!manager.isLoaded(this._activeId)) return;
3703
4042
  const msgs = this._binding.getMessages().map(
3704
4043
  (m) => m.status === "streaming" || m.status === "pending" ? { ...m, status: "completed" } : m
3705
4044
  );
@@ -3729,12 +4068,12 @@ class AparteConversationController {
3729
4068
  }
3730
4069
  if (!convId) {
3731
4070
  if (!this._ensureInFlight) {
3732
- const text = (userMsg.content ?? "").toString().trim() || "New Chat";
4071
+ const text = (userMsg.content ?? "").toString().trim();
3733
4072
  this._ensureInFlight = manager.createNew(text).then((conv) => conv.id);
3734
4073
  }
3735
4074
  convId = await this._ensureInFlight;
3736
4075
  this._ensureInFlight = null;
3737
- if (this._activeId !== wasActiveId) {
4076
+ if (this._activeId !== wasActiveId && this._activeId !== convId) {
3738
4077
  void manager.delete(convId).catch(() => {
3739
4078
  });
3740
4079
  return;
@@ -3872,7 +4211,9 @@ class AparteChatHost {
3872
4211
  setMessages: (msgs) => {
3873
4212
  this._beginConversationSwap();
3874
4213
  this._vp()?.resetSpacer?.();
3875
- this.binding.setMessages(msgs.map(adoptMessageSegments));
4214
+ const adopted = msgs.map(adoptMessageSegments);
4215
+ this.binding.setMessages(adopted);
4216
+ this._vp()?.setMessages?.(adopted);
3876
4217
  },
3877
4218
  appendMessage: (msg) => this.appendMessage(msg),
3878
4219
  getMessages: () => this.binding.getMessages(),
@@ -3887,6 +4228,17 @@ class AparteChatHost {
3887
4228
  },
3888
4229
  importTree: (tree) => {
3889
4230
  this._vp()?.importTree?.(tree);
4231
+ },
4232
+ // One writer for the wait. A wrapper that hears `onLoadingChange` draws the
4233
+ // wait itself and writes the viewport's attribute from its own
4234
+ // `loading || hostLoading` — so the host writing it too meant that, with the
4235
+ // consumer's own prop on, a `setLoading(false)` here removed the attribute
4236
+ // (and `aria-busy`) while the wrapper still drew the skeleton and still held
4237
+ // the composer shut. A binding that cannot hear it has no other way to know,
4238
+ // and there the attribute stays the host's to write.
4239
+ setLoading: (on) => {
4240
+ if (this.binding.onLoadingChange) this.binding.onLoadingChange(on);
4241
+ else this._vp()?.setLoading?.(on);
3890
4242
  }
3891
4243
  };
3892
4244
  this._controller = new AparteConversationController(convBinding, {
@@ -3992,7 +4344,7 @@ class AparteChatHost {
3992
4344
  // defaults rather than the page's.
3993
4345
  resolveConfig(this.binding.host).getSegmentDefaults(segment.type)
3994
4346
  ) : segment;
3995
- this._lastBubble()?.addSegment?.(stamped);
4347
+ this._streamBubble(last?.id)?.addSegment?.(stamped);
3996
4348
  if (!last) return;
3997
4349
  const segments = [...last.segments || [], stamped];
3998
4350
  const next = [...msgs.slice(0, -1), { ...last, segments }];
@@ -4007,7 +4359,7 @@ class AparteChatHost {
4007
4359
  if (last && this._isOrphan(last.id)) return;
4008
4360
  const current = last?.segments?.find((s) => s.id === segmentId);
4009
4361
  const stamped = current ? stampSegmentOnUpdate(current, updates) : updates;
4010
- this._lastBubble()?.updateSegment?.(segmentId, stamped);
4362
+ this._streamBubble(last?.id)?.updateSegment?.(segmentId, stamped);
4011
4363
  if (!last || !last.segments) return;
4012
4364
  const segments = last.segments.map(
4013
4365
  (s) => s.id === segmentId ? mergeSegmentUpdate(s, stamped) : s
@@ -4019,7 +4371,7 @@ class AparteChatHost {
4019
4371
  /** Remove a transient segment (a status row the host added itself). */
4020
4372
  removeSegment(segmentId) {
4021
4373
  this._flushStreamState();
4022
- this._lastBubble()?.removeSegment?.(segmentId);
4374
+ this._streamBubble(this.binding.getMessages().at(-1)?.id)?.removeSegment?.(segmentId);
4023
4375
  const msgs = this.binding.getMessages();
4024
4376
  const last = msgs[msgs.length - 1];
4025
4377
  if (!last?.segments) return;
@@ -4331,6 +4683,27 @@ class AparteChatHost {
4331
4683
  `aparte-chat-bubble[message-id="${cssEscape(id)}"]`
4332
4684
  );
4333
4685
  }
4686
+ /**
4687
+ * The bubble the streaming writes below belong to: the one carrying that message's
4688
+ * id, and NOTHING when it is not on the page yet.
4689
+ *
4690
+ * It used to be "the last bubble in the viewport", which is a different message
4691
+ * whenever the DOM is a render behind the list — which is every framework here.
4692
+ * `appendMessage` puts the new reply in React's state and React paints it on its own
4693
+ * schedule, so a first chunk arriving in between went into the PREVIOUS reply's
4694
+ * bubble; `syncBubbles` then wrote the same segment onto the real one, and the same
4695
+ * `data-segment-id` was on two bubbles.
4696
+ *
4697
+ * Skipping the paint is safe because it is only ever a paint: the message list is
4698
+ * written either way, and `syncBubbles` — called by the framework after each render,
4699
+ * always re-syncing the last message — puts it on screen as soon as the bubble
4700
+ * exists. Without an id there is nothing to resolve, so the last bubble stands: that
4701
+ * is a host writing into a transcript it has not appended to.
4702
+ */
4703
+ _streamBubble(messageId) {
4704
+ if (messageId) return this._bubbleById(messageId);
4705
+ return this._lastBubble();
4706
+ }
4334
4707
  _lastBubble() {
4335
4708
  const vp = this.binding.viewport;
4336
4709
  if (!vp) return null;
@@ -4357,9 +4730,15 @@ class AparteChatHost {
4357
4730
  const d = e?.detail;
4358
4731
  if (d?.messageId) this._setStreamingId(d.messageId);
4359
4732
  };
4733
+ const endsThisTurn = (e) => {
4734
+ const id = e?.detail?.messageId;
4735
+ return !id || this._streamingId === null || id === this._streamingId;
4736
+ };
4360
4737
  const onDone = (e) => {
4361
- this.binding.onTypingChange?.(false);
4362
- this._setStreamingId(null);
4738
+ if (endsThisTurn(e)) {
4739
+ this.binding.onTypingChange?.(false);
4740
+ this._setStreamingId(null);
4741
+ }
4363
4742
  const d = e?.detail;
4364
4743
  if (!d?.messageId || !d.usage) return;
4365
4744
  const next = this.binding.getMessages().map(
@@ -4369,7 +4748,8 @@ class AparteChatHost {
4369
4748
  this.binding.onMessagesChange?.(next);
4370
4749
  this._bubbleById(d.messageId)?.setUsage?.(d.usage);
4371
4750
  };
4372
- const onEnd = () => {
4751
+ const onEnd = (e) => {
4752
+ if (!endsThisTurn(e)) return;
4373
4753
  this.binding.onTypingChange?.(false);
4374
4754
  this._setStreamingId(null);
4375
4755
  };
@@ -4589,6 +4969,15 @@ function readableToAsyncIterable(stream, signal) {
4589
4969
  }
4590
4970
  };
4591
4971
  }
4972
+ function eventOrigin(e) {
4973
+ return e.composedPath?.()[0] ?? e.target;
4974
+ }
4975
+ function rootOf(node) {
4976
+ return node.getRootNode();
4977
+ }
4978
+ function activeElementIn(node) {
4979
+ return node.getRootNode().activeElement ?? document.activeElement;
4980
+ }
4592
4981
  let steppedPanelSeq = 0;
4593
4982
  let _relabels = null;
4594
4983
  function onRelabel(fn) {
@@ -5110,11 +5499,14 @@ function requestUserInput(request) {
5110
5499
  return resolveConfig(request.target ?? null).requestUserInput(request);
5111
5500
  }
5112
5501
  const COMPACTION_PREAMBLE = "The earlier part of this conversation was compacted. This is a summary of what came before; continue from it.";
5502
+ const warnedUninlinedFiles = /* @__PURE__ */ new Set();
5503
+ const CHAT_SELECTOR = "aparte-chat, aparte-chat-viewport, [data-aparte-chat]";
5113
5504
  class AparteClient {
5114
5505
  _boundHandler = null;
5115
5506
  _boundAbortHandler = null;
5116
5507
  _boundRetryHandler = null;
5117
5508
  _boundEditHandler = null;
5509
+ _releaseKeyResolver = null;
5118
5510
  _isAborted = false;
5119
5511
  /**
5120
5512
  * Does this window event belong to this client?
@@ -5135,7 +5527,7 @@ class AparteClient {
5135
5527
  const scope = this.options.scopeToTargetId;
5136
5528
  if (scope) return detail?.targetId === scope;
5137
5529
  if (this._config === aparteGlobalConfig) return true;
5138
- const target = this._resolveTarget(detail?.targetId);
5530
+ const target = this._resolveTarget(detail?.targetId, e);
5139
5531
  if (!target) return true;
5140
5532
  const owner = resolveConfig(target);
5141
5533
  return owner === this._config || owner === aparteGlobalConfig;
@@ -5151,6 +5543,7 @@ class AparteClient {
5151
5543
  ...options
5152
5544
  };
5153
5545
  this._config = options.config ?? aparteGlobalConfig;
5546
+ this._registerKeyResolver();
5154
5547
  if (this.options.autoRegister) {
5155
5548
  registerDefaultRenderers(this._config);
5156
5549
  } else {
@@ -5176,6 +5569,20 @@ class AparteClient {
5176
5569
  _beginUserTurn() {
5177
5570
  this._isAborted = false;
5178
5571
  }
5572
+ /**
5573
+ * Put this client's `keyResolver` on its config, and hold the teardown so
5574
+ * `stop()` can take it back off.
5575
+ *
5576
+ * The registration used to be fire-and-forget, so it outlived the client that
5577
+ * made it: a stopped client kept signing model-list requests, and a remount —
5578
+ * the way the wrappers document swapping options — added a source behind the
5579
+ * one it meant to replace instead of replacing it. Idempotent, so the usual
5580
+ * `new AparteClient({…}).start()` registers once.
5581
+ */
5582
+ _registerKeyResolver() {
5583
+ if (this._releaseKeyResolver || !this.options.keyResolver) return;
5584
+ this._releaseKeyResolver = this._config.registerKeyProvider(this.options.keyResolver);
5585
+ }
5179
5586
  /**
5180
5587
  * Sets up the event listeners.
5181
5588
  * This is called once in the constructor.
@@ -5194,6 +5601,7 @@ class AparteClient {
5194
5601
  * Start listening for aparte-send events on the window.
5195
5602
  */
5196
5603
  start() {
5604
+ this._registerKeyResolver();
5197
5605
  if (!this._boundHandler) {
5198
5606
  this._setupListeners();
5199
5607
  }
@@ -5229,6 +5637,8 @@ class AparteClient {
5229
5637
  */
5230
5638
  stop() {
5231
5639
  this.abort();
5640
+ this._releaseKeyResolver?.();
5641
+ this._releaseKeyResolver = null;
5232
5642
  if (!this._boundHandler) return;
5233
5643
  window.removeEventListener("aparte-send", this._boundHandler);
5234
5644
  this._boundHandler = null;
@@ -5429,7 +5839,7 @@ class AparteClient {
5429
5839
  const { messageId, targetId } = event.detail ?? {};
5430
5840
  this._beginUserTurn();
5431
5841
  if (!messageId) return;
5432
- const targetElement = this._resolveTarget(targetId);
5842
+ const targetElement = this._resolveTarget(targetId, event);
5433
5843
  if (!targetElement) {
5434
5844
  console.warn("[AparteClient] aparte-retry — no target found");
5435
5845
  return;
@@ -5490,7 +5900,7 @@ class AparteClient {
5490
5900
  const { messageId, content: newContent, targetId } = event.detail ?? {};
5491
5901
  this._beginUserTurn();
5492
5902
  if (!messageId || newContent === void 0) return;
5493
- const targetElement = this._resolveTarget(targetId);
5903
+ const targetElement = this._resolveTarget(targetId, event);
5494
5904
  if (!targetElement) {
5495
5905
  console.warn("[AparteClient] aparte-edit — no target found");
5496
5906
  return;
@@ -5532,24 +5942,83 @@ class AparteClient {
5532
5942
  authConfig
5533
5943
  );
5534
5944
  }
5945
+ /**
5946
+ * The chain the gesture really came up, nearest node first.
5947
+ *
5948
+ * Read from a `window` listener, `event.target` is the shadow HOST when the event
5949
+ * left a tree of the consumer's own, and `parentElement` stops at that tree's root —
5950
+ * so a walk from either starts, or ends, outside the chat the person typed into.
5951
+ * `composedPath()` is the one chain that crosses. The fallback is for an event
5952
+ * dispatched by hand, which has no path.
5953
+ */
5954
+ _originChain(event) {
5955
+ const path = event?.composedPath?.() ?? [];
5956
+ if (path.length > 0) return path;
5957
+ const chain = [];
5958
+ let node = event ? eventOrigin(event) : null;
5959
+ while (node) {
5960
+ chain.push(node);
5961
+ node = node.parentElement;
5962
+ }
5963
+ return chain;
5964
+ }
5965
+ /**
5966
+ * The trees a lookup must search, the one the gesture came from FIRST.
5967
+ *
5968
+ * `document.getElementById` and `document.querySelectorAll` cannot see into a shadow
5969
+ * root, so a chat mounted in one — the arrangement the theming guide endorses — was
5970
+ * invisible to both: alone, the send was dropped with a warning; on a page that also
5971
+ * holds a chat in the light DOM, the scan answered with the WRONG chat, so the
5972
+ * person's message AND the model's reply landed in a transcript they never typed
5973
+ * into. Nearest tree first is what keeps it from swapping the other way round.
5974
+ */
5975
+ _rootsFor(event) {
5976
+ const roots = [];
5977
+ for (const node of this._originChain(event)) {
5978
+ if (typeof node.getRootNode !== "function") continue;
5979
+ const root = rootOf(node);
5980
+ if (!roots.includes(root)) roots.push(root);
5981
+ }
5982
+ if (!roots.includes(document)) roots.push(document);
5983
+ return roots;
5984
+ }
5985
+ /** The element an id names, looked up in the gesture's own tree before the document. */
5986
+ _byId(targetId, event) {
5987
+ for (const root of this._rootsFor(event)) {
5988
+ const el2 = root.getElementById?.(targetId);
5989
+ if (el2) return el2;
5990
+ }
5991
+ return null;
5992
+ }
5993
+ /** The first chat that can render, own tree first — the path when nothing names one. */
5994
+ _scanForTarget(event) {
5995
+ for (const root of this._rootsFor(event)) {
5996
+ for (const candidate of root.querySelectorAll(CHAT_SELECTOR)) {
5997
+ const target = this._asRenderTarget(candidate);
5998
+ if (target) return target;
5999
+ }
6000
+ }
6001
+ return null;
6002
+ }
5535
6003
  /**
5536
6004
  * Resolve a target element by id (from event detail.targetId) or via targetResolver / DOM scan.
5537
6005
  */
5538
- _resolveTarget(targetId) {
6006
+ _resolveTarget(targetId, event) {
5539
6007
  if (targetId) {
5540
- const el2 = document.getElementById(targetId);
6008
+ const el2 = this._byId(targetId, event);
5541
6009
  if (el2) return this._asRenderTarget(el2) ?? el2;
5542
6010
  }
5543
6011
  if (this.options.targetResolver) {
5544
6012
  const el2 = this.options.targetResolver();
5545
6013
  if (el2) return this._asRenderTarget(el2) ?? el2;
5546
6014
  }
5547
- const candidates = document.querySelectorAll("aparte-chat, aparte-chat-viewport, [data-aparte-chat]");
5548
- for (const candidate of candidates) {
5549
- const target = this._asRenderTarget(candidate);
5550
- if (target) return target;
6015
+ const scanned = this._scanForTarget(event);
6016
+ if (scanned) return scanned;
6017
+ for (const root of this._rootsFor(event)) {
6018
+ const first = root.querySelector(CHAT_SELECTOR);
6019
+ if (first) return first;
5551
6020
  }
5552
- return candidates[0] ?? null;
6021
+ return null;
5553
6022
  }
5554
6023
  /**
5555
6024
  * Resolve an element to a usable render target: itself when it exposes
@@ -5640,7 +6109,7 @@ class AparteClient {
5640
6109
  let targetElement = null;
5641
6110
  const targetId = event.detail?.targetId;
5642
6111
  if (targetId) {
5643
- const byId = document.getElementById(targetId);
6112
+ const byId = this._byId(targetId, event);
5644
6113
  const resolved = this._asRenderTarget(byId);
5645
6114
  if (resolved) {
5646
6115
  targetElement = resolved;
@@ -5649,32 +6118,22 @@ class AparteClient {
5649
6118
  }
5650
6119
  }
5651
6120
  if (!targetElement && this.options.targetResolver) {
5652
- const resolved = this.options.targetResolver();
5653
- if (resolved && typeof resolved.appendMessage === "function") {
5654
- targetElement = resolved;
5655
- }
5656
- }
5657
- if (!targetElement) {
5658
- let walker = event.target;
5659
- while (walker && typeof walker.appendMessage !== "function") {
5660
- walker = walker.parentElement;
5661
- }
5662
- if (walker) {
5663
- targetElement = walker;
5664
- }
6121
+ targetElement = this._asRenderTarget(
6122
+ this.options.targetResolver()
6123
+ );
5665
6124
  }
5666
6125
  if (!targetElement) {
5667
- const candidates = document.querySelectorAll(
5668
- "aparte-chat, aparte-chat-viewport, [data-aparte-chat]"
5669
- );
5670
- for (const candidate of candidates) {
5671
- const resolved = this._asRenderTarget(candidate);
5672
- if (resolved) {
5673
- targetElement = resolved;
6126
+ for (const node of this._originChain(event)) {
6127
+ const walker = node;
6128
+ if (typeof walker?.appendMessage === "function") {
6129
+ targetElement = walker;
5674
6130
  break;
5675
6131
  }
5676
6132
  }
5677
6133
  }
6134
+ if (!targetElement) {
6135
+ targetElement = this._scanForTarget(event);
6136
+ }
5678
6137
  if (!targetElement) {
5679
6138
  console.warn("[AparteClient] ⚠️ No target element found with appendMessage support. Provide a targetResolver in AparteClientOptions or ensure the composer has a `target` attribute.");
5680
6139
  return;
@@ -5841,7 +6300,8 @@ ${text}` : text;
5841
6300
  *
5842
6301
  * `thinking` and `tool_call` stay out on purpose — the first is the model's own
5843
6302
  * scratchpad, and most APIs neither want it back nor bill for it kindly; the
5844
- * second is already in the history as a call and a result.
6303
+ * second was already carried, within the turn, as the assistant's calls and the
6304
+ * `tool` messages answering them.
5845
6305
  */
5846
6306
  _segmentsToText(segments) {
5847
6307
  if (!segments?.length) return "";
@@ -5885,7 +6345,8 @@ ${fence}`);
5885
6345
  * - Images → AparteImagePart (base64 data URL)
5886
6346
  * - Text files (txt, md, json, csv, xml, html, css, js, ts, …) → AparteTextPart
5887
6347
  * injected as a fenced block so all models (including local) can read them.
5888
- * - Other binary files → silently ignored.
6348
+ * - Other binary files → no content part, and a one-time `console.warn` naming the
6349
+ * file (see the branch below for why it is not silent).
5889
6350
  */
5890
6351
  async _filesToContentParts(files) {
5891
6352
  const TEXT_TYPES = /^(text\/|application\/(json|xml|javascript|typescript|x-yaml|yaml|toml|csv|markdown))/i;
@@ -5918,6 +6379,12 @@ ${content}
5918
6379
  reader.readAsText(file);
5919
6380
  });
5920
6381
  }
6382
+ if (!warnedUninlinedFiles.has(file.name)) {
6383
+ warnedUninlinedFiles.add(file.name);
6384
+ console.warn(
6385
+ `[aparte] ${file.name} (${file.type || "unknown type"}) is attached to the message but is not sent to the model: aparté inlines images and text files only. Handle it in your app (RAG, an upload) — the chip stays visible either way.`
6386
+ );
6387
+ }
5921
6388
  return Promise.resolve(null);
5922
6389
  })
5923
6390
  );
@@ -6107,6 +6574,20 @@ class AparteMessageRepository {
6107
6574
  getMessageById(id) {
6108
6575
  return this._nodes.get(id)?.current;
6109
6576
  }
6577
+ /**
6578
+ * Every message the tree holds — every branch, not just the active path.
6579
+ *
6580
+ * `getMessages()` walks head → root, so a question about the whole conversation
6581
+ * ("is anything still streaming?") answered from it could not see a turn that a
6582
+ * retry had just taken off the path — and the viewport wrote that turn's remaining
6583
+ * segments into the bubble which replaced it. No order is promised beyond
6584
+ * insertion: a caller that needs the topology reads `export()`.
6585
+ */
6586
+ getAllMessages() {
6587
+ const all = [];
6588
+ for (const node of this._nodes.values()) all.push(node.current);
6589
+ return all;
6590
+ }
6110
6591
  /**
6111
6592
  * Returns the IDs of all siblings of `messageId`
6112
6593
  * (i.e. all children of its parent, including itself).
@@ -6165,15 +6646,19 @@ class AparteMessageRepository {
6165
6646
  this._head = findHead(node);
6166
6647
  }
6167
6648
  /**
6168
- * Remove all descendants of `messageId` (inclusive) and set `head` to
6169
- * the parent of `messageId`. Used by edit/truncate flows.
6649
+ * Remove all descendants of `messageId` (inclusive) and, when the head was among
6650
+ * them, set `head` to the parent of `messageId`. Used by edit/truncate flows.
6651
+ *
6652
+ * A head in a branch this call did not touch stays where it is: it used to be
6653
+ * relocated regardless, which took the active path out of the version the reader was
6654
+ * on for a truncate somewhere else entirely.
6170
6655
  *
6171
6656
  * @param messageId The first message to remove
6172
- * @param newParentId Optional explicit parent to set head to
6173
6657
  */
6174
6658
  resetHead(messageId) {
6175
6659
  const node = this._nodes.get(messageId);
6176
6660
  if (!node) return;
6661
+ const headBefore = this._head;
6177
6662
  this._deleteDescendants(node);
6178
6663
  const parentOrRoot = node.prev ?? this._root;
6179
6664
  parentOrRoot.children = parentOrRoot.children.filter((id) => id !== messageId);
@@ -6181,7 +6666,8 @@ class AparteMessageRepository {
6181
6666
  parentOrRoot.next = null;
6182
6667
  }
6183
6668
  this._nodes.delete(messageId);
6184
- this._head = node.prev;
6669
+ const headSurvived = headBefore !== null && headBefore !== node && this._nodes.get(headBefore.current.id) === headBefore;
6670
+ if (!headSurvived) this._head = node.prev;
6185
6671
  }
6186
6672
  /**
6187
6673
  * Update a message's data without changing its position in the tree.
@@ -6336,13 +6822,13 @@ const APARTE_DEFAULT_UI_EVENTS = [
6336
6822
  "aparte-reset-done",
6337
6823
  "aparte-path-changed",
6338
6824
  // <aparte-conversation-list>
6339
- "aparte-select-conversation",
6340
- "aparte-delete-conversation",
6341
- "aparte-archive-conversation",
6342
- "aparte-unarchive-conversation",
6343
- "aparte-rename-conversation",
6344
- "aparte-pin-conversation",
6345
- "aparte-unpin-conversation",
6825
+ "aparte-conversation-select",
6826
+ "aparte-conversation-delete",
6827
+ "aparte-conversation-archive",
6828
+ "aparte-conversation-unarchive",
6829
+ "aparte-conversation-rename",
6830
+ "aparte-conversation-pin",
6831
+ "aparte-conversation-unpin",
6346
6832
  // the select primitives
6347
6833
  "aparte-select-change",
6348
6834
  "aparte-select-open",
@@ -6470,15 +6956,20 @@ export {
6470
6956
  subscribeConfigChange as a4,
6471
6957
  unregisterSegmentRenderer as a5,
6472
6958
  uuid as a6,
6473
- mergeSegmentUpdate as a7,
6474
- writeStreamedMarkdown as a8,
6475
- stampSegmentActivity as a9,
6476
- stampSegmentOnInsert as aa,
6477
- stampSegmentOnUpdate as ab,
6478
- renumberSegments as ac,
6479
- openSegmentIds as ad,
6480
- isTerminalStatus as ae,
6481
- adoptMessageSegments as af,
6959
+ eventOrigin as a7,
6960
+ activeElementIn as a8,
6961
+ mergeSegmentUpdate as a9,
6962
+ writeStreamedMarkdown as aa,
6963
+ stampSegmentActivity as ab,
6964
+ stampSegmentOnInsert as ac,
6965
+ stampSegmentOnUpdate as ad,
6966
+ renumberSegments as ae,
6967
+ openSegmentIds as af,
6968
+ isTerminalStatus as ag,
6969
+ adoptMessageSegments as ah,
6970
+ APARTE_DERIVED_SEGMENTS as ai,
6971
+ chatBoundaryOf as aj,
6972
+ rootOf as ak,
6482
6973
  APARTE_DEFAULT_BUBBLE_ACTIONS as b,
6483
6974
  APARTE_DEFAULT_HOST_HANDLERS as c,
6484
6975
  APARTE_DEFAULT_ICON_FALLBACKS as d,
@@ -6505,4 +6996,4 @@ export {
6505
6996
  collectRendererStyles as y,
6506
6997
  contentToText as z
6507
6998
  };
6508
- //# sourceMappingURL=dialog-triggers-cYGTlfZf.js.map
6999
+ //# sourceMappingURL=dialog-triggers-CJWw0P3l.js.map