@aparte/core 0.16.8 → 0.16.10

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 (42) hide show
  1. package/dist/client/aparte-client.d.ts +57 -2
  2. package/dist/client/aparte-client.d.ts.map +1 -1
  3. package/dist/client/lifecycle-events.d.ts +8 -1
  4. package/dist/client/lifecycle-events.d.ts.map +1 -1
  5. package/dist/client/stream-adapter.d.ts +8 -0
  6. package/dist/client/stream-adapter.d.ts.map +1 -1
  7. package/dist/components/bubble/aparte-chat-bubble.d.ts +6 -0
  8. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  9. package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
  10. package/dist/components/composer/aparte-composer-input.d.ts +1 -1
  11. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  12. package/dist/components/conversation-list/aparte-conversation-list.d.ts +5 -0
  13. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  14. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts +91 -10
  15. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -1
  16. package/dist/components/status/aparte-chat-status.d.ts +41 -3
  17. package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
  18. package/dist/components/viewport/aparte-chat-viewport.d.ts +8 -1
  19. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  20. package/dist/conversations/conversation-manager.d.ts +29 -0
  21. package/dist/conversations/conversation-manager.d.ts.map +1 -1
  22. package/dist/conversations/index.d.ts +1 -1
  23. package/dist/conversations/index.d.ts.map +1 -1
  24. package/dist/custom-elements.json +10916 -10397
  25. package/dist/{dialog-triggers-D8wNICrT.js → dialog-triggers-cYGTlfZf.js} +128 -30
  26. package/dist/{dialog-triggers-D8wNICrT.js.map → dialog-triggers-cYGTlfZf.js.map} +1 -1
  27. package/dist/elicitation/panel.d.ts.map +1 -1
  28. package/dist/index.css +162 -40
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +447 -94
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.node.d.ts +1 -1
  34. package/dist/index.node.d.ts.map +1 -1
  35. package/dist/index.node.js +1 -1
  36. package/dist/index.node.js.map +1 -1
  37. package/dist/interop/element-props.d.ts.map +1 -1
  38. package/dist/primitives/select/aparte-select.d.ts +25 -3
  39. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  40. package/dist/types/events.d.ts +4 -2
  41. package/dist/types/events.d.ts.map +1 -1
  42. package/package.json +2 -2
@@ -3151,9 +3151,24 @@ class AparteConversationManager {
3151
3151
  _listeners = /* @__PURE__ */ new Set();
3152
3152
  _initialized = false;
3153
3153
  _retention = null;
3154
+ _titleProvider = null;
3154
3155
  constructor(adapter, options) {
3155
3156
  this._adapter = adapter;
3156
3157
  this._retention = options?.retention ?? null;
3158
+ this._titleProvider = options?.titleProvider ?? null;
3159
+ }
3160
+ // ─── Title provider ─────────────────────────────────────────────────────
3161
+ /**
3162
+ * Replace how a new conversation is titled from its first user message.
3163
+ * `null` restores the default (the message's text). The provider is consulted
3164
+ * once per conversation, on that first message; `updateTitle` is untouched.
3165
+ */
3166
+ setTitleProvider(provider) {
3167
+ this._titleProvider = provider;
3168
+ }
3169
+ /** The registered title provider, or `null` when the default applies. */
3170
+ getTitleProvider() {
3171
+ return this._titleProvider;
3157
3172
  }
3158
3173
  // ─── Initialisation ────────────────────────────────────────────────────
3159
3174
  /** Load all conversations from the adapter. Call once at app startup. */
@@ -3229,7 +3244,7 @@ class AparteConversationManager {
3229
3244
  ...conv,
3230
3245
  messages: [...conv.messages, msg],
3231
3246
  updatedAt: Date.now(),
3232
- title: isFirstUserMsg ? this._autoTitle(msg) : conv.title
3247
+ title: isFirstUserMsg ? await this._title(msg) : conv.title
3233
3248
  };
3234
3249
  this._replace(updated);
3235
3250
  await this._adapter.save(updated);
@@ -3362,10 +3377,23 @@ class AparteConversationManager {
3362
3377
  }
3363
3378
  });
3364
3379
  }
3380
+ /**
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.
3383
+ */
3384
+ async _title(msg) {
3385
+ const fallback = this._autoTitle(msg);
3386
+ if (!this._titleProvider) return fallback;
3387
+ try {
3388
+ const title = (await this._titleProvider((msg.content ?? "").trim(), msg)).trim();
3389
+ return title || fallback;
3390
+ } catch (err) {
3391
+ console.warn("[AparteConversationManager] the title provider threw; the default title stands:", err);
3392
+ return fallback;
3393
+ }
3394
+ }
3365
3395
  _autoTitle(msg) {
3366
- const raw = msg.content;
3367
- const text = typeof raw === "string" ? raw : (raw ?? "").toString();
3368
- return text.trim() || "New Chat";
3396
+ return (msg.content ?? "").trim() || "New Chat";
3369
3397
  }
