@aparte/core 0.2.0-alpha.0 → 0.4.0-alpha.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 (42) hide show
  1. package/README.md +21 -0
  2. package/dist/client/aparte-client.d.ts +21 -0
  3. package/dist/client/aparte-client.d.ts.map +1 -1
  4. package/dist/components/bubble/aparte-chat-bubble.d.ts +11 -0
  5. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  6. package/dist/components/chat/aparte-chat.d.ts +13 -0
  7. package/dist/components/chat/aparte-chat.d.ts.map +1 -1
  8. package/dist/components/viewport/aparte-chat-viewport.d.ts +13 -2
  9. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  10. package/dist/config/aparte-config.d.ts +16 -2
  11. package/dist/config/aparte-config.d.ts.map +1 -1
  12. package/dist/config/icon-provider.d.ts +21 -21
  13. package/dist/config/icon-provider.d.ts.map +1 -1
  14. package/dist/conversations/conversation-controller.d.ts.map +1 -1
  15. package/dist/custom-elements.json +3658 -3385
  16. package/dist/host/aparte-chat-host.d.ts +47 -1
  17. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  18. package/dist/{index-D89k-n2Q.js → index-BBg6dsYx.js} +193 -43
  19. package/dist/index-BBg6dsYx.js.map +1 -0
  20. package/dist/index.css +60 -6
  21. package/dist/index.d.ts +10 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +123 -26
  24. package/dist/index.js.map +1 -1
  25. package/dist/index.node.d.ts +2 -0
  26. package/dist/index.node.d.ts.map +1 -1
  27. package/dist/index.node.js +16 -14
  28. package/dist/index.node.js.map +1 -1
  29. package/dist/primitives/select/aparte-optgroup.d.ts +2 -0
  30. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  31. package/dist/primitives/select/aparte-select.d.ts +2 -0
  32. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  33. package/dist/types/imperative-api.d.ts +27 -0
  34. package/dist/types/imperative-api.d.ts.map +1 -1
  35. package/dist/types/model-provider.d.ts +9 -3
  36. package/dist/types/model-provider.d.ts.map +1 -1
  37. package/dist/utils/files-to-attachments.d.ts +27 -0
  38. package/dist/utils/files-to-attachments.d.ts.map +1 -0
  39. package/dist/utils/is-awaiting-reply.d.ts +22 -0
  40. package/dist/utils/is-awaiting-reply.d.ts.map +1 -0
  41. package/package.json +8 -1
  42. package/dist/index-D89k-n2Q.js.map +0 -1