3370
3398
  }
3371
3399
  function filesToAttachments(files) {
@@ -4385,13 +4413,13 @@ class AparteChatHost {
4385
4413
  });
4386
4414
  }
4387
4415
  }
4388
- function dispatchLifecycleEvent(target, name, detail) {
4389
- const id = target.id || void 0;
4416
+ function dispatchLifecycleEvent(target, name, detail, targetId) {
4417
+ const id = targetId ?? (target.id || void 0);
4390
4418
  const stamped = detail && typeof detail === "object" ? { targetId: id, ...detail } : detail;
4391
4419
  target.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail: stamped }));
4392
4420
  }
4393
4421
  function createStreamAdapter(opts) {
4394
- const { target, config, messageId } = opts;
4422
+ const { target, config, messageId, targetId } = opts;
4395
4423
  let parser = new AparteStreamParser({ blocks: config.getStreamBlocks() });
4396
4424
  let streaming = /* @__PURE__ */ new Set();
4397
4425
  let thinkingId = null;
@@ -4479,7 +4507,7 @@ function createStreamAdapter(opts) {
4479
4507
  }
4480
4508
  case "tool-awaiting-approval":
4481
4509
  target.updateSegment?.(`tool-${e.toolCallId}`, { status: "awaiting-approval" });
4482
- dispatchLifecycleEvent(target, "aparte-tool-approval-request", { toolCallId: e.toolCallId, toolName: e.name, input: e.input });
4510
+ dispatchLifecycleEvent(target, "aparte-tool-approval-request", { toolCallId: e.toolCallId, toolName: e.name, input: e.input }, targetId);
4483
4511
  break;
4484
4512
  case "tool-approved":
4485
4513
  target.updateSegment?.(`tool-${e.toolCallId}`, { status: "pending" });
@@ -4513,7 +4541,7 @@ function createStreamAdapter(opts) {
4513
4541
  }
4514
4542
  break;
4515
4543
  case "run-aborted":
4516
- dispatchLifecycleEvent(target, "aparte-message-aborted", { messageId });
4544
+ dispatchLifecycleEvent(target, "aparte-message-aborted", { messageId }, targetId);
4517
4545
  break;
4518
4546
  case "run-done":
4519
4547
  target.updateMessage?.(messageId, { status: "completed" });
@@ -4690,12 +4718,20 @@ function buildEnumField(field, onChange, fallbackLabel, settle) {
4690
4718
  wrap.appendChild(list);
4691
4719
  const controls = () => Array.from(list.querySelectorAll(".aparte-elic-control"));
4692
4720
  const other = () => list.querySelector('input[value="__other__"]');
4721
+ let arrowed = false;
4722
+ list.addEventListener("keydown", (e) => {
4723
+ arrowed = e.key.startsWith("Arrow");
4724
+ });
4725
+ list.addEventListener("click", () => {
4726
+ arrowed = false;
4727
+ });
4693
4728
  list.addEventListener("change", () => {
4694
4729
  const o = other();
4695
4730
  if (o && otherText) {
4696
4731
  otherText.style.display = o.checked ? "" : "none";
4697
- if (o.checked) otherText.focus();
4732
+ if (o.checked && !arrowed) otherText.focus();
4698
4733
  }
4734
+ arrowed = false;
4699
4735
  onChange();
4700
4736
  });
4701
4737
  otherText?.addEventListener("input", onChange);
@@ -5313,6 +5349,7 @@ class AparteClient {
5313
5349
  * sequence, so it can't drift between the three entry points.
5314
5350
  */
5315
5351
  async _streamTurn(targetElement, messageId, provider, messages, modelId, authConfig, opts) {
5352
+ const chatId = this._targetIdOf(targetElement);
5316
5353
  const registeredTools = this._toolsForCurrentModel();
5317
5354
  let baseRequest = {
5318
5355
  messages,
@@ -5328,22 +5365,22 @@ class AparteClient {
5328
5365
  baseRequest = { ...baseRequest, tools: void 0 };
5329
5366
  }
5330
5367
  if (this._isAborted) {
5331
- dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
5368
+ dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId }, chatId);
5332
5369
  this._updateMessage(targetElement, messageId, { status: "completed" });
5333
5370
  return;
5334
5371
  }
5335
- dispatchLifecycleEvent(targetElement, "aparte-message-start", { messageId, role: "assistant" });
5372
+ dispatchLifecycleEvent(targetElement, "aparte-message-start", { messageId, role: "assistant" }, chatId);
5336
5373
  const streamController = new AbortController();
5337
5374
  try {
5338
5375
  const usage = await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController);
5339
5376
  if (this._isAborted || streamController.signal.aborted) {
5340
5377
  this._updateMessage(targetElement, messageId, { status: "completed" });
5341
5378
  } else {
5342
- dispatchLifecycleEvent(targetElement, "aparte-message-done", { messageId, role: "assistant", usage });
5379
+ dispatchLifecycleEvent(targetElement, "aparte-message-done", { messageId, role: "assistant", usage }, chatId);
5343
5380
  }
5344
5381
  } catch (error) {
5345
5382
  if (this._isAborted || streamController.signal.aborted) {
5346
- dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
5383
+ dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId }, chatId);
5347
5384
  this._updateMessage(targetElement, messageId, { status: "completed" });
5348
5385
  return;
5349
5386
  }
@@ -5531,13 +5568,64 @@ class AparteClient {
5531
5568
  return null;
5532
5569
  }
5533
5570
  /**
5534
- * Convert AparteMessage[] to AparteChatMessage[] for re-submission.
5571
+ * Which chat a render target BELONGS to — the id the lifecycle events must carry.
5572
+ *
5573
+ * Not `target.id`: the target is whatever renders, and an `<aparte-chat>` shell
5574
+ * delegates that to its `.viewport`, which has no id. So every lifecycle event on
5575
+ * a shell-shaped chat went out unstamped, and the receive side reads a missing id
5576
+ * as "for every chat on the page" — deliberately, so a single-chat page needs no
5577
+ * wiring. On a two-chat page one chat's turn therefore drove every composer.
5578
+ *
5579
+ * The climb is the same rule `aparte-composer._ownTargetId` uses on the receive
5580
+ * side, and it has to be, or the two would disagree about which chat is which:
5581
+ * the nearest `aparte-chat` / `[data-aparte-chat]` ancestor (or the element
5582
+ * itself) that HAS an id. Deliberately not `chatBoundaryOf` — that returns the
5583
+ * boundary whether or not it is identified, and an id-less boundary must fall
5584
+ * through to the next one up rather than answer "no id".
5585
+ */
5586
+ _targetIdOf(el2) {
5587
+ let node = el2 ?? null;
5588
+ while (node) {
5589
+ const tag = node.tagName?.toLowerCase();
5590
+ const isHost = tag === "aparte-chat" || node.hasAttribute?.("data-aparte-chat");
5591
+ if (isHost && node.id) return node.id;
5592
+ node = node.parentElement;
5593
+ }
5594
+ return el2?.id || void 0;
5595
+ }
5596
+ /**
5597
+ * Convert AparteMessage[] to AparteChatMessage[] for re-submission (retry, edit).
5598
+ *
5599
+ * The SLICE is each caller's own business — retry cuts before the reply it is
5600
+ * regenerating, edit cuts after the message being reworded, send cuts at the last
5601
+ * answered turn. What a message CONTRIBUTES to the wire is not: send, retry and
5602
+ * edit must not disagree about it, and they did. This one had no filter at all,
5603
+ * so a failed turn and a turn stopped before its first token both went out as
5604
+ * `{ role: 'assistant', content: '' }` — a claim the assistant said nothing, which
5605
+ * some providers reject outright and the rest read as an empty reply worth
5606
+ * imitating.
5535
5607
  */
5536
5608
  _messagesToChatMessages(messages) {
5537
- return messages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({
5609
+ return this._wireHistory(messages);
5610
+ }
5611
+ /**
5612
+ * The one rule for what a message contributes to the wire.
5613
+ *
5614
+ * Not the slice — the contribution: a role the wire has (`user`/`assistant`), a
5615
+ * turn that neither failed nor is still in flight, and text that is not empty.
5616
+ *
5617
+ * `_wireText` (not `m.content`): assistant replies stream their text into
5618
+ * `segments`, leaving `content` as '', so without flattening every reply would be
5619
+ * empty here and the drop below would swallow the whole transcript.
5620
+ */
5621
+ _wireHistory(messages) {
5622
+ return messages.filter((m) => {
5623
+ if (m.role !== "user" && m.role !== "assistant") return false;
5624
+ return m.status !== "error" && !AparteClient._inFlight(m);
5625
+ }).map((m) => ({
5538
5626
  role: m.role,
5539
5627
  content: this._wireText(m)
5540
- }));
5628
+ })).filter((m) => m.content.length > 0);
5541
5629
  }
5542
5630
  /** The "no model selected" warning is said once per client, not once per dropped send. */
5543
5631
  _warnedNoModel = false;
@@ -5671,22 +5759,30 @@ class AparteClient {
5671
5759
  messages.push({ role: "user", content: userMsg });
5672
5760
  return messages;
5673
5761
  }
5762
+ /**
5763
+ * A turn that has not finished yet. `status` is OPTIONAL on `AparteMessage`, so
5764
+ * the question the history gates can ask is "is this still in flight?", never
5765
+ * "is this completed?".
5766
+ *
5767
+ * The gates below used to ask the second one, and a host that SEEDS a transcript
5768
+ * — restoring a saved conversation, hydrating a server-rendered one — has no
5769
+ * reason to invent a status for turns that are already over. With none, the
5770
+ * cutoff never advanced past 0 and the whole transcript was sliced away: the
5771
+ * model got the new question with no context, and nothing in the UI said so,
5772
+ * because the viewport still showed every message.
5773
+ */
5774
+ static _inFlight(m) {
5775
+ return m.status === "streaming" || m.status === "pending";
5776
+ }
5674
5777
  _toHistoryMessages(messages) {
5675
5778
  let cutoff = 0;
5676
5779
  for (let i = 0; i < messages.length; i++) {
5677
5780
  const m = messages[i];
5678
- if (m.role === "assistant" && m.status === "completed") {
5781
+ if (m.role === "assistant" && m.status !== "error" && !AparteClient._inFlight(m)) {
5679
5782
  cutoff = i + 1;
5680
5783
  }
5681
5784
  }
5682
- return messages.slice(0, cutoff).filter((m) => {
5683
- if (m.role === "user") return m.status !== "error";
5684
- if (m.role === "assistant") return m.status === "completed";
5685
- return false;
5686
- }).map((m) => ({
5687
- role: m.role,
5688
- content: this._wireText(m)
5689
- })).filter((m) => m.content.length > 0);
5785
+ return this._wireHistory(messages.slice(0, cutoff));
5690
5786
  }
5691
5787
  /**
5692
5788
  * A message's text as the MODEL should read it — the one mapper both history
@@ -5859,10 +5955,12 @@ ${content}
5859
5955
  */
5860
5956
  async _runViaStreamRunner(streamRunner, targetElement, messageId, provider, baseRequest, authConfig, streamController) {
5861
5957
  const signal = streamController.signal;
5958
+ const chatId = this._targetIdOf(targetElement);
5862
5959
  const adapter = createStreamAdapter({
5863
5960
  target: targetElement,
5864
5961
  config: this._config,
5865
- messageId
5962
+ messageId,
5963
+ ...chatId ? { targetId: chatId } : {}
5866
5964
  });
5867
5965
  const prefixSegments = baseRequest._meta?.["prefixSegments"];
5868
5966
  const emitter = (event) => {
@@ -5955,7 +6053,7 @@ ${content}
5955
6053
  segments: [errorSegment]
5956
6054
  });
5957
6055
  }