@@ -78,6 +78,12 @@ export declare class AparteChatHost {
78
78
  private _pendingSiblings?;
79
79
  /** Abort handle for an in-flight {@link streamTokens} loop. */
80
80
  private _streamAbort?;
81
+ /** Segment chunks written to the bubble, awaiting one coalesced state sync. */
82
+ private readonly _segmentBuffer;
83
+ /** {@link streamTokens} text written to the bubble, awaiting the same sync. */
84
+ private _tokenBuffer;
85
+ /** Handle of the scheduled coalesced sync, or null when none is pending. */
86
+ private _flushHandle;
81
87
  private _conversationId;
82
88
  private _controller?;
83
89
  private _teardown;
@@ -129,8 +135,35 @@ export declare class AparteChatHost {
129
135
  updateSegment(segmentId: string, updates: Partial<AparteSegment>): void;
130
136
  /** Remove a transient segment (e.g. pipeline-waiting indicator). */
131
137
  removeSegment(segmentId: string): void;
132
- /** Append text to a segment's content in the last message. */
138
+ /**
139
+ * Append text to a segment's content in the last message — the streaming path
140
+ * for thinking blocks, tool pills and any segment that grows token by token.
141
+ *
142
+ * Two writes, deliberately asymmetric:
143
+ * 1. straight into the bubble, every chunk, so the text appears immediately —
144
+ * the same direct path `appendToken` uses;
145
+ * 2. into the framework's message list, **coalesced to once per frame**.
146
+ *
147
+ * It used to do only (2), synchronously: a full list rebuild plus
148
+ * `setMessages` + `onMessagesChange` for every chunk, i.e. one complete
149
+ * framework render per token. At 60 tok/s from a local model that is unusable,
150
+ * and nothing in the imperative API hinted that `appendToSegment` cost so much
151
+ * more than `injectTokenStream`. Consumers had to write their own rAF batcher.
152
+ *
153
+ * Any structural change flushes the buffer first (see
154
+ * {@link _flushStreamState}), so ordering is never observable.
155
+ */
133
156
  appendToSegment(segmentId: string, content: string): void;
157
+ /** Schedule the coalesced segment-state sync (idempotent within a frame). */
158
+ private _scheduleStreamFlush;
159
+ /**
160
+ * Write every buffered stream chunk — segment chunks and {@link streamTokens}
161
+ * text alike — into the framework's message list in **one** update. Called by
162
+ * the scheduled frame, and eagerly by any mutation that would otherwise observe
163
+ * a stale list (a new segment, a new message, a conversation swap): flushing
164
+ * first keeps the emitted order identical to the call order.
165
+ */
166
+ private _flushStreamState;
134
167
  /** Read the current message list. */
135
168
  getMessages(): AparteMessage[];
136
169
  /** Clear all messages + reset viewport state. */
@@ -148,8 +181,21 @@ export declare class AparteChatHost {
148
181
  * appends each token, completes the message at the end. Per-framework
149
182
  * wrappers adapt their native stream type (RxJS Observable, AsyncIterable,
150
183
  * ReadableStream) into this.
184
+ *
185
+ * Each token reaches the bubble immediately and the framework's message list
186
+ * is synced **once per frame** (same discipline as {@link appendToSegment}),
187
+ * with a guaranteed flush before completion and on abort. Before that sync
188
+ * existed the two disagreed: the DOM had the reply, the framework list still
189
+ * held `content: ''` — so any re-render from state wiped it, and a custom
190
+ * bubble (which renders from that state) showed nothing at all.
151
191
  */
152
192
  streamTokens(messageId: string, tokens: AsyncIterable<string>): Promise<void>;
193
+ /**
194
+ * Buffer a streamed token for the coalesced state sync. A different message id
195
+ * (a second stream started without stopping the first) flushes the previous
196
+ * one, so text never migrates between messages.
197
+ */
198
+ private _bufferToken;
153
199
  /** Abort an in-flight {@link streamTokens} loop. */
154
200
  stopTokenStream(): void;
155
201
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"aparte-chat-host.d.ts","sourceRoot":"","sources":["../../src/host/aparte-chat-host.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"aparte-chat-host.d.ts","sourceRoot":"","sources":["../../src/host/aparte-chat-host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,aAAa,EACb,aAAa,EAIhB,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EACH,4BAA4B,EAE/B,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAyCpE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IAClC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;IAEtC,kDAAkD;IAClD,WAAW,IAAI,aAAa,EAAE,CAAC;IAC/B,qEAAqE;IACrE,WAAW,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC;IAE7C,2EAA2E;IAC3E,gBAAgB,CAAC,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC;IACnD,6EAA6E;IAC7E,iBAAiB,CAAC,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC;IACjD,4CAA4C;IAC5C,cAAc,CAAC,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IACzC,0DAA0D;IAC1D,iBAAiB,CAAC,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IACrD,wEAAwE;IACxE,WAAW,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAClC,wEAAwE;IACxE,aAAa,CAAC,IAAI,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAqB;IAClC,0EAA0E;IAC1E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,8EAA8E;IAC9E,qBAAqB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,cAAc;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAEhD,wEAAwE;IACxE,OAAO,CAAC,YAAY,CAAuB;IAC3C,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,6EAA6E;IAC7E,OAAO,CAAC,gBAAgB,CAAC,CAAsB;IAC/C,+DAA+D;IAC/D,OAAO,CAAC,YAAY,CAAC,CAAkB;IACvC,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,+EAA+E;IAC/E,OAAO,CAAC,YAAY,CAAoD;IACxE,4EAA4E;IAC5E,OAAO,CAAC,YAAY,CAAuB;IAE3C,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,WAAW,CAAC,CAA+B;IACnD,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,MAAM,CAAS;gBAEX,OAAO,EAAE,qBAAqB,EAAE,OAAO,GAAE,qBAA0B;IAQ/E;;;;OAIG;IACH,IAAI,IAAI,MAAM,IAAI;IA0BlB,MAAM,IAAI,IAAI;IAed,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAA8B;IAC9D,IAAI,WAAW,IAAI,OAAO,CAAuC;IAEjE,gFAAgF;IAChF,IAAI,sBAAsB,IAAI,4BAA4B,GAAG,SAAS,CAErE;IAED,qFAAqF;IACrF,gBAAgB,IAAI,IAAI;IAIxB,OAAO,CAAC,eAAe;IAQvB,gEAAgE;IAChE,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnD,OAAO,CAAC,iBAAiB,CAAC,CAAa;IAEvC,OAAO,CAAC,iBAAiB;IAoCzB,yEAAyE;IACzE,OAAO,CAAC,sBAAsB;IAe9B;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAO3C,gDAAgD;IAChD,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI;IAevE,gEAAgE;IAChE,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;IAYxE,yEAAyE;IACzE,UAAU,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAcxC,qDAAqD;IACrD,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI;IAevE,oEAAoE;IACpE,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAatC;;;;;;;;;;;;;;;;;OAiBG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAczD,6EAA6E;IAC7E,OAAO,CAAC,oBAAoB;IAa5B;;;;;;OAMG;IACH,OAAO,CAAC,iBAAiB;IAgDzB,qCAAqC;IACrC,WAAW,IAAI,aAAa,EAAE;IAE9B,iDAAiD;IACjD,aAAa,IAAI,IAAI;IAMrB,0EAA0E;IAC1E,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAKpC,iEAAiE;IACjE,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI;IAK1E,wDAAwD;IACxD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IASrC,6EAA6E;IAC7E,sBAAsB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI;IAWnD;;;;;;;;;;;;OAYG;IACG,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAwCnF;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAQpB,oDAAoD;IACpD,eAAe,IAAI,IAAI;IAavB;;;;OAIG;IACH,WAAW,IAAI,IAAI;IAYnB;;;OAGG;IACH,oBAAoB,IAAI,IAAI;IAiB5B,OAAO,CAAC,qBAAqB;IAe7B,4EAA4E;IAC5E,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,mBAAmB;IAiB3B,OAAO,CAAC,0BAA0B;IAmClC,OAAO,CAAC,yBAAyB;CAqCpC"}
@@ -1165,13 +1165,24 @@ class AparteConfigClass {
1165
1165
  return this._artifactPreviewBuilder;
1166
1166
  }
1167
1167
  /**
1168
- * Get the current icon provider, or a proxy that falls back to DEFAULT_ICON_FALLBACKS
1168
+ * The icon set as a **complete** provider: every name resolves, falling back
1169
+ * to `DEFAULT_ICON_FALLBACKS` for anything the registered provider doesn't
1170
+ * implement. Callers (bubble action bar, composer controls) can therefore
1171
+ * invoke `icons.copy()` unconditionally.
1172
+ *
1173
+ * It used to hand back the registered provider verbatim, so a provider that
1174
+ * implemented a subset — which `getIcon()` has always supported, and which
1175
+ * the type now states — crashed every other icon with
1176
+ * "icons.retry is not a function".
1169
1177
  */
1170
1178
  getIconProvider() {
1171
- if (this._iconProvider) return this._iconProvider;
1172
- return Object.fromEntries(
1173
- Object.entries(DEFAULT_ICON_FALLBACKS).map(([k, v]) => [k, () => v])
1174
- );
1179
+ const registered = this._iconProvider;
1180
+ const complete = {};
1181
+ for (const name of Object.keys(DEFAULT_ICON_FALLBACKS)) {
1182
+ const fn = registered?.[name];
1183
+ complete[name] = fn ?? (() => DEFAULT_ICON_FALLBACKS[name]);
1184
+ }
1185
+ return complete;
1175
1186
  }
1176
1187
  /**
1177
1188
  * Set a custom avatar renderer. Lets framework consumers (Angular,
@@ -1267,6 +1278,15 @@ class AparteConfigClass {
1267
1278
  this._locale = locale;
1268
1279
  this._notify();
1269
1280
  }
1281
+ /**
1282
+ * Restore the built-in English locale (the same `DEFAULT_LOCALE` core ships
1283
+ * with). Counterpart of {@link setLocale} for language toggles — avoids
1284
+ * having to import `DEFAULT_LOCALE` yourself.
1285
+ */
1286
+ resetLocale() {
1287
+ this._locale = DEFAULT_LOCALE;
1288
+ this._notify();
1289
+ }
1270
1290
  /**
1271
1291
  * Get the current locale
1272
1292
  */
@@ -1287,9 +1307,8 @@ class AparteConfigClass {
1287
1307
  * Falls back to textual representation if no provider is set
1288
1308
  */
1289
1309
  getIcon(name) {
1290
- if (this._iconProvider && this._iconProvider[name]) {
1291
- return this._iconProvider[name]();
1292
- }
1310
+ const icon = this._iconProvider?.[name];
1311
+ if (icon) return icon();
1293
1312
  return DEFAULT_ICON_FALLBACKS[name];
1294
1313
  }
1295
1314
  /**
@@ -1480,7 +1499,12 @@ class AparteConfigClass {
1480
1499
  const provider = this._aiProviders.get(defaultProvider);
1481
1500
  if (!provider) return void 0;
1482
1501
  const models = provider.getModels();
1483
- if (models instanceof Promise) return void 0;
1502
+ if (models instanceof Promise) {
1503
+ console.warn(
1504
+ `[AparteConfig] Provider "${defaultProvider}".getModels() returned a Promise — it is ignored here, so model capabilities (e.g. function_calling) resolve to none and tools are disabled. getModels() must return the list synchronously; implement fetchModels() for async fetching.`
1505
+ );
1506
+ return void 0;
1507
+ }
1484
1508
  return models.find((m) => m.id === defaultModel);
1485
1509
  }
1486
1510
  // ─────────────────────────────────────────────────────────────────────────
@@ -3305,6 +3329,19 @@ class ConversationManager {
3305
3329
  return text.trim() || "New Chat";
3306
3330
  }
3307
3331
  }
3332
+ function attachmentId() {
3333
+ return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
3334
+ }
3335
+ function filesToAttachments(files) {
3336
+ return files.map((file) => ({
3337
+ id: attachmentId(),
3338
+ name: file.name,
3339
+ type: file.type || "application/octet-stream",
3340
+ url: URL.createObjectURL(file),
3341
+ size: file.size,
3342
+ blob: file
3343
+ }));
3344
+ }
3308
3345
  class AparteConversationController {
3309
3346
  _binding;
3310
3347
  _options;
@@ -3349,16 +3386,7 @@ class AparteConversationController {
3349
3386
  if (targetId && targetId !== this._binding.hostId) return;
3350
3387
  const content = evt.detail?.content ?? "";
3351
3388
  const files = evt.detail?.files;
3352
- const attachments = files?.length ? files.map((f) => ({
3353
- id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`,
3354
- name: f.name,
3355
- type: f.type || "application/octet-stream",
3356
- url: URL.createObjectURL(f),
3357
- size: f.size,
3358
- // Carry the raw File so the storage adapter can persist it
3359
- // to its attachments table; reload reconstructs `url`.
3360
- blob: f
3361
- })) : void 0;
3389
+ const attachments = files?.length ? filesToAttachments(files) : void 0;
3362
3390
  const userMsg = {
3363
3391
  id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`,
3364
3392
  role: "user",
@@ -3608,6 +3636,12 @@ class AparteChatHost {
3608
3636
  _pendingSiblings;
3609
3637
  /** Abort handle for an in-flight {@link streamTokens} loop. */
3610
3638
  _streamAbort;
3639
+ /** Segment chunks written to the bubble, awaiting one coalesced state sync. */
3640
+ _segmentBuffer = /* @__PURE__ */ new Map();
3641
+ /** {@link streamTokens} text written to the bubble, awaiting the same sync. */
3642
+ _tokenBuffer = null;
3643
+ /** Handle of the scheduled coalesced sync, or null when none is pending. */
3644
+ _flushHandle = null;
3611
3645
  _conversationId = null;
3612
3646
  _controller;
3613
3647
  _teardown = [];
@@ -3720,6 +3754,9 @@ class AparteChatHost {
3720
3754
  }
3721
3755
  /** Drop transient streaming state so a new conversation starts clean. */
3722
3756
  _beginConversationSwap() {
3757
+ this._segmentBuffer.clear();
3758
+ this._tokenBuffer = null;
3759
+ this._flushStreamState();
3723
3760
  this.stopTokenStream();
3724
3761
  this._setStreamingId(null);
3725
3762
  this.binding.onTypingChange?.(false);
@@ -3741,12 +3778,14 @@ class AparteChatHost {
3741
3778
  * is the append-specific signal instead.
3742
3779
  */
3743
3780
  appendMessage(message) {
3781
+ this._flushStreamState();
3744
3782
  if (message.role === "user") this._vp()?.setAutoScroll?.(true);
3745
3783
  this.binding.setMessages([...this.binding.getMessages(), message]);
3746
3784
  this.binding.onMessageAppended?.(message);
3747
3785
  }
3748
3786
  /** Atomic partial update of a message by id. */
3749
3787
  updateMessage(messageId, updates) {
3788
+ this._flushStreamState();
3750
3789
  const msgs = this.binding.getMessages();
3751
3790
  const index = msgs.findIndex((m) => m.id === messageId);
3752
3791
  if (index === -1) return;
@@ -3772,6 +3811,7 @@ class AparteChatHost {
3772
3811
  }
3773
3812
  /** Add a segment to the last message (immediate bubble + state sync). */
3774
3813
  addSegment(segment) {
3814
+ this._flushStreamState();
3775
3815
  const msgs = this.binding.getMessages();
3776
3816
  const last = msgs[msgs.length - 1];
3777
3817
  if (last && this._isOrphan(last.id)) return;
@@ -3785,6 +3825,7 @@ class AparteChatHost {
3785
3825
  }
3786
3826
  /** Update a segment in the last message in place. */
3787
3827
  updateSegment(segmentId, updates) {
3828
+ this._flushStreamState();
3788
3829
  const msgs = this.binding.getMessages();
3789
3830
  const last = msgs[msgs.length - 1];
3790
3831
  if (last && this._isOrphan(last.id)) return;
@@ -3799,6 +3840,7 @@ class AparteChatHost {
3799
3840
  }
3800
3841
  /** Remove a transient segment (e.g. pipeline-waiting indicator). */
3801
3842
  removeSegment(segmentId) {
3843
+ this._flushStreamState();
3802
3844
  this._lastBubble()?.removeSegment?.(segmentId);
3803
3845
  const msgs = this.binding.getMessages();
3804
3846
  const last = msgs[msgs.length - 1];
@@ -3809,18 +3851,85 @@ class AparteChatHost {
3809
3851
  this.binding.setMessages(next);
3810
3852
  this.binding.onMessagesChange?.(next);
3811
3853
  }
3812
- /** Append text to a segment's content in the last message. */
3854
+ /**
3855
+ * Append text to a segment's content in the last message — the streaming path
3856
+ * for thinking blocks, tool pills and any segment that grows token by token.
3857
+ *
3858
+ * Two writes, deliberately asymmetric:
3859
+ * 1. straight into the bubble, every chunk, so the text appears immediately —
3860
+ * the same direct path `appendToken` uses;
3861
+ * 2. into the framework's message list, **coalesced to once per frame**.
3862
+ *
3863
+ * It used to do only (2), synchronously: a full list rebuild plus
3864
+ * `setMessages` + `onMessagesChange` for every chunk, i.e. one complete
3865
+ * framework render per token. At 60 tok/s from a local model that is unusable,
3866
+ * and nothing in the imperative API hinted that `appendToSegment` cost so much
3867
+ * more than `injectTokenStream`. Consumers had to write their own rAF batcher.
3868
+ *
3869
+ * Any structural change flushes the buffer first (see
3870
+ * {@link _flushStreamState}), so ordering is never observable.
3871
+ */
3813
3872
  appendToSegment(segmentId, content) {
3814
3873
  const msgs = this.binding.getMessages();
3815
3874
  const last = msgs[msgs.length - 1];
3816
3875
  if (!last || !last.segments) return;
3817
3876
  if (this._isOrphan(last.id)) return;
3818
- const segments = last.segments.map(
3819
- (s) => s.id === segmentId && "content" in s ? { ...s, content: s.content + content } : s
3820
- );
3821
- const next = [...msgs.slice(0, -1), { ...last, segments }];
3822
- this.binding.setMessages(next);
3823
- this.binding.onMessagesChange?.(next);
3877
+ this._vp()?.appendToSegment?.(last.id, segmentId, content);
3878
+ this._segmentBuffer.set(segmentId, (this._segmentBuffer.get(segmentId) ?? "") + content);
3879
+ this._scheduleStreamFlush();
3880
+ }
3881
+ /** Schedule the coalesced segment-state sync (idempotent within a frame). */
3882
+ _scheduleStreamFlush() {
3883
+ if (this._flushHandle !== null) return;
3884
+ const run = () => {
3885
+ this._flushHandle = null;
3886
+ this._flushStreamState();
3887
+ };
3888
+ this._flushHandle = typeof requestAnimationFrame === "function" ? requestAnimationFrame(run) : setTimeout(run, 0);
3889
+ }
3890
+ /**
3891
+ * Write every buffered stream chunk — segment chunks and {@link streamTokens}
3892
+ * text alike — into the framework's message list in **one** update. Called by
3893
+ * the scheduled frame, and eagerly by any mutation that would otherwise observe
3894
+ * a stale list (a new segment, a new message, a conversation swap): flushing
3895
+ * first keeps the emitted order identical to the call order.
3896
+ */
3897
+ _flushStreamState() {
3898
+ if (this._flushHandle !== null) {
3899
+ if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(this._flushHandle);
3900
+ else clearTimeout(this._flushHandle);
3901
+ this._flushHandle = null;
3902
+ }
3903
+ const tokens = this._tokenBuffer;
3904
+ const pending = this._segmentBuffer.size > 0 ? new Map(this._segmentBuffer) : null;
3905
+ if (!tokens && !pending) return;
3906
+ this._tokenBuffer = null;
3907
+ this._segmentBuffer.clear();
3908
+ let msgs = this.binding.getMessages();
3909
+ let changed = false;
3910
+ if (tokens) {
3911
+ const index = msgs.findIndex((m) => m.id === tokens.messageId);
3912
+ if (index !== -1) {
3913
+ const next = [...msgs];
3914
+ next[index] = { ...next[index], content: (next[index].content ?? "") + tokens.text };
3915
+ msgs = next;
3916
+ changed = true;
3917
+ }
3918
+ }
3919
+ if (pending) {
3920
+ const last = msgs[msgs.length - 1];
3921
+ if (last?.segments) {
3922
+ const segments = last.segments.map((s) => {
3923
+ const chunk = pending.get(s.id);
3924
+ return chunk !== void 0 && "content" in s ? { ...s, content: s.content + chunk } : s;
3925
+ });
3926
+ msgs = [...msgs.slice(0, -1), { ...last, segments }];
3927
+ changed = true;
3928
+ }
3929
+ }
3930
+ if (!changed) return;
3931
+ this.binding.setMessages(msgs);
3932
+ this.binding.onMessagesChange?.(msgs);
3824
3933
  }
3825
3934
  /** Read the current message list. */
3826
3935
  getMessages() {
@@ -3866,6 +3975,13 @@ class AparteChatHost {
3866
3975
  * appends each token, completes the message at the end. Per-framework
3867
3976
  * wrappers adapt their native stream type (RxJS Observable, AsyncIterable,
3868
3977
  * ReadableStream) into this.
3978
+ *
3979
+ * Each token reaches the bubble immediately and the framework's message list
3980
+ * is synced **once per frame** (same discipline as {@link appendToSegment}),
3981
+ * with a guaranteed flush before completion and on abort. Before that sync
3982
+ * existed the two disagreed: the DOM had the reply, the framework list still
3983
+ * held `content: ''` — so any re-render from state wiped it, and a custom
3984
+ * bubble (which renders from that state) showed nothing at all.
3869
3985
  */
3870
3986
  async streamTokens(messageId, tokens) {
3871
3987
  this.stopTokenStream();
@@ -3885,10 +4001,13 @@ class AparteChatHost {
3885
4001
  const res = await Promise.race([iterator.next(), aborted]);
3886
4002
  if (res === "aborted" || ac.signal.aborted || res.done) break;
3887
4003
  this._vp()?.appendToken?.(messageId, res.value);
4004
+ this._bufferToken(messageId, res.value);
3888
4005
  }
4006
+ this._flushStreamState();
3889
4007
  if (!ac.signal.aborted) this._vp()?.completeMessage?.(messageId);
3890
4008
  this._setStreamingId(null);
3891
4009
  } catch (err) {
4010
+ this._flushStreamState();
3892
4011
  this._setStreamingId(null);
3893
4012
  throw err;
3894
4013
  } finally {
@@ -3901,11 +4020,22 @@ class AparteChatHost {
3901
4020
  if (this._streamAbort === ac) this._streamAbort = void 0;
3902
4021
  }
3903
4022
  }
4023
+ /**
4024
+ * Buffer a streamed token for the coalesced state sync. A different message id
4025
+ * (a second stream started without stopping the first) flushes the previous
4026
+ * one, so text never migrates between messages.
4027
+ */
4028
+ _bufferToken(messageId, chunk) {
4029
+ if (this._tokenBuffer && this._tokenBuffer.messageId !== messageId) this._flushStreamState();
4030
+ this._tokenBuffer = this._tokenBuffer ? { messageId, text: this._tokenBuffer.text + chunk } : { messageId, text: chunk };
4031
+ this._scheduleStreamFlush();
4032
+ }
3904
4033
  /** Abort an in-flight {@link streamTokens} loop. */
3905
4034
  stopTokenStream() {
3906
4035
  if (this._streamAbort) {
3907
4036
  this._streamAbort.abort();
3908
4037
  this._streamAbort = void 0;
4038
+ this._flushStreamState();
3909
4039
  this._setStreamingId(null);
3910
4040
  }
3911
4041
  }
@@ -4850,8 +4980,9 @@ ${summary}`,
4850
4980
  const targetId = event.detail?.targetId;
4851
4981
  if (targetId) {
4852
4982
  const byId = document.getElementById(targetId);
4853
- if (byId && typeof byId.appendMessage === "function") {
4854
- targetElement = byId;
4983
+ const resolved = this._asRenderTarget(byId);
4984
+ if (resolved) {
4985
+ targetElement = resolved;
4855
4986
  } else {
4856
4987
  console.warn("[AparteClient] ⚠️ targetId present but element not found or missing appendMessage:", targetId);
4857
4988
  }
@@ -4918,7 +5049,7 @@ ${summary}`,
4918
5049
  try {
4919
5050
  authConfig = await this._resolveAuth(providerId);
4920
5051
  const rawFiles = Array.isArray(event.detail?.files) ? event.detail.files : [];
4921
- const filesToInject = this.options.rawFileInject === "none" ? [] : this.options.rawFileInject === "images-only" ? rawFiles.filter((f) => f.type.startsWith("image/")) : rawFiles;
5052
+ const filesToInject = this._selectFilesToInject(rawFiles);
4922
5053
  const contentParts = filesToInject.length > 0 ? await this._filesToContentParts(filesToInject) : [];
4923
5054
  messages = this._buildMessages(content, targetElement, contentParts.length > 0 ? contentParts : void 0);
4924
5055
  } catch (error) {
@@ -4976,6 +5107,19 @@ ${summary}`,
4976
5107
  if (!message.segments) return "";
4977
5108
  return message.segments.filter((s) => s.type === "text" || s.type === "code").map((s) => s.content ?? "").join("\n").trim();
4978
5109
  }
5110
+ /**
5111
+ * Which of the send's pending files get inlined into the request: the
5112
+ * `rawFileInject` mode first —
5113
+ * 'none' → nothing inline (RAG handles all file types),
5114
+ * 'images-only' → images inline, text/docs to the app layer,
5115
+ * 'all' (default) → images + text files inline —
5116
+ * then the optional per-file `fileInjectFilter` veto.
5117
+ */
5118
+ _selectFilesToInject(rawFiles) {
5119
+ const byMode = this.options.rawFileInject === "none" ? [] : this.options.rawFileInject === "images-only" ? rawFiles.filter((f) => f.type.startsWith("image/")) : rawFiles;
5120
+ const filter = this.options.fileInjectFilter;
5121
+ return filter ? byMode.filter((f) => filter(f)) : byMode;
5122
+ }
4979
5123
  /**
4980
5124
  * Convert an array of File objects to AparteContentPart[].
4981
5125
  * - Images → AparteImagePart (base64 data URL)
@@ -6066,6 +6210,10 @@ function applyElementProps(el2, props, transformValue = (value) => value) {
6066
6210
  }
6067
6211
  }
6068
6212
  }
6213
+ function isAwaitingReply(message) {
6214
+ if (message.status === "streaming" || message.status === "pending") return true;
6215
+ return message.status === void 0 && message.role === "assistant" && !message.content?.trim() && !message.segments?.length;
6216
+ }
6069
6217
  function el(tag, className, text) {
6070
6218
  const node = document.createElement(tag);
6071
6219
  if (className) node.className = className;
@@ -6252,18 +6400,20 @@ export {
6252
6400
  BackendTransport as B,
6253
6401
  ConversationManager as C,
6254
6402
  DEFAULT_ICON_FALLBACKS as D,
6255
- isSafeUrl as E,
6256
- parseAparteEventStream as F,
6257
- parseMarkdownToSegments as G,
6258
- populateBubbleFromMessage as H,
6259
- readableToAsyncIterable as I,
6260
- registerDefaultRenderers as J,
6261
- registerSegmentRenderer as K,
6262
- requestUserInput as L,
6403
+ isAwaitingReply as E,
6404
+ isFormatAdapter as F,
6405
+ isSafeUrl as G,
6406
+ parseAparteEventStream as H,
6407
+ parseMarkdownToSegments as I,
6408
+ populateBubbleFromMessage as J,
6409
+ readableToAsyncIterable as K,
6410
+ registerDefaultRenderers as L,
6263
6411
  MessageRepository as M,
6264
- resolveConfig as N,
6265
- runWithConfig as O,
6266
- unregisterSegmentRenderer as P,
6412
+ registerSegmentRenderer as N,
6413
+ requestUserInput as O,
6414
+ resolveConfig as P,
6415
+ runWithConfig as Q,
6416
+ unregisterSegmentRenderer as R,
6267
6417
  APARTE_HOST_ATTR as a,
6268
6418
  AparteChatHost as b,
6269
6419
  AparteClient as c,
@@ -6288,7 +6438,7 @@ export {
6288
6438
  defaultSanitizer as v,
6289
6439
  deriveArtifactKind as w,
6290
6440
  detachConfig as x,
6291
- getSegmentRenderer as y,
6292
- isFormatAdapter as z
6441
+ filesToAttachments as y,
6442
+ getSegmentRenderer as z
6293
6443
  };
6294
- //# sourceMappingURL=index-D89k-n2Q.js.map
6444
+ //# sourceMappingURL=index-BBg6dsYx.js.map