5958
- dispatchLifecycleEvent(target, "aparte-message-error", { messageId, error });
6056
+ dispatchLifecycleEvent(target, "aparte-message-error", { messageId, error }, this._targetIdOf(target));
5959
6057
  }
5960
6058
  }
5961
6059
  function findHead(node) {
@@ -6269,7 +6367,7 @@ function applyElementProps(el2, props, transformValue = (value) => value) {
6269
6367
  if (value === null || value === void 0) el2.style.removeProperty(key);
6270
6368
  else if (typeof value === "object" || typeof value === "function") el2.style.removeProperty(key);
6271
6369
  else el2.style.setProperty(key, String(value));
6272
- } else if (key.startsWith("on")) {
6370
+ } else if (key.toLowerCase().startsWith("on")) {
6273
6371
  el2.removeAttribute(key);
6274
6372
  } else if (value === null || value === void 0 || value === false) {
6275
6373
  el2.removeAttribute(key);
@@ -6407,4 +6505,4 @@ export {
6407
6505
  collectRendererStyles as y,
6408
6506
  contentToText as z
6409
6507
  };
6410
- //# sourceMappingURL=dialog-triggers-D8wNICrT.js.map
6508
+ //# sourceMappingURL=dialog-triggers-cYGTlfZf.js.map