@aparte/core 0.15.1 → 0.16.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 (143) hide show
  1. package/dist/client/aparte-client.d.ts +53 -51
  2. package/dist/client/aparte-client.d.ts.map +1 -1
  3. package/dist/client/lifecycle-events.d.ts +0 -12
  4. package/dist/client/lifecycle-events.d.ts.map +1 -1
  5. package/dist/client/stream-adapter.d.ts +2 -12
  6. package/dist/client/stream-adapter.d.ts.map +1 -1
  7. package/dist/components/bubble/aparte-chat-bubble.d.ts +45 -1
  8. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  9. package/dist/components/chat/aparte-chat.d.ts +1 -1
  10. package/dist/components/composer/aparte-composer-action.d.ts +1 -1
  11. package/dist/components/composer/aparte-composer-add-attachment.d.ts +1 -1
  12. package/dist/components/composer/aparte-composer-attachments.d.ts +1 -2
  13. package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
  14. package/dist/components/composer/aparte-composer-cancel.d.ts +1 -1
  15. package/dist/components/composer/aparte-composer-input.d.ts +1 -1
  16. package/dist/components/composer/aparte-composer-send.d.ts +12 -12
  17. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  18. package/dist/components/composer/aparte-composer.d.ts +29 -12
  19. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  20. package/dist/components/context/aparte-context.d.ts +31 -8
  21. package/dist/components/context/aparte-context.d.ts.map +1 -1
  22. package/dist/components/conversation-list/aparte-conversation-list.d.ts +93 -36
  23. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  24. package/dist/components/conversation-list/index.d.ts +1 -1
  25. package/dist/components/conversation-list/index.d.ts.map +1 -1
  26. package/dist/components/elicitation/aparte-elicitation.d.ts +20 -2
  27. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  28. package/dist/components/index.d.ts +7 -1
  29. package/dist/components/index.d.ts.map +1 -1
  30. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts +105 -0
  31. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -0
  32. package/dist/components/scroll-rail/index.d.ts +3 -0
  33. package/dist/components/scroll-rail/index.d.ts.map +1 -0
  34. package/dist/components/sidebar/aparte-sidebar.d.ts +186 -0
  35. package/dist/components/sidebar/aparte-sidebar.d.ts.map +1 -0
  36. package/dist/components/sidebar/index.d.ts +3 -0
  37. package/dist/components/sidebar/index.d.ts.map +1 -0
  38. package/dist/components/split/aparte-split.d.ts +354 -0
  39. package/dist/components/split/aparte-split.d.ts.map +1 -0
  40. package/dist/components/split/geometry.d.ts +65 -0
  41. package/dist/components/split/geometry.d.ts.map +1 -0
  42. package/dist/components/split/index.d.ts +3 -0
  43. package/dist/components/split/index.d.ts.map +1 -0
  44. package/dist/components/suggestions/aparte-suggestions.d.ts +7 -1
  45. package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -1
  46. package/dist/components/viewport/aparte-chat-viewport.d.ts +52 -2
  47. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  48. package/dist/config/aparte-config.d.ts +95 -33
  49. package/dist/config/aparte-config.d.ts.map +1 -1
  50. package/dist/config/icon-provider.d.ts +6 -0
  51. package/dist/config/icon-provider.d.ts.map +1 -1
  52. package/dist/config/index.d.ts +2 -2
  53. package/dist/config/index.d.ts.map +1 -1
  54. package/dist/config/locale.d.ts +140 -27
  55. package/dist/config/locale.d.ts.map +1 -1
  56. package/dist/config/sanitize.d.ts +9 -0
  57. package/dist/config/sanitize.d.ts.map +1 -1
  58. package/dist/conversations/conversation-controller.d.ts +13 -1
  59. package/dist/conversations/conversation-controller.d.ts.map +1 -1
  60. package/dist/conversations/conversation-manager.d.ts +9 -0
  61. package/dist/conversations/conversation-manager.d.ts.map +1 -1
  62. package/dist/custom-elements.json +10278 -7784
  63. package/dist/{is-awaiting-reply-BZ_iJ3s3.js → dialog-triggers-CVCxqKg3.js} +718 -1182
  64. package/dist/dialog-triggers-CVCxqKg3.js.map +1 -0
  65. package/dist/elicitation/approval-panel.d.ts +1 -1
  66. package/dist/elicitation/approval-panel.d.ts.map +1 -1
  67. package/dist/elicitation/panel.d.ts +24 -16
  68. package/dist/elicitation/panel.d.ts.map +1 -1
  69. package/dist/elicitation/types.d.ts +31 -0
  70. package/dist/elicitation/types.d.ts.map +1 -1
  71. package/dist/generated/element-attributes.d.ts +47 -1
  72. package/dist/generated/element-attributes.d.ts.map +1 -1
  73. package/dist/{glyphs-B3BZ2TIb.js → glyphs-DQTeOj4w.js} +22 -13
  74. package/dist/{glyphs-B3BZ2TIb.js.map → glyphs-DQTeOj4w.js.map} +1 -1
  75. package/dist/host/aparte-chat-host.d.ts +10 -2
  76. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  77. package/dist/icons/extended.d.ts +4 -2
  78. package/dist/icons/extended.d.ts.map +1 -1
  79. package/dist/icons/glyphs.d.ts +9 -0
  80. package/dist/icons/glyphs.d.ts.map +1 -1
  81. package/dist/icons.js +16 -15
  82. package/dist/icons.js.map +1 -1
  83. package/dist/index.css +1284 -397
  84. package/dist/index.d.ts +26 -15
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +2099 -296
  87. package/dist/index.js.map +1 -1
  88. package/dist/index.node.d.ts +21 -5
  89. package/dist/index.node.d.ts.map +1 -1
  90. package/dist/index.node.js +39 -34
  91. package/dist/index.node.js.map +1 -1
  92. package/dist/interop/dialog-triggers.d.ts +27 -0
  93. package/dist/interop/dialog-triggers.d.ts.map +1 -0
  94. package/dist/interop/element-props.d.ts +16 -8
  95. package/dist/interop/element-props.d.ts.map +1 -1
  96. package/dist/parsers/aparte-stream-parser.d.ts +52 -22
  97. package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
  98. package/dist/parsers/index.d.ts +1 -1
  99. package/dist/parsers/index.d.ts.map +1 -1
  100. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  101. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  102. package/dist/renderers/segments/code.d.ts.map +1 -1
  103. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  104. package/dist/runtime/message-repository.d.ts.map +1 -1
  105. package/dist/transport/backend-handler.d.ts.map +1 -1
  106. package/dist/transport/backend-transport.d.ts +6 -1
  107. package/dist/transport/backend-transport.d.ts.map +1 -1
  108. package/dist/types/chat.d.ts +8 -14
  109. package/dist/types/chat.d.ts.map +1 -1
  110. package/dist/types/element-map.d.ts +54 -4
  111. package/dist/types/element-map.d.ts.map +1 -1
  112. package/dist/types/event-map.d.ts +16 -10
  113. package/dist/types/event-map.d.ts.map +1 -1
  114. package/dist/types/events.d.ts +59 -117
  115. package/dist/types/events.d.ts.map +1 -1
  116. package/dist/types/imperative-api.d.ts +3 -1
  117. package/dist/types/imperative-api.d.ts.map +1 -1
  118. package/dist/types/index.d.ts +5 -4
  119. package/dist/types/index.d.ts.map +1 -1
  120. package/dist/types/models.d.ts +11 -31
  121. package/dist/types/models.d.ts.map +1 -1
  122. package/dist/types/segments.d.ts +11 -47
  123. package/dist/types/segments.d.ts.map +1 -1
  124. package/dist/types/stream-blocks.d.ts +57 -0
  125. package/dist/types/stream-blocks.d.ts.map +1 -0
  126. package/dist/types/tools.d.ts +57 -0
  127. package/dist/types/tools.d.ts.map +1 -1
  128. package/dist/utils/copy-text.d.ts +24 -0
  129. package/dist/utils/copy-text.d.ts.map +1 -0
  130. package/dist/utils/css-escape.d.ts +5 -0
  131. package/dist/utils/css-escape.d.ts.map +1 -1
  132. package/dist/utils/tool-input.d.ts +20 -0
  133. package/dist/utils/tool-input.d.ts.map +1 -0
  134. package/package.json +10 -3
  135. package/dist/is-awaiting-reply-BZ_iJ3s3.js.map +0 -1
  136. package/dist/renderers/segments/artifact/binary-file.d.ts +0 -45
  137. package/dist/renderers/segments/artifact/binary-file.d.ts.map +0 -1
  138. package/dist/renderers/segments/artifact/card.d.ts +0 -3
  139. package/dist/renderers/segments/artifact/card.d.ts.map +0 -1
  140. package/dist/renderers/segments/artifact/preview-document.d.ts +0 -27
  141. package/dist/renderers/segments/artifact/preview-document.d.ts.map +0 -1
  142. package/dist/renderers/segments/artifact/shared.d.ts +0 -29
  143. package/dist/renderers/segments/artifact/shared.d.ts.map +0 -1
@@ -1,5 +1,5 @@
1
- import { A as APARTE_ICON_GLYPHS } from "./glyphs-B3BZ2TIb.js";
2
- import { deriveArtifactKind, runStreamAgent } from "@aparte/engine";
1
+ import { A as APARTE_ICON_GLYPHS } from "./glyphs-DQTeOj4w.js";
2
+ import { runStreamAgent } from "@aparte/engine";
3
3
  function contentToText(content) {
4
4
  if (typeof content === "string") return content;
5
5
  return content.filter((p) => p.type === "text").map((p) => p.text).join("");
@@ -90,7 +90,7 @@ const APARTE_DEFAULT_LOCALE = {
90
90
  elicitationOtherLabel: "Custom answer",
91
91
  submitButton: "Submit",
92
92
  stopButton: "Stop",
93
- elicitationNext: "Next",
93
+ elicitationRecommended: "Recommended",
94
94
  elicitationSkip: "Skip",
95
95
  elicitationYes: "Yes",
96
96
  elicitationNo: "No",
@@ -106,10 +106,15 @@ const APARTE_DEFAULT_LOCALE = {
106
106
  toolCompleted: "Done",
107
107
  toolRejected: "Rejected",
108
108
  toolStopped: "Stopped",
109
+ toolFailed: "Failed",
109
110
  toolInput: "Input",
110
111
  toolOutput: "Output",
111
112
  approvalOptionsLabel: "Your decision",
113
+ approvalArgsLabel: "Arguments",
112
114
  actionUpload: "Attach file",
115
+ removeAttachment: "Remove {name}",
116
+ selectSearchPlaceholder: "Search…",
117
+ selectSearchLabel: "Search options",
113
118
  download: "Download",
114
119
  previewPending: "Press Preview to run this artifact.",
115
120
  sandboxError: "The sandbox failed during generation.",
@@ -124,6 +129,7 @@ const APARTE_DEFAULT_LOCALE = {
124
129
  run: "Run",
125
130
  file: "File",
126
131
  modelSelectorPlaceholder: "Select a model...",
132
+ approvalModeLabel: "Approval mode",
127
133
  roleNameUser: "You",
128
134
  roleNameAssistant: "Assistant",
129
135
  yourMessage: "Your message",
@@ -138,12 +144,27 @@ const APARTE_DEFAULT_LOCALE = {
138
144
  nextResponse: "Next response",
139
145
  approveTool: "Approve",
140
146
  rejectTool: "Reject",
141
- tokensPerSecondLabel: "tok/s",
142
147
  messageInfo: "Details",
143
148
  newChat: "New Chat",
144
- deleteConversation: "Delete conversation",
145
- archiveConversation: "Archive conversation",
146
- unarchiveConversation: "Unarchive conversation",
149
+ deleteConversation: "Delete",
150
+ archiveConversation: "Archive",
151
+ unarchiveConversation: "Unarchive",
152
+ conversationActions: "Conversation actions",
153
+ renameConversation: "Rename",
154
+ conversationTitle: "Conversation title",
155
+ pinConversation: "Pin",
156
+ unpinConversation: "Unpin",
157
+ deleteConversationConfirm: "Delete “{title}”?",
158
+ cancel: "Cancel",
159
+ conversationGroupPinned: "Pinned",
160
+ conversationGroupToday: "Today",
161
+ conversationGroupYesterday: "Yesterday",
162
+ conversationGroupWeek: "Previous 7 days",
163
+ conversationGroupMonth: "Previous 30 days",
164
+ scrollRailLabel: "Conversation outline",
165
+ sidebarLabel: "Conversations",
166
+ splitHandleLabel: "Resize the panes",
167
+ transcript: "Transcript",
147
168
  direction: "ltr"
148
169
  };
149
170
  function isFormatAdapter(p) {
@@ -237,10 +258,21 @@ class AparteStreamParser {
237
258
  _options;
238
259
  /** Normalized to an array; the parser matches whichever pair opens first. */
239
260
  _thinkingDelimiters;
261
+ /** The registered block grammars, in registration order. */
262
+ _blocks;
240
263
  _state;
264
+ /**
265
+ * A block already BUILT and waiting for its turn to be emitted — a self-closing
266
+ * tag that followed prose, whose text run has to go out first. One segment per
267
+ * step, but `toSegment` is still called exactly once, as `AparteStreamBlock`
268
+ * documents: re-reading the tag on the next step called a consumer's grammar
269
+ * twice for one tag.
270
+ */
271
+ _pendingSegment = null;
241
272
  constructor(options = {}) {
242
273
  const delims = options.thinkingDelimiters;
243
274
  this._thinkingDelimiters = delims ? Array.isArray(delims) ? delims : [delims] : DEFAULT_THINKING_DELIMITERS;
275
+ this._blocks = options.blocks ?? [];
244
276
  this._options = {
245
277
  autoGenerateIds: options.autoGenerateIds ?? true,
246
278
  idPrefix: options.idPrefix ?? "seg"
@@ -257,7 +289,7 @@ class AparteStreamParser {
257
289
  parse(chunk) {
258
290
  this._state.buffer += chunk;
259
291
  const completedSegments = [];
260
- while (this._state.buffer.length > 0) {
292
+ while (this._state.buffer.length > 0 || this._pendingSegment) {
261
293
  const parsed = this._parseNext();
262
294
  if (!parsed) {
263
295
  break;
@@ -288,11 +320,14 @@ class AparteStreamParser {
288
320
  }
289
321
  finalSegments.push(this._closed(this._state.activeSegment));
290
322
  this._state.activeSegment = null;
291
- this._state.buffer = "";
292
323
  } else if (this._state.buffer.trim()) {
293
324
  finalSegments.push(this._closed(this._createTextSegment(stripTrailingFence(this._state.buffer))));
294
- this._state.buffer = "";
295
325
  }
326
+ this._state.mode = "text";
327
+ this._state.buffer = "";
328
+ this._state.codeLanguage = void 0;
329
+ this._state.thinkingEnd = void 0;
330
+ this._state.blockEnd = void 0;
296
331
  return finalSegments;
297
332
  }
298
333
  /**
@@ -300,6 +335,7 @@ class AparteStreamParser {
300
335
  */
301
336
  reset() {
302
337
  this._state = this._createInitialState();
338
+ this._pendingSegment = null;
303
339
  }
304
340
  /**
305
341
  * Get current parser state (for debugging/inspection)
@@ -311,6 +347,11 @@ class AparteStreamParser {
311
347
  // Private Parsing Logic
312
348
  // ─────────────────────────────────────────────────────────────────────────
313
349
  _parseNext() {
350
+ if (this._pendingSegment) {
351
+ const segment = this._pendingSegment;
352
+ this._pendingSegment = null;
353
+ return { segment, remaining: this._state.buffer };
354
+ }
314
355
  const buffer = this._state.buffer;
315
356
  switch (this._state.mode) {
316
357
  case "text":
@@ -319,8 +360,8 @@ class AparteStreamParser {
319
360
  return this._parseCodeMode(buffer);
320
361
  case "thinking":
321
362
  return this._parseThinkingMode(buffer);
322
- case "artifact":
323
- return this._parseArtifactMode(buffer);
363
+ case "block":
364
+ return this._parseBlockMode(buffer);
324
365
  default:
325
366
  return null;
326
367
  }
@@ -332,14 +373,23 @@ class AparteStreamParser {
332
373
  const i = buffer.indexOf(p.start);
333
374
  if (i !== -1 && (thinkingStart === -1 || i < thinkingStart)) thinkingStart = i;
334
375
  }
335
- const artifactStart = buffer.indexOf("<artifact");
376
+ let blockStart = -1;
377
+ let blockHit;
378
+ for (const b of this._blocks) {
379
+ const i = this._indexOfTag(buffer, b.tag);
380
+ if (i !== -1 && (blockStart === -1 || i < blockStart)) {
381
+ blockStart = i;
382
+ blockHit = b;
383
+ }
384
+ }
336
385
  const patterns = [];
337
386
  if (codeBlockStart !== -1) patterns.push({ type: "code", index: codeBlockStart });
338
387
  if (thinkingStart !== -1) patterns.push({ type: "thinking", index: thinkingStart });
339
- if (artifactStart !== -1) patterns.push({ type: "artifact", index: artifactStart });
388
+ if (blockStart !== -1) patterns.push({ type: "block", index: blockStart });
340
389
  patterns.sort((a, b) => a.index - b.index);
341
390
  if (patterns.length === 0) {
342
- if (buffer.endsWith("`") || buffer.endsWith("``") || this._thinkingDelimiters.some((p) => this._isPartialMatch(buffer, p.start)) || this._isPartialMatch(buffer, "<artifact")) {
391
+ if (buffer.endsWith("`") || buffer.endsWith("``") || this._thinkingDelimiters.some((p) => this._isPartialMatch(buffer, p.start)) || // `<ta`, and also the whole `<tag` with nothing after it yet.
392
+ this._blocks.some((b) => this._isPartialMatch(buffer, "<" + b.tag) || buffer.endsWith("<" + b.tag))) {
343
393
  return null;
344
394
  }
345
395
  if (!this._state.activeSegment || this._state.activeSegment.type !== "text") {
@@ -378,12 +428,16 @@ class AparteStreamParser {
378
428
  }
379
429
  return { segment: segmentToEmit, remaining: res.remaining };
380
430
  } else {
381
- const res = this._startArtifactBlock(buffer);
431
+ const res = this._startBlock(buffer, blockHit);
382
432
  if (!res) {
383
433
  if (segmentToEmit) this._state.activeSegment = segmentToEmit;
384
434
  return null;
385
435
  }
386
- return { segment: segmentToEmit, remaining: res.remaining };
436
+ if (res.segment && segmentToEmit) {
437
+ this._pendingSegment = res.segment;
438
+ return { segment: segmentToEmit, remaining: res.remaining };
439
+ }
440
+ return { segment: segmentToEmit ?? res.segment, remaining: res.remaining };
387
441
  }
388
442
  }
389
443
  _startCodeBlock(buffer) {
@@ -465,70 +519,123 @@ class AparteStreamParser {
465
519
  this._state.thinkingEnd = void 0;
466
520
  return { segment, remaining: buffer.slice(closeIndex + endDelim.length) };
467
521
  }
522
+ // ─── registered blocks ──────────────────────────────────────────────────
523
+ /**
524
+ * Where `<tag` opens a block in `buffer`, or -1. A hit needs the name to END
525
+ * there: `<artifacts>` is not `<artifact>`, so the character after the name must
526
+ * be whitespace, `/` or `>`. A `<tag` sitting at the very end of the buffer is
527
+ * undecidable and reported as -1 — the partial-match hold in `_parseTextMode`
528
+ * keeps it for the next chunk.
529
+ */
530
+ _indexOfTag(buffer, tag) {
531
+ const open = "<" + tag;
532
+ let from = 0;
533
+ for (; ; ) {
534
+ const i = buffer.indexOf(open, from);
535
+ if (i === -1) return -1;
536
+ const next = buffer[i + open.length];
537
+ if (next === void 0) return -1;
538
+ if (next === ">" || next === "/" || /\s/.test(next)) return i;
539
+ from = i + 1;
540
+ }
541
+ }
468
542
  /**
469
- * Start an artifact block. Buffer must start with `<artifact`.
470
- * The full opening tag must be present (`>`); otherwise we return null and wait
471
- * for more chunks.
543
+ * Where the opening `<tag …>` ends, or -1 while it is still incomplete.
472
544
  *
473
- * Supported attributes (Anthropic-style, single or double quotes):
474
- * - type — required (MIME type, verbatim)
475
- * - title — optional human label
545
+ * The first `>` is not the answer: `<note title="v1 -> v2">` carries one inside
546
+ * a quoted value, and cutting there loses every attribute AND leaks the rest of
547
+ * the tag into the body as raw markup. A quote only OPENS after an `=`, so a
548
+ * stray `"` written in an attribute-less tag cannot hold the buffer open.
476
549
  *
477
- * Any other attributes are ignored gracefully.
550
+ * A quote the model never closes must not hold it open either. `-1` means
551
+ * "wait", and waiting on a value that runs to the end of the reply freezes the
552
+ * stream: nothing is emitted until `finalize()` flushes the whole tail as raw
553
+ * text. These tag conventions are single-line, so a newline reached inside an
554
+ * open value says the quote was a typo — and the tag is then read the way it
555
+ * has always been read, at its first `>`.
556
+ */
557
+ _tagEnd(buffer, tag) {
558
+ let quote = null;
559
+ let afterEquals = false;
560
+ let firstGt = -1;
561
+ for (let i = 1 + tag.length; i < buffer.length; i++) {
562
+ const c = buffer[i];
563
+ if (c === ">" && firstGt === -1) firstGt = i;
564
+ if (quote) {
565
+ if (c === quote) quote = null;
566
+ else if (c === "\n") return firstGt;
567
+ continue;
568
+ }
569
+ if (c === ">") return i;
570
+ if (c === "=") {
571
+ afterEquals = true;
572
+ continue;
573
+ }
574
+ if (afterEquals && (c === '"' || c === "'")) {
575
+ quote = c;
576
+ afterEquals = false;
577
+ continue;
578
+ }
579
+ if (!/\s/.test(c)) afterEquals = false;
580
+ }
581
+ return -1;
582
+ }
583
+ /**
584
+ * Open a registered block. The buffer starts with `<tag`; the whole opening tag
585
+ * must be present (its `>`) or we wait. Attributes are read as written —
586
+ * `a="x"`, `a='x'`, `a=x` — and a self-closing `<tag …/>` is a block with no
587
+ * body, returned closed at once.
478
588
  */
479
- _startArtifactBlock(buffer) {
480
- if (!buffer.startsWith("<artifact")) return null;
481
- const tagEnd = buffer.indexOf(">");
589
+ _startBlock(buffer, block) {
590
+ const tagEnd = this._tagEnd(buffer, block.tag);
482
591
  if (tagEnd === -1) return null;
483
- const tag = buffer.slice(0, tagEnd + 1);
484
- const inner = tag.slice("<artifact".length, -1);
485
- const mimeMatch = inner.match(/\bmimeType\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
486
- const typeMatch = inner.match(/\btype\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
487
- const titleMatch = inner.match(/\btitle\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
488
- const mimeType = (mimeMatch && (mimeMatch[1] ?? mimeMatch[2])) ?? (typeMatch && (typeMatch[1] ?? typeMatch[2])) ?? "text/plain";
489
- const title = titleMatch ? titleMatch[1] ?? titleMatch[2] : void 0;
490
- this._state.mode = "artifact";
491
- this._state.activeSegment = this._createArtifactSegment(mimeType, title);
492
- return { segment: null, remaining: buffer.slice(tagEnd + 1) };
493
- }
494
- /**
495
- * Stream content into the active artifact segment until `</artifact>` is seen.
496
- * Mirrors `_parseThinkingMode` so partial matches at chunk boundaries don't
497
- * split the closing tag across two updates.
498
- */
499
- _parseArtifactMode(buffer) {
500
- const endDelim = "</artifact>";
592
+ const inner = buffer.slice(1 + block.tag.length, tagEnd);
593
+ const selfClosing = inner.trimEnd().endsWith("/");
594
+ const attrSource = selfClosing ? inner.trimEnd().slice(0, -1) : inner;
595
+ const attrs = /* @__PURE__ */ Object.create(null);
596
+ for (const m of attrSource.matchAll(/([A-Za-z_][\w:-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/g)) {
597
+ attrs[m[1]] = m[2] ?? m[3] ?? m[4] ?? "";
598
+ }
599
+ const segment = block.toSegment({ attrs, id: this._options.autoGenerateIds ? this._generateId() : "" });
600
+ segment.content = "";
601
+ const remaining = buffer.slice(tagEnd + 1);
602
+ if (selfClosing) return { segment: this._closed(segment), remaining };
603
+ segment.isStreaming = true;
604
+ this._state.mode = "block";
605
+ this._state.blockEnd = `</${block.tag}>`;
606
+ this._state.activeSegment = segment;
607
+ return { segment: null, remaining };
608
+ }
609
+ /**
610
+ * Stream the body of the open block until its closing tag — the same
611
+ * partial-delimiter handling the thinking mode uses, so a closing
612
+ * tag split across two chunks is never emitted as content.
613
+ */
614
+ _parseBlockMode(buffer) {
615
+ const endDelim = this._state.blockEnd;
501
616
  const closeIndex = buffer.indexOf(endDelim);
617
+ const active = this._state.activeSegment;
502
618
  if (closeIndex === -1) {
503
619
  if (this._isPartialMatch(buffer, endDelim)) {
504
620
  const partialLength = this._getPartialMatchLength(buffer, endDelim);
505
621
  const safeContent = buffer.slice(0, buffer.length - partialLength);
506
- if (safeContent && this._state.activeSegment && "content" in this._state.activeSegment) {
507
- this._state.activeSegment.content += safeContent;
508
- }
622
+ if (safeContent && active) active.content += safeContent;
509
623
  return safeContent ? { segment: null, remaining: buffer.slice(-partialLength) } : null;
510
624
  }
511
625
  if (buffer.length > endDelim.length) {
512
626
  const safeContent = buffer.slice(0, -endDelim.length);
513
- if (this._state.activeSegment && "content" in this._state.activeSegment) {
514
- this._state.activeSegment.content += safeContent;
515
- }
627
+ if (active) active.content += safeContent;
516
628
  return { segment: null, remaining: buffer.slice(-endDelim.length) };
517
629
  }
518
630
  return null;
519
631
  }
520
- const tail = buffer.slice(0, closeIndex);
521
- if (this._state.activeSegment && "content" in this._state.activeSegment) {
522
- this._state.activeSegment.content += tail;
523
- }
632
+ if (active) active.content += buffer.slice(0, closeIndex);
524
633
  const segment = this._closed(this._state.activeSegment);
525
634
  this._state.mode = "text";
526
635
  this._state.activeSegment = null;
636
+ this._state.blockEnd = void 0;
527
637
  return { segment, remaining: buffer.slice(closeIndex + endDelim.length) };
528
638
  }
529
- // ─────────────────────────────────────────────────────────────────────────
530
- // Helpers
531
- // ─────────────────────────────────────────────────────────────────────────
532
639
  _createInitialState() {
533
640
  return {
534
641
  mode: "text",
@@ -593,16 +700,6 @@ class AparteStreamParser {
593
700
  label: "Thinking"
594
701
  };
595
702
  }
596
- _createArtifactSegment(mimeType, title) {
597
- return {
598
- id: this._options.autoGenerateIds ? this._generateId() : "",
599
- type: "artifact",
600
- mimeType,
601
- artifactType: deriveArtifactKind(mimeType),
602
- title,
603
- content: ""
604
- };
605
- }
606
703
  _isPartialMatch(buffer, pattern) {
607
704
  for (let i = 1; i < pattern.length; i++) {
608
705
  if (buffer.endsWith(pattern.slice(0, i))) {
@@ -709,7 +806,7 @@ function createAparteChatHandler(options) {
709
806
  } catch {
710
807
  return jsonError(400, "Invalid JSON body.");
711
808
  }
712
- const adapter = options.providers[providerId];
809
+ const adapter = Object.hasOwn(options.providers, providerId) ? options.providers[providerId] : void 0;
713
810
  if (!adapter) {
714
811
  return jsonError(400, `Unknown providerId "${providerId}". Register it in the handler's providers map.`);
715
812
  }
@@ -738,7 +835,8 @@ function createAparteChatHandler(options) {
738
835
  // a client disconnect aborts the vendor call too
739
836
  });
740
837
  } catch (err) {
741
- return jsonError(502, `Vendor request failed: ${err?.message ?? "network error"}`);
838
+ console.error("[aparte] vendor request failed", err);
839
+ return jsonError(502, "Vendor request failed.");
742
840
  }
743
841
  if (!vendor.ok) {
744
842
  const raw = await vendor.text().catch(() => "");
@@ -907,8 +1005,10 @@ const GLOBAL_ATTRS = /* @__PURE__ */ new Set([
907
1005
  "aria-level"
908
1006
  ]);
909
1007
  const TAG_ATTRS = {
910
- a: /* @__PURE__ */ new Set(["href", "target", "rel"]),
911
- // legacy `name` dropped obsolete + a DOM-clobbering vector
1008
+ // `target` and `rel` are NOT copied from the model's markup — `copyAttributes`
1009
+ // decides both below. Legacy `name` is dropped too (obsolete + a DOM-clobbering
1010
+ // vector).
1011
+ a: /* @__PURE__ */ new Set(["href"]),
912
1012
  img: /* @__PURE__ */ new Set(["src", "alt", "width", "height", "loading", "srcset", "sizes", "decoding"]),
913
1013
  source: /* @__PURE__ */ new Set(["src", "srcset", "type", "media", "sizes"]),
914
1014
  input: /* @__PURE__ */ new Set(["type", "checked", "disabled"]),
@@ -925,6 +1025,8 @@ const SAFE_URL = /^(?:https?:|mailto:|tel:|ftp:|sms:)/i;
925
1025
  const RELATIVE_URL = /^(?:[#/.?]|[a-z0-9._~%+-]+(?:[/?#]|$))/i;
926
1026
  const SAFE_DATA_IMG = /^data:image\/(?:png|jpe?g|gif|webp|avif|bmp|x-icon|svg\+xml)[;,]/i;
927
1027
  const CONTROL_WS = /[\u0000-\u0020]+/g;
1028
+ const EXTERNAL_URL = /^(?:https?:)?[/\\]{2}|^https?:[/\\]/i;
1029
+ const APARTE_CLASS = /^aparte-/i;
928
1030
  function isSafeSrcset(value, tag) {
929
1031
  const urlTag = tag === "source" ? "img" : tag;
930
1032
  for (const m of value.matchAll(/[a-z][a-z0-9+.-]*:[^\s,]*/gi)) {
@@ -990,10 +1092,23 @@ function copyAttributes(src, dest, tag) {
990
1092
  dest.setAttribute("style", scrubbed);
991
1093
  continue;
992
1094
  }
1095
+ if (name === "class") {
1096
+ const kept = value.split(/\s+/).filter((token) => token && !APARTE_CLASS.test(token));
1097
+ if (!kept.length) continue;
1098
+ dest.setAttribute("class", kept.join(" "));
1099
+ continue;
1100
+ }
993
1101
  dest.setAttribute(name, value);
994
1102
  }
995
- if (tag === "a" && dest.getAttribute("target") === "_blank") {
996
- dest.setAttribute("rel", "noopener noreferrer");
1103
+ if (tag === "a") {
1104
+ const href = (dest.getAttribute("href") ?? "").replace(CONTROL_WS, "");
1105
+ const wish = (src.getAttribute("target") ?? "").trim().toLowerCase();
1106
+ if (wish === "_self" && !EXTERNAL_URL.test(href)) {
1107
+ dest.setAttribute("target", "_self");
1108
+ } else if (wish || EXTERNAL_URL.test(href)) {
1109
+ dest.setAttribute("target", "_blank");
1110
+ dest.setAttribute("rel", "noopener noreferrer");
1111
+ }
997
1112
  }
998
1113
  }
999
1114
  function sanitizeChildren(src, dest, doc) {
@@ -1016,8 +1131,19 @@ function sanitizeChildren(src, dest, doc) {
1016
1131
  dest.appendChild(clean);
1017
1132
  }
1018
1133
  }
1134
+ const UNWRAPPED_TAGS = /* @__PURE__ */ new Set(["html", "head", "body"]);
1135
+ const DANGEROUS_ALT = Array.from(DANGEROUS_TAGS).join("|");
1136
+ const DANGEROUS_PAIRED_ALT = Array.from(DANGEROUS_TAGS).filter((t) => !UNWRAPPED_TAGS.has(t)).join("|");
1137
+ const PAIRED_DANGEROUS = new RegExp(`<\\s*(${DANGEROUS_PAIRED_ALT})\\b[\\s\\S]*?<\\s*\\/\\s*\\1\\s*>`, "gi");
1138
+ const LONE_DANGEROUS = new RegExp(`<\\s*\\/?\\s*(?:${DANGEROUS_ALT})\\b[^>]*>`, "gi");
1139
+ const INLINE_HANDLER = /["'/\s]on[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi;
1140
+ const UNSAFE_SCHEME_ATTR = /(?:href|src|xlink:href)\s*=\s*(?:"\s*(?:javascript|vbscript|data):[^"]*"|'\s*(?:javascript|vbscript|data):[^']*'|(?:javascript|vbscript|data):[^\s>]*)/gi;
1019
1141
  function fallbackScrub(html) {
1020
- return html.replace(/<\s*(script|style|iframe|object|embed|form|svg|math|applet|template)\b[\s\S]*?<\s*\/\s*\1\s*>/gi, "").replace(/<\s*(script|style|iframe|object|embed|applet|template|link|meta|base|frame|frameset)\b[^>]*>/gi, "").replace(/\son[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "").replace(/(?:href|src|xlink:href)\s*=\s*(?:"\s*(?:javascript|vbscript|data):[^"]*"|'\s*(?:javascript|vbscript|data):[^']*'|(?:javascript|vbscript|data):[^\s>]*)/gi, "");
1142
+ let out = html.replace(PAIRED_DANGEROUS, "").replace(LONE_DANGEROUS, "");
1143
+ for (let next = out.replace(INLINE_HANDLER, " "); next !== out; next = out.replace(INLINE_HANDLER, " ")) {
1144
+ out = next;
1145
+ }
1146
+ return out.replace(UNSAFE_SCHEME_ATTR, " ");
1021
1147
  }
1022
1148
  const defaultSanitizer = (html) => {
1023
1149
  if (!html) return html;
@@ -1084,9 +1210,7 @@ const APARTE_DEFAULT_BUBBLE_ACTIONS = {
1084
1210
  info: false
1085
1211
  };
1086
1212
  const APARTE_DEFAULT_HOST_HANDLERS = {
1087
- attachmentPreview: false,
1088
- artifactRedownload: false,
1089
- artifactRehydrate: false
1213
+ attachmentPreview: false
1090
1214
  };
1091
1215
  const NOOP = () => {
1092
1216
  };
@@ -1108,7 +1232,6 @@ class AparteConfig {
1108
1232
  _iconProvider;
1109
1233
  _avatarProvider;
1110
1234
  _keyProvider;
1111
- _artifactPreviewBuilder;
1112
1235
  _locale = APARTE_DEFAULT_LOCALE;
1113
1236
  _actions = [];
1114
1237
  _listeners = /* @__PURE__ */ new Set();
@@ -1134,10 +1257,14 @@ class AparteConfig {
1134
1257
  /** Host policy for the elicitation panel's free-text escape — see {@link setElicitationOptions}. */
1135
1258
  _elicitationAllowOther = true;
1136
1259
  _elicitationLayout = "stepped";
1260
+ /** Whether a single choice answers on the click (buttons) or keeps its radios + the composer's button. */
1261
+ _elicitationAnswerOnClick = true;
1137
1262
  _elicitationFieldRenderer;
1138
1263
  // Transport: where chat requests go + how auth is handled (AparteDirectTransport = browser-direct).
1139
1264
  _transport = new AparteDirectTransport();
1140
1265
  _modelPreferenceProvider;
1266
+ // Per-call approval policy (a mode: plan / ask / auto). `null` = the tools' own flags.
1267
+ _approvalPolicy = null;
1141
1268
  // Conversation persistence (optional, agnostic)
1142
1269
  _conversationManager;
1143
1270
  /*
@@ -1183,6 +1310,8 @@ class AparteConfig {
1183
1310
  // Tool Registry
1184
1311
  _tools = /* @__PURE__ */ new Map();
1185
1312
  _toolRenderers = /* @__PURE__ */ new Map();
1313
+ /** Tagged blocks the stream parser recognises in the prose — see {@link registerStreamBlock}. */
1314
+ _streamBlocks = /* @__PURE__ */ new Map();
1186
1315
  _segmentDefaults = /* @__PURE__ */ new Map();
1187
1316
  // Host handlers — what the app declares it can actually complete.
1188
1317
  _hostHandlers = { ...APARTE_DEFAULT_HOST_HANDLERS };
@@ -1236,7 +1365,7 @@ class AparteConfig {
1236
1365
  * @example
1237
1366
  * aparteGlobalConfig.setBubbleActions({ retry: true, edit: true }) // you run AparteClient
1238
1367
  * aparteGlobalConfig.setBubbleActions({ feedback: true }) // you listen for aparte-feedback
1239
- * aparteGlobalConfig.setBubbleActions({ copy: false }) // hide everything
1368
+ * aparteGlobalConfig.setBubbleActions({ copy: false }) // hides copy; calls merge, so anything turned on above stays on
1240
1369
  * // Explicit per-role ordered sets (replace the flag defaults for that role):
1241
1370
  * aparteGlobalConfig.setBubbleActions({ user: ['edit', 'copy'], assistant: ['copy', 'thumbUp', 'thumbDown', 'retry'] })
1242
1371
  */
@@ -1249,7 +1378,7 @@ class AparteConfig {
1249
1378
  * trigger only for the ones you claim — see {@link AparteHostHandlersConfig}.
1250
1379
  *
1251
1380
  * @example
1252
- * aparteGlobalConfig.setHostHandlers({ attachmentPreview: true, artifactRedownload: true });
1381
+ * aparteGlobalConfig.setHostHandlers({ attachmentPreview: true });
1253
1382
  */
1254
1383
  setHostHandlers(config) {
1255
1384
  this._hostHandlers = { ...this._hostHandlers, ...config };
@@ -1263,9 +1392,7 @@ class AparteConfig {
1263
1392
  // forgotten here.
1264
1393
  getHostHandlers() {
1265
1394
  return {
1266
- attachmentPreview: this._hostHandlers.attachmentPreview ?? APARTE_DEFAULT_HOST_HANDLERS.attachmentPreview,
1267
- artifactRedownload: this._hostHandlers.artifactRedownload ?? APARTE_DEFAULT_HOST_HANDLERS.artifactRedownload,
1268
- artifactRehydrate: this._hostHandlers.artifactRehydrate ?? APARTE_DEFAULT_HOST_HANDLERS.artifactRehydrate
1395
+ attachmentPreview: this._hostHandlers.attachmentPreview ?? APARTE_DEFAULT_HOST_HANDLERS.attachmentPreview
1269
1396
  };
1270
1397
  }
1271
1398
  /** Returns the resolved bubble actions config (flag defaults applied; per-role sets passed through). */
@@ -1384,19 +1511,6 @@ class AparteConfig {
1384
1511
  this._iconProvider = provider;
1385
1512
  this._notify();
1386
1513
  }
1387
- /**
1388
- * Register an artifact preview builder (app-level). When set, the artifact
1389
- * renderer uses it to build the preview iframe `srcdoc`; when unset, core's
1390
- * CDN-free fallback is used. This is how the product opts into a
1391
- * React/Babel/Tailwind live preview without leaking those into core.
1392
- */
1393
- setArtifactPreviewBuilder(builder) {
1394
- this._artifactPreviewBuilder = builder;
1395
- }
1396
- /** The registered artifact preview builder, or undefined for the core fallback. */
1397
- getArtifactPreviewBuilder() {
1398
- return this._artifactPreviewBuilder;
1399
- }
1400
1514
  /**
1401
1515
  * The icon set as a **complete** provider: every name resolves, falling back
1402
1516
  * to `APARTE_DEFAULT_ICON_FALLBACKS` for anything the registered provider doesn't
@@ -1521,7 +1635,17 @@ class AparteConfig {
1521
1635
  this._notify();
1522
1636
  }
1523
1637
  /**
1524
- * Get the current locale
1638
+ * Get the current locale.
1639
+ *
1640
+ * Typed as the closed list PLUS the open half, and the asymmetry is the point:
1641
+ * `t()` keys on `AparteLocale` alone — a typo there is a compile error — while a
1642
+ * plugin reading its OWN key off this object gets `string | undefined` and
1643
+ * supplies its own default at the call site.
1644
+ *
1645
+ * `t()` is the one that has to be narrow, because it is the one that fails
1646
+ * silently: it returns `''` for a key nobody declared, and an empty label is
1647
+ * invisible on screen and in the console. A misspelt read here is `undefined`,
1648
+ * which the `?? 'fallback'` every caller already writes turns into the default.
1525
1649
  */
1526
1650
  getLocale() {
1527
1651
  return this._locale;
@@ -1595,6 +1719,13 @@ class AparteConfig {
1595
1719
  }
1596
1720
  this._aiProviders.set(provider.id, provider);
1597
1721
  }
1722
+ if (!this.hasSelectedModel() && this._aiProviders.size === 1) {
1723
+ const only = [...this._aiProviders.values()][0];
1724
+ const models = only.getModels();
1725
+ if (models.length === 1) {
1726
+ this._modelConfig = { ...this._modelConfig, defaultProvider: only.id, defaultModel: models[0].id };
1727
+ }
1728
+ }
1598
1729
  this._notify();
1599
1730
  }
1600
1731
  /**
@@ -1648,6 +1779,47 @@ class AparteConfig {
1648
1779
  setModelPreferenceProvider(provider) {
1649
1780
  this._modelPreferenceProvider = provider;
1650
1781
  }
1782
+ /**
1783
+ * Decide per CALL whether a tool runs, asks, or is refused — a mode, not a flag.
1784
+ *
1785
+ * With a policy registered, the client's default approval channel evaluates it for
1786
+ * every tool call: `allow` runs without asking, `ask` puts the call to the person
1787
+ * exactly as a `needsApproval` tool is, `deny` refuses it with the policy's own
1788
+ * `reason` as what the model reads. `undefined` leaves the tool's `needsApproval`
1789
+ * to decide, as before. `null` removes the policy.
1790
+ *
1791
+ * `@aparte/plugin-approval` builds one from a classification of your tool names and
1792
+ * a switchable mode (plan / ask / auto-edit / auto); a host that owns its own
1793
+ * `approvalResolver` on `AparteClientOptions` is not affected — that resolver
1794
+ * already decides everything.
1795
+ *
1796
+ * @example
1797
+ * ```ts
1798
+ * aparteGlobalConfig.setApprovalPolicy((call) =>
1799
+ * call.name === 'run_command'
1800
+ * ? { verdict: 'ask' }
1801
+ * : { verdict: 'allow' });
1802
+ * ```
1803
+ */
1804
+ setApprovalPolicy(policy) {
1805
+ this._approvalPolicy = policy;
1806
+ this._notify();
1807
+ }
1808
+ /** The registered approval policy, or `null`. */
1809
+ getApprovalPolicy() {
1810
+ return this._approvalPolicy;
1811
+ }
1812
+ /**
1813
+ * What the policy says about one call, with the tool's own `needsApproval` as the
1814
+ * answer when it has no opinion. The one place the two are combined, so the client's
1815
+ * gate predicate and its approval channel cannot disagree.
1816
+ */
1817
+ ruleOnToolCall(call) {
1818
+ const tool = this._tools.get(call.name)?.tool;
1819
+ const ruling = this._approvalPolicy?.(call, tool);
1820
+ if (ruling) return ruling;
1821
+ return { verdict: tool?.needsApproval ? "ask" : "allow" };
1822
+ }
1651
1823
  /**
1652
1824
  * Restore previously saved model preference via the registered provider.
1653
1825
  * Should be called once at app startup, before any component mounts.
@@ -1736,15 +1908,26 @@ class AparteConfig {
1736
1908
  * collecting structured data, not two different questions asked mid-conversation.
1737
1909
  * No product asks a person two questions by stacking them in one box; it is kept
1738
1910
  * as an option because the form case is real, not because it was the right default.
1911
+ *
1912
+ * `answerOnClick` decides how a question asked ON ITS OWN with a single choice — an
1913
+ * `enum` without `multiple` or a `default`, a `boolean` without a `default` — is
1914
+ * answered. `true` (default) renders the options as buttons and the click is the
1915
+ * answer: one decision, one gesture, the shape every chat product uses for "which
1916
+ * one?". `false` keeps the radios and the composer's button, so a person can
1917
+ * change their mind before committing — the shape a form or a screen reader
1918
+ * flow may prefer, and the one a host that wants a uniform "select, then send"
1919
+ * across every question asks for. A form of several questions always collects and
1920
+ * submits, whatever this says.
1739
1921
  */
1740
1922
  setElicitationOptions(options) {
1741
1923
  if (options.allowOther !== void 0) this._elicitationAllowOther = options.allowOther;
1742
1924
  if (options.layout !== void 0) this._elicitationLayout = options.layout;
1925
+ if (options.answerOnClick !== void 0) this._elicitationAnswerOnClick = options.answerOnClick;
1743
1926
  this._notify();
1744
1927
  }
1745
1928
  /** The elicitation policy (see {@link setElicitationOptions}). */
1746
1929
  getElicitationOptions() {
1747
- return { allowOther: this._elicitationAllowOther, layout: this._elicitationLayout };
1930
+ return { allowOther: this._elicitationAllowOther, layout: this._elicitationLayout, answerOnClick: this._elicitationAnswerOnClick };
1748
1931
  }
1749
1932
  /**
1750
1933
  * Render one field of the question panel yourself.
@@ -1937,6 +2120,38 @@ class AparteConfig {
1937
2120
  getToolRenderer(toolName) {
1938
2121
  return this._toolRenderers.get(toolName);
1939
2122
  }
2123
+ /**
2124
+ * Teach the stream parser a tagged block: `<tag attr="…">…</tag>` in the model's
2125
+ * prose becomes the segment `toSegment` builds, streamed delta by delta.
2126
+ *
2127
+ * The parser does the work once for every grammar — the earliest opening tag wins
2128
+ * against a code fence and a reasoning delimiter, a tag cut at a chunk boundary is
2129
+ * held back, attributes are parsed quoted or bare, and the block is closed at
2130
+ * `</tag>` or at the end of the stream. What the segment IS is yours: a `type` of
2131
+ * your own with a renderer from `registerSegmentRenderer`, or a built-in one. One
2132
+ * grammar per tag; registering the same tag again replaces it.
2133
+ *
2134
+ * Read by the stream adapter when a turn starts, so a block registered mid-turn
2135
+ * applies from the next turn. See {@link AparteStreamBlock}.
2136
+ *
2137
+ * @example
2138
+ * aparteGlobalConfig.registerStreamBlock({
2139
+ * tag: 'cite',
2140
+ * toSegment: ({ attrs, id }) => ({ id, type: 'citation', url: attrs['url'] ?? '', content: '' }),
2141
+ * });
2142
+ */
2143
+ registerStreamBlock(block) {
2144
+ this._streamBlocks.set(block.tag, block);
2145
+ this._notify();
2146
+ }
2147
+ /** Forget a block grammar by its tag. */
2148
+ unregisterStreamBlock(tag) {
2149
+ if (this._streamBlocks.delete(tag)) this._notify();
2150
+ }
2151
+ /** The registered block grammars, in registration order. */
2152
+ getStreamBlocks() {
2153
+ return Array.from(this._streamBlocks.values());
2154
+ }
1940
2155
  // ─────────────────────────────────────────────────────────────────────────
1941
2156
  // Conversation Manager (optional, agnostic persistence layer)
1942
2157
  // ─────────────────────────────────────────────────────────────────────────
@@ -2059,7 +2274,7 @@ class AparteConfig {
2059
2274
  if (!this._warnedNoPresenter) {
2060
2275
  this._warnedNoPresenter = true;
2061
2276
  console.warn(
2062
- "[aparte] requestUserInput() was called with no elicitation presenter, so it rejected with an AbortError — nothing was ever shown to anybody. Add <aparte-elicitation></aparte-elicitation> inside your <aparte-chat>, or register your own by calling setElicitationPresenter() on the config this chat resolves — the scoped one if you passed a `config`, aparteGlobalConfig otherwise."
2277
+ "[aparte] requestUserInput() was called with no elicitation presenter, so it rejected with an AbortError — nothing was ever shown to anybody. Add <aparte-elicitation></aparte-elicitation> inside your <aparte-chat> (the Angular wrapper's tag too) — or, under the React/Vue/Svelte wrappers, inside their [data-aparte-chat] host (every wrapper renders it by default, unless you passed `elicitation={false}`) — or register your own by calling setElicitationPresenter() on the config this chat resolves — the scoped one if you passed a `config`, aparteGlobalConfig otherwise."
2063
2278
  );
2064
2279
  }
2065
2280
  return Promise.reject(new AparteElicitationAbortError("no-presenter"));
@@ -2087,7 +2302,6 @@ class AparteConfig {
2087
2302
  this._iconProvider = void 0;
2088
2303
  this._segmentDefaults.clear();
2089
2304
  this._avatarProvider = void 0;
2090
- this._artifactPreviewBuilder = void 0;
2091
2305
  this._keyProvider = void 0;
2092
2306
  this._conversationManager = void 0;
2093
2307
  this._elicitationPresenters = [];
@@ -2099,12 +2313,15 @@ class AparteConfig {
2099
2313
  this._fetchedModels.clear();
2100
2314
  this._tools.clear();
2101
2315
  this._toolRenderers.clear();
2316
+ this._streamBlocks.clear();
2102
2317
  this._modelConfig = {};
2103
2318
  this._requireModelSelection = false;
2104
2319
  this._elicitationAllowOther = true;
2105
2320
  this._elicitationLayout = "stepped";
2321
+ this._elicitationAnswerOnClick = true;
2106
2322
  this._elicitationFieldRenderer = void 0;
2107
2323
  this._modelPreferenceProvider = void 0;
2324
+ this._approvalPolicy = null;
2108
2325
  this._bubbleActionsConfig = { ...APARTE_DEFAULT_BUBBLE_ACTIONS };
2109
2326
  this._hostHandlers = { ...APARTE_DEFAULT_HOST_HANDLERS };
2110
2327
  this._notify();
@@ -2287,6 +2504,40 @@ const thinkingRenderer = {
2287
2504
  },
2288
2505
  getStyles: () => ``
2289
2506
  };
2507
+ function copyText(text) {
2508
+ if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
2509
+ return navigator.clipboard.writeText(text);
2510
+ }
2511
+ return new Promise((resolve, reject) => {
2512
+ if (typeof document === "undefined" || typeof document.execCommand !== "function") {
2513
+ reject(new Error("[aparte] No clipboard available in this context"));
2514
+ return;
2515
+ }
2516
+ const area = document.createElement("textarea");
2517
+ area.value = text;
2518
+ area.setAttribute("readonly", "");
2519
+ area.setAttribute("aria-hidden", "true");
2520
+ area.style.position = "fixed";
2521
+ area.style.top = "0";
2522
+ area.style.left = "-9999px";
2523
+ area.style.opacity = "0";
2524
+ const active = document.activeElement;
2525
+ document.body.appendChild(area);
2526
+ area.focus();
2527
+ area.select();
2528
+ area.setSelectionRange(0, text.length);
2529
+ let copied = false;
2530
+ try {
2531
+ copied = document.execCommand("copy");
2532
+ } catch {
2533
+ copied = false;
2534
+ }
2535
+ area.remove();
2536
+ if (active instanceof HTMLElement) active.focus();
2537
+ if (copied) resolve();
2538
+ else reject(new Error("[aparte] Clipboard write failed"));
2539
+ });
2540
+ }
2290
2541
  const HIGHLIGHT_DEBOUNCE_MS = 400;
2291
2542
  const MAX_THROTTLE_ENTRIES = 256;
2292
2543
  const _lastHighlightAt = /* @__PURE__ */ new Map();
@@ -2369,7 +2620,7 @@ const codeRenderer = {
2369
2620
  if (copyBtn) {
2370
2621
  copyBtn.addEventListener("click", () => {
2371
2622
  const source = element.querySelector(".aparte-code-content-wrapper")?.textContent ?? "";
2372
- void navigator.clipboard.writeText(source).catch(() => {
2623
+ void copyText(source).catch(() => {
2373
2624
  });
2374
2625
  copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
2375
2626
  copyBtn.setAttribute("title", contextConfig(copyBtn).t("copied"));
@@ -2439,8 +2690,7 @@ const errorRenderer = {
2439
2690
  },
2440
2691
  getStyles: () => ``
2441
2692
  };
2442
- function describeInput(segment) {
2443
- const input = segment.toolCall?.input;
2693
+ function describeToolInput(input) {
2444
2694
  if (!input || typeof input !== "object" || Object.keys(input).length === 0) return "";
2445
2695
  try {
2446
2696
  return JSON.stringify(input, null, 2);
@@ -2448,6 +2698,9 @@ function describeInput(segment) {
2448
2698
  return "";
2449
2699
  }
2450
2700
  }
2701
+ function describeInput(segment) {
2702
+ return describeToolInput(segment.toolCall?.input);
2703
+ }
2451
2704
  function stateBadge(segment) {
2452
2705
  const cfg = contextConfig();
2453
2706
  const status = segment.status ?? "pending";
@@ -2457,6 +2710,7 @@ function stateBadge(segment) {
2457
2710
  if (status === "resolved") return `${cfg.getIcon("check")}${word("toolCompleted")}`;
2458
2711
  if (status === "rejected") return `${cfg.getIcon("close")}${word("toolRejected")}`;
2459
2712
  if (status === "aborted") return `${cfg.getIcon("stop")}${word("toolStopped")}`;
2713
+ if (status === "failed") return `${cfg.getIcon("close")}${word("toolFailed")}`;
2460
2714
  return "";
2461
2715
  }
2462
2716
  function setPart(element, part, text) {
@@ -2565,6 +2819,11 @@ const toolCallRenderer = {
2565
2819
  */
2566
2820
  relabel: (element, segment) => {
2567
2821
  const cfg = contextConfig();
2822
+ const custom = cfg.getToolRenderer(segment.toolCall?.name);
2823
+ if (custom) {
2824
+ custom.relabel?.(element, segment);
2825
+ return;
2826
+ }
2568
2827
  const icon = element.querySelector(".aparte-tool-icon");
2569
2828
  if (icon) icon.innerHTML = cfg.getIcon("tool");
2570
2829
  const statusEl = element.querySelector(".aparte-tool-state");
@@ -2588,6 +2847,10 @@ const toolCallRenderer = {
2588
2847
  const input = describeInput(segment);
2589
2848
  const wantsDetail = !!input || !!segment.result;
2590
2849
  const custom = contextConfig().getToolRenderer(segment.toolCall?.name);
2850
+ if (custom?.update) {
2851
+ custom.update(element, segment);
2852
+ return;
2853
+ }
2591
2854
  if (custom || wantsDetail !== (element.tagName === "DETAILS")) {
2592
2855
  const out = toolCallRenderer.render(segment);
2593
2856
  const tpl = document.createElement("template");
@@ -2617,709 +2880,6 @@ const toolCallRenderer = {
2617
2880
  // has no other way onto the page.
2618
2881
  getStyles: () => ""
2619
2882
  };
2620
- function stripCodeFences(content) {
2621
- let s = content;
2622
- if (s.startsWith("```") || s.startsWith("~~~")) {
2623
- const fenceChar = s[0];
2624
- let i = 0;
2625
- while (i < s.length && s[i] === fenceChar) i++;
2626
- while (i < s.length && s[i] !== "\n") i++;
2627
- if (i < s.length && s[i] === "\n") i++;
2628
- s = s.slice(i);
2629
- }
2630
- const closeAt = findClosingFence(s);
2631
- if (closeAt !== -1) s = s.slice(0, closeAt);
2632
- return s.trim();
2633
- }
2634
- function findClosingFence(s) {
2635
- let i = 0;
2636
- while (i < s.length) {
2637
- const lineStart = i;
2638
- let k = lineStart;
2639
- while (k < s.length && (s[k] === " " || s[k] === " ")) k++;
2640
- if (k < s.length && (s[k] === "`" || s[k] === "~")) {
2641
- const fenceChar = s[k];
2642
- let runs = 0;
2643
- while (k < s.length && s[k] === fenceChar) {
2644
- runs++;
2645
- k++;
2646
- }
2647
- if (runs >= 3) {
2648
- let onlyWs = true;
2649
- while (k < s.length && s[k] !== "\n") {
2650
- if (s[k] !== " " && s[k] !== " " && s[k] !== "\r") {
2651
- onlyWs = false;
2652
- break;
2653
- }
2654
- k++;
2655
- }
2656
- if (onlyWs) {
2657
- let cut = lineStart;
2658
- if (cut > 0 && s[cut - 1] === "\n") cut--;
2659
- return cut;
2660
- }
2661
- }
2662
- }
2663
- while (i < s.length && s[i] !== "\n") i++;
2664
- if (i < s.length) i++;
2665
- }
2666
- return -1;
2667
- }
2668
- function labelForKind(kind) {
2669
- switch (kind) {
2670
- case "react":
2671
- return "React component";
2672
- case "html":
2673
- return "HTML document";
2674
- case "svg":
2675
- return "SVG image";
2676
- case "js":
2677
- return "JavaScript snippet";
2678
- case "css":
2679
- return "CSS stylesheet";
2680
- case "json":
2681
- return "JSON document";
2682
- case "markdown":
2683
- return "Markdown document";
2684
- case "csv":
2685
- return "CSV table";
2686
- case "text":
2687
- return "Text file";
2688
- case "python":
2689
- return "Python script";
2690
- case "typescript":
2691
- return "TypeScript file";
2692
- case "bash":
2693
- return "Bash script";
2694
- case "sql":
2695
- return "SQL query";
2696
- case "pdf":
2697
- return "PDF generator";
2698
- case "xlsx":
2699
- return "Excel generator";
2700
- case "docx":
2701
- return "Word generator";
2702
- default:
2703
- return "Artifact";
2704
- }
2705
- }
2706
- const PREVIEW_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:";
2707
- function withMetaCsp(doc) {
2708
- const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttr(PREVIEW_CSP)}">`;
2709
- const doctype = doc.match(/^\s*<!doctype[^>]*>/i);
2710
- const at = doctype ? doctype[0].length : 0;
2711
- return `${doc.slice(0, at)}<head>${meta}</head>${doc.slice(at)}`;
2712
- }
2713
- function buildSafePreviewDocument(kind, body, title) {
2714
- return withMetaCsp(buildPreviewBody(kind, body, title));
2715
- }
2716
- function buildPreviewBody(kind, body, title) {
2717
- switch (kind) {
2718
- case "html": {
2719
- if (startsWithDoctype(body)) return body;
2720
- return `<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>${escapeAttr(title)}</title></head><body>${body}</body></html>`;
2721
- }
2722
- // An SVG with only a `viewBox` — the recommended, responsive form, and the one
2723
- // a model writes most often — has NO intrinsic size. As a flex item its cross
2724
- // size then collapses to zero and the frame is blank: the preview worked for an
2725
- // SVG that declared `width`/`height` and silently showed nothing for the
2726
- // idiomatic one. Reported from the landing the moment its demo payload became a
2727
- // real file instead of a hand-written chart with dimensions on it.
2728
- //
2729
- // The rule is narrowed by attribute selector rather than applied to every SVG,
2730
- // so an SVG that DOES state its size keeps the size it asked for.
2731
- case "svg":
2732
- return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
2733
- <style>html,body{margin:0;height:100%;display:flex;align-items:center;justify-content:center;background:#fff}svg{max-width:90%;max-height:90%}svg:not([width]):not([height]){width:90%;height:90%}</style>
2734
- </head><body>${body}</body></html>`;
2735
- case "js": {
2736
- const safeBody = escapeClosingScriptTag(body);
2737
- return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
2738
- <style>body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;padding:1rem;background:#fff;color:#0f172a}</style>
2739
- </head><body><div id="root"></div><script>
2740
- try { ${safeBody}
2741
- } catch (e) { document.getElementById('root').innerHTML = '<pre style="color:#b91c1c">' + (e && e.stack || e) + '</pre>'; }
2742
- <\/script></body></html>`;
2743
- }
2744
- case "css":
2745
- return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
2746
- <style>${body}</style></head><body>
2747
- <div class="demo">
2748
- <h1>Heading</h1>
2749
- <p>Paragraph with a <a href="#">link</a> and <strong>strong</strong> text.</p>
2750
- <button>Button</button>
2751
- <input placeholder="Input"/>
2752
- <ul><li>One</li><li>Two</li><li>Three</li></ul>
2753
- </div></body></html>`;
2754
- default:
2755
- return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
2756
- <style>body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;padding:1rem;background:#fff;color:#0f172a}pre{white-space:pre-wrap;word-break:break-word;margin:0}</style>
2757
- </head><body><pre>${escapeHtml(body)}</pre></body></html>`;
2758
- }
2759
- }
2760
- function startsWithDoctype(s) {
2761
- let i = 0;
2762
- while (i < s.length && (s[i] === " " || s[i] === " " || s[i] === "\n" || s[i] === "\r")) i++;
2763
- const probe = s.slice(i, i + 9).toLowerCase();
2764
- return probe === "<!doctype";
2765
- }
2766
- function escapeClosingScriptTag(body) {
2767
- let out = "";
2768
- let i = 0;
2769
- while (i < body.length) {
2770
- if (body[i] === "<" && body.slice(i, i + 8).toLowerCase() === "<\/script") {
2771
- const next = body[i + 8];
2772
- if (next === void 0 || next === "/" || next === ">" || /\s/.test(next)) {
2773
- out += "<\\/script";
2774
- i += 8;
2775
- continue;
2776
- }
2777
- }
2778
- out += body[i];
2779
- i++;
2780
- }
2781
- return out;
2782
- }
2783
- const FILE_ICON_LABEL = {
2784
- xlsx: "XLS",
2785
- pdf: "PDF",
2786
- docx: "DOC"
2787
- };
2788
- const _binaryArtifactCache = /* @__PURE__ */ new Map();
2789
- const MAX_BINARY_ARTIFACTS = 24;
2790
- function cacheBinaryArtifact(id, entry) {
2791
- _binaryArtifactCache.delete(id);
2792
- if (_binaryArtifactCache.size >= MAX_BINARY_ARTIFACTS) {
2793
- const oldest = _binaryArtifactCache.keys().next().value;
2794
- if (oldest !== void 0) _binaryArtifactCache.delete(oldest);
2795
- }
2796
- _binaryArtifactCache.set(id, entry);
2797
- }
2798
- const _fileGenHandlers = /* @__PURE__ */ new Map();
2799
- const MAX_FILE_GEN_HANDLERS = 32;
2800
- const _lastDispatchAt = /* @__PURE__ */ new Map();
2801
- const RE_DISPATCH_DEBOUNCE_MS = 3e4;
2802
- let _artifactReadyHookInstalled = false;
2803
- function installArtifactReadyHook() {
2804
- if (_artifactReadyHookInstalled || typeof window === "undefined") return;
2805
- _artifactReadyHookInstalled = true;
2806
- window.addEventListener("aparte-artifact-ready", (event) => {
2807
- const segId = event.detail?.segmentId;
2808
- if (segId) markThrottle(_lastDispatchAt, segId, Date.now());
2809
- });
2810
- }
2811
- function renderBinaryFileArtifact(segment, kind) {
2812
- const title = segment.title?.trim() || labelForKind(kind);
2813
- const iconLabel = FILE_ICON_LABEL[kind] ?? kind.toUpperCase();
2814
- const isStreaming = !!segment.isStreaming;
2815
- const canRedownload = contextConfig().getHostHandlers().artifactRedownload;
2816
- const downloadLabel = escapeHtml(contextConfig().t("download"));
2817
- const cached = _binaryArtifactCache.get(segment.id);
2818
- if (cached && !isStreaming) {
2819
- const previewBody = cached.previewHtml ? contextConfig().sanitizeHtml(cached.previewHtml) : `<div class="aparte-art-file__preview-empty">Preview not available for ${escapeHtml(kind)} yet</div>`;
2820
- return `
2821
- <div class="aparte-segment aparte-card aparte-segment-artifact-file"
2822
- data-segment-id="${escapeHtml(segment.id)}"
2823
- data-artifact-type="${escapeHtml(kind)}"
2824
- data-state="ready">
2825
- <div class="aparte-art-file__card">
2826
- <div class="aparte-art-file__icon" data-kind="${escapeHtml(kind)}">${escapeHtml(iconLabel)}</div>
2827
- <div class="aparte-art-file__meta">
2828
- <div class="aparte-art-file__meta-name" data-role="file-name">${escapeHtml(cached.name)}</div>
2829
- <div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(formatBytes(cached.bytes))} · ${escapeHtml(kind.toUpperCase())}</div>
2830
- </div>
2831
- <div class="aparte-art-file__actions">
2832
- ${canRedownload ? `<button type="button" class="aparte-btn aparte-btn--primary aparte-btn--solid aparte-art-file__btn aparte-art-file__btn--primary" data-action="download">${downloadLabel}</button>` : ""}
2833
- </div>
2834
- </div>
2835
- <div class="aparte-art-file__body">
2836
- <div class="aparte-art-file__code-pane" data-role="code-pane" hidden>
2837
- <pre><code class="language-js"></code></pre>
2838
- </div>
2839
- <div class="aparte-art-file__preview-pane" data-role="preview-pane">${previewBody}</div>
2840
- </div>
2841
- </div>
2842
- `;
2843
- }
2844
- const cleanContent = stripCodeFences(segment.content || "");
2845
- const subText = isStreaming ? contextConfig().t("generating") : contextConfig().t("rebuildingPreview");
2846
- return `
2847
- <div class="aparte-segment aparte-card aparte-segment-artifact-file"
2848
- data-segment-id="${escapeHtml(segment.id)}"
2849
- data-artifact-type="${escapeHtml(kind)}"
2850
- data-state="${isStreaming ? "streaming" : "compiling"}">
2851
- <div class="aparte-art-file__card">
2852
- <div class="aparte-art-file__icon" data-kind="${escapeHtml(kind)}">${escapeHtml(iconLabel)}</div>
2853
- <div class="aparte-art-file__meta">
2854
- <div class="aparte-art-file__meta-name" data-role="file-name">${escapeHtml(title)}</div>
2855
- <div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(subText)}</div>
2856
- </div>
2857
- <div class="aparte-art-file__actions">
2858
- ${canRedownload ? `<button type="button" class="aparte-btn aparte-btn--primary aparte-btn--solid aparte-art-file__btn aparte-art-file__btn--primary" data-action="download" disabled>${downloadLabel}</button>` : ""}
2859
- </div>
2860
- </div>
2861
- <div class="aparte-art-file__body">
2862
- <div class="aparte-art-file__code-pane" data-role="code-pane">
2863
- <pre><code class="language-js">${escapeHtml(cleanContent)}</code></pre>
2864
- </div>
2865
- <div class="aparte-art-file__preview-pane" data-role="preview-pane" hidden></div>
2866
- </div>
2867
- </div>
2868
- `;
2869
- }
2870
- function setupBinaryFileArtifact(element, segment, kind) {
2871
- if (element.dataset["aparteInit"] !== "true") {
2872
- element.dataset["aparteInit"] = "true";
2873
- _fileGenHandlers.get(segment.id)?.();
2874
- const cleanup = () => {
2875
- window.removeEventListener("aparte-file-gen-ready", onReady);
2876
- window.removeEventListener("aparte-file-gen-error", onError);
2877
- _fileGenHandlers.delete(segment.id);
2878
- };
2879
- const onReady = (event) => {
2880
- const detail = event.detail;
2881
- if (!detail || detail.segmentId !== segment.id) return;
2882
- cacheBinaryArtifact(segment.id, {
2883
- buffer: detail.buffer,
2884
- mime: detail.mime,
2885
- name: detail.filename,
2886
- bytes: detail.bytes,
2887
- previewHtml: detail.previewHtml
2888
- });
2889
- swapToPreview(element, detail, kind);
2890
- cleanup();
2891
- };
2892
- window.addEventListener("aparte-file-gen-ready", onReady);
2893
- const onError = (event) => {
2894
- const detail = event.detail;
2895
- if (!detail || detail.segmentId !== segment.id) return;
2896
- showSandboxError(element, detail.phase ?? "exec", detail.error ?? "Unknown error");
2897
- cleanup();
2898
- };
2899
- window.addEventListener("aparte-file-gen-error", onError);
2900
- if (_fileGenHandlers.size >= MAX_FILE_GEN_HANDLERS) {
2901
- const oldest = _fileGenHandlers.keys().next().value;
2902
- if (oldest !== void 0) _fileGenHandlers.get(oldest)?.();
2903
- }
2904
- _fileGenHandlers.set(segment.id, cleanup);
2905
- element.addEventListener("click", (ev) => {
2906
- const target = ev.target;
2907
- const action = target.closest("[data-action]")?.getAttribute("data-action");
2908
- if (action !== "download") return;
2909
- const cached2 = _binaryArtifactCache.get(segment.id);
2910
- if (!cached2) return;
2911
- const blob = new Blob([cached2.buffer], { type: cached2.mime });
2912
- const url = URL.createObjectURL(blob);
2913
- const a = document.createElement("a");
2914
- a.href = url;
2915
- a.download = cached2.name;
2916
- a.style.display = "none";
2917
- document.body.appendChild(a);
2918
- a.click();
2919
- document.body.removeChild(a);
2920
- setTimeout(() => URL.revokeObjectURL(url), 1e3);
2921
- });
2922
- }
2923
- const currentState = element.getAttribute("data-state");
2924
- if (segment.isStreaming || currentState === "ready") return;
2925
- const cached = _binaryArtifactCache.get(segment.id);
2926
- if (cached) {
2927
- swapToPreview(
2928
- element,
2929
- { filename: cached.name, bytes: cached.bytes, previewHtml: cached.previewHtml },
2930
- kind
2931
- );
2932
- return;
2933
- }
2934
- if (!contextConfig().getHostHandlers().artifactRehydrate) return;
2935
- const lastAt = _lastDispatchAt.get(segment.id) ?? 0;
2936
- if (Date.now() - lastAt < RE_DISPATCH_DEBOUNCE_MS) return;
2937
- markThrottle(_lastDispatchAt, segment.id, Date.now());
2938
- const reloadMessageId = `__reload__${Date.now()}`;
2939
- queueMicrotask(() => {
2940
- window.dispatchEvent(new CustomEvent("aparte-artifact-ready", {
2941
- detail: {
2942
- messageId: reloadMessageId,
2943
- segmentId: segment.id,
2944
- mimeType: segment.mimeType,
2945
- artifactType: segment.artifactType,
2946
- title: segment.title,
2947
- content: stripCodeFences(segment.content || "")
2948
- }
2949
- }));
2950
- });
2951
- const wrapper = element.querySelector('[data-role="code-pane"]');
2952
- if (wrapper) {
2953
- const cleanContent = stripCodeFences(segment.content || "");
2954
- void contextConfig(element).highlightCode(cleanContent, "js").then((html) => {
2955
- wrapper.innerHTML = html;
2956
- }).catch(() => {
2957
- });
2958
- }
2959
- }
2960
- function updateBinaryFileArtifact(element, segment, isStreaming) {
2961
- const state = element.getAttribute("data-state");
2962
- if (state === "ready") return;
2963
- const cleanContent = stripCodeFences(segment.content || "");
2964
- if (isStreaming) {
2965
- streamHighlight(element, '[data-role="code-pane"]', cleanContent, "js", segment.id);
2966
- } else {
2967
- const codeEl = element.querySelector('[data-role="code-pane"] code');
2968
- if (codeEl) codeEl.textContent = cleanContent;
2969
- }
2970
- if (!isStreaming && state === "streaming") {
2971
- element.setAttribute("data-state", "compiling");
2972
- const wrapper = element.querySelector('[data-role="code-pane"]');
2973
- if (wrapper) {
2974
- void contextConfig(element).highlightCode(cleanContent, "js").then((html) => {
2975
- wrapper.innerHTML = html;
2976
- }).catch(() => {
2977
- });
2978
- }
2979
- const sub = element.querySelector('[data-role="file-sub"]');
2980
- if (sub) sub.textContent = "Running sandbox…";
2981
- }
2982
- }
2983
- function swapToPreview(element, detail, kind) {
2984
- element.setAttribute("data-state", "ready");
2985
- const codePane = element.querySelector('[data-role="code-pane"]');
2986
- if (codePane) codePane.hidden = true;
2987
- const preview = element.querySelector('[data-role="preview-pane"]');
2988
- if (preview) {
2989
- preview.innerHTML = detail.previewHtml ? contextConfig().sanitizeHtml(detail.previewHtml) : `<div class="aparte-art-file__preview-empty">Preview not available for ${escapeHtml(kind)} yet</div>`;
2990
- preview.hidden = false;
2991
- }
2992
- const nameEl = element.querySelector('[data-role="file-name"]');
2993
- if (nameEl) nameEl.textContent = detail.filename;
2994
- const sub = element.querySelector('[data-role="file-sub"]');
2995
- if (sub) sub.textContent = `${formatBytes(detail.bytes)} · ${kind.toUpperCase()}`;
2996
- const dlBtn = element.querySelector('[data-action="download"]');
2997
- if (dlBtn) dlBtn.disabled = false;
2998
- }
2999
- function formatBytes(n) {
3000
- if (n < 1024) return `${n} B`;
3001
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
3002
- return `${(n / (1024 * 1024)).toFixed(2)} MB`;
3003
- }
3004
- function showSandboxError(element, phase, errorMsg) {
3005
- const state = element.getAttribute("data-state");
3006
- if (state === "ready") return;
3007
- element.setAttribute("data-state", "error");
3008
- const sub = element.querySelector('[data-role="file-sub"]');
3009
- if (sub) sub.textContent = `Error (${phase})`;
3010
- const body = element.querySelector(".aparte-art-file__body");
3011
- if (body) {
3012
- const short = (errorMsg.split("\n")[0] ?? "").slice(0, 240);
3013
- body.innerHTML = `
3014
- <div class="aparte-art-file__error">
3015
- <div class="aparte-art-file__error-title">${escapeHtml(contextConfig().t("sandboxError"))}</div>
3016
- <div class="aparte-output aparte-art-file__error-msg">${escapeHtml(short)}</div>
3017
- <div class="aparte-art-file__error-hint">${escapeHtml(contextConfig().t("sandboxErrorHint"))}</div>
3018
- </div>
3019
- `;
3020
- }
3021
- }
3022
- const PREVIEWABLE_KINDS = /* @__PURE__ */ new Set(["react", "html", "svg", "js", "css"]);
3023
- const BINARY_FILE_KINDS = /* @__PURE__ */ new Set(["pdf", "xlsx", "docx"]);
3024
- const artifactRenderer = {
3025
- type: "artifact",
3026
- render: (segment) => {
3027
- const kind = (segment.artifactType || "unknown").toLowerCase();
3028
- if (BINARY_FILE_KINDS.has(kind)) {
3029
- return renderBinaryFileArtifact(segment, kind);
3030
- }
3031
- const title = segment.title?.trim() || labelForKind(kind);
3032
- const displayLang = languageForKind(kind);
3033
- const isStreaming = !!segment.isStreaming;
3034
- const previewable = PREVIEWABLE_KINDS.has(kind);
3035
- const cfg = contextConfig();
3036
- const downloadLabel = cfg.t("download");
3037
- const previewLabel = cfg.t("preview");
3038
- const codeLabel = cfg.t("code");
3039
- const isBinary = BINARY_FILE_KINDS.has(kind);
3040
- const cleanContent = stripCodeFences(segment.content || "");
3041
- const cardId = escapeAttr(segment.id);
3042
- return `
3043
- <div class="aparte-segment aparte-card aparte-segment-artifact-card"
3044
- data-segment-id="${escapeHtml(segment.id)}"
3045
- data-artifact-type="${escapeHtml(kind)}"
3046
- data-streaming="${isStreaming ? "true" : "false"}"
3047
- data-tab="code"
3048
- data-previewable="${previewable ? "true" : "false"}"
3049
- data-binary="${isBinary ? "true" : "false"}">
3050
- <header class="aparte-art-card__header">
3051
- <div class="aparte-art-card__title-block">
3052
- <span class="aparte-badge aparte-badge--outline aparte-art-card__kind" data-kind="${escapeHtml(kind)}">${escapeHtml(displayLang)}</span>
3053
- <span class="aparte-art-card__title">${escapeHtml(title)}</span>
3054
- ${isStreaming ? `<span class="aparte-dot aparte-art-card__pulse" role="img" aria-label="${escapeAttr(cfg.t("generating"))}"></span>` : ""}
3055
- </div>
3056
- <div class="aparte-art-card__actions">
3057
- <button type="button" class="aparte-btn aparte-btn--icon aparte-art-card__btn" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}" aria-label="${escapeAttr(contextConfig().t("copy"))}">
3058
- ${contextConfig().getIcon("copy")}
3059
- </button>
3060
- <button type="button" class="aparte-btn aparte-btn--icon aparte-art-card__btn" data-action="download" title="${escapeAttr(downloadLabel)}" aria-label="${escapeAttr(downloadLabel)}" ${isStreaming ? "disabled" : ""}>
3061
- ${contextConfig().getIcon("download")}
3062
- </button>
3063
- </div>
3064
- </header>
3065
- <nav class="aparte-tabs aparte-tabs--underline aparte-art-card__tabs" role="tablist">
3066
- <button type="button" class="aparte-tabs__tab" role="tab" id="aparte-art-${cardId}-tab-code" aria-controls="aparte-art-${cardId}-pane-code" aria-selected="true" tabindex="0" data-tab-target="code">${escapeHtml(codeLabel)}</button>
3067
- ${previewable ? `<button type="button" class="aparte-tabs__tab" role="tab" id="aparte-art-${cardId}-tab-preview" aria-controls="aparte-art-${cardId}-pane-preview" aria-selected="false" tabindex="-1" data-tab-target="preview" ${isStreaming ? "disabled" : ""}>${escapeHtml(previewLabel)}</button>` : ""}
3068
- </nav>
3069
- <div class="aparte-art-card__body">
3070
- <div class="aparte-art-card__pane" role="tabpanel" id="aparte-art-${cardId}-pane-code" aria-labelledby="aparte-art-${cardId}-tab-code" tabindex="0" data-pane="code">
3071
- <div class="aparte-code-content-wrapper">
3072
- <pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>
3073
- </div>
3074
- </div>
3075
- ${previewable ? `
3076
- <div class="aparte-art-card__pane" role="tabpanel" id="aparte-art-${cardId}-pane-preview" aria-labelledby="aparte-art-${cardId}-tab-preview" tabindex="0" data-pane="preview">
3077
- <div class="aparte-art-card__pending">${escapeHtml(cfg.t("previewPending"))}</div>
3078
- </div>
3079
- ` : ""}
3080
- </div>
3081
- </div>
3082
- `;
3083
- },
3084
- /**
3085
- * Every string this card shows: the copy button's tooltip, glyph and accessible
3086
- * name, the download button's title and label, and the two tab names. They were
3087
- * all hardcoded literals until they got locale keys — including the copy button's
3088
- * `aria-label`, which said "Copy" while its own `title` one attribute away already
3089
- * went through `t('copy')`, so a French reader got a French tooltip and an English
3090
- * announcement.
3091
- *
3092
- * Nothing here touches the tab state or the preview pane: a mounted iframe is
3093
- * running model-authored code, and re-rendering this card is exactly what the
3094
- * hook exists to avoid.
3095
- */
3096
- relabel: (element) => {
3097
- const cfg = contextConfig();
3098
- const copyBtn = element.querySelector('.aparte-art-card__btn[data-action="copy"]');
3099
- if (copyBtn) {
3100
- copyBtn.setAttribute("title", cfg.t("copy"));
3101
- copyBtn.setAttribute("aria-label", cfg.t("copy"));
3102
- copyBtn.innerHTML = cfg.getIcon("copy");
3103
- }
3104
- const dl = element.querySelector('.aparte-art-card__btn[data-action="download"]');
3105
- if (dl) {
3106
- const label = cfg.t("download");
3107
- dl.setAttribute("title", label);
3108
- dl.setAttribute("aria-label", label);
3109
- }
3110
- const previewTab = element.querySelector('[data-tab-target="preview"]');
3111
- if (previewTab) previewTab.textContent = cfg.t("preview");
3112
- const codeTab = element.querySelector('[data-tab-target="code"]');
3113
- if (codeTab) codeTab.textContent = cfg.t("code");
3114
- },
3115
- setup: (element, segment) => {
3116
- latestSegment.set(element, segment);
3117
- const kind = (segment.artifactType || "").toLowerCase();
3118
- if (BINARY_FILE_KINDS.has(kind)) {
3119
- setupBinaryFileArtifact(element, segment, kind);
3120
- return;
3121
- }
3122
- const wrapper = element.querySelector(".aparte-code-content-wrapper");
3123
- if (wrapper) {
3124
- const displayLang = languageForKind(kind);
3125
- const cleanContent = stripCodeFences(segment.content || "");
3126
- void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
3127
- wrapper.innerHTML = html;
3128
- }).catch(() => {
3129
- });
3130
- }
3131
- const tabs = [...element.querySelectorAll("[data-tab-target]")];
3132
- const select = (btn, focus) => {
3133
- const target = btn.getAttribute("data-tab-target");
3134
- if (!target || btn.disabled) return;
3135
- if (target === "preview") mountPreviewFrame(element, segment);
3136
- element.setAttribute("data-tab", target);
3137
- for (const b of tabs) {
3138
- const on = b === btn;
3139
- b.setAttribute("aria-selected", on ? "true" : "false");
3140
- b.tabIndex = on ? 0 : -1;
3141
- }
3142
- if (focus) btn.focus();
3143
- };
3144
- for (const [i, btn] of tabs.entries()) {
3145
- btn.addEventListener("click", () => select(btn, false));
3146
- btn.addEventListener("keydown", (e) => {
3147
- const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
3148
- let next = -1;
3149
- if (step) next = (i + step + tabs.length) % tabs.length;
3150
- else if (e.key === "Home") next = 0;
3151
- else if (e.key === "End") next = tabs.length - 1;
3152
- if (next < 0) return;
3153
- e.preventDefault();
3154
- for (let n = 0; n < tabs.length; n++) {
3155
- const candidate = tabs[(next + n * (step || 1) + tabs.length) % tabs.length];
3156
- if (candidate && !candidate.disabled) {
3157
- select(candidate, true);
3158
- return;
3159
- }
3160
- }
3161
- });
3162
- }
3163
- const copyBtn = element.querySelector('[data-action="copy"]');
3164
- if (copyBtn) {
3165
- copyBtn.addEventListener("click", () => {
3166
- const code = stripCodeFences(segment.content || "");
3167
- void navigator.clipboard.writeText(code).catch(() => {
3168
- });
3169
- const original = copyBtn.innerHTML;
3170
- copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
3171
- copyBtn.setAttribute("title", contextConfig(copyBtn).t("copied"));
3172
- setTimeout(() => {
3173
- copyBtn.innerHTML = original;
3174
- copyBtn.setAttribute("title", contextConfig(copyBtn).t("copy"));
3175
- }, 1500);
3176
- });
3177
- }
3178
- const dlBtn = element.querySelector('[data-action="download"]');
3179
- if (dlBtn) {
3180
- dlBtn.addEventListener("click", () => {
3181
- if (dlBtn.disabled) return;
3182
- const kind2 = (segment.artifactType || "").toLowerCase();
3183
- const isBinary = BINARY_FILE_KINDS.has(kind2);
3184
- if (isBinary) {
3185
- element.dispatchEvent(new CustomEvent("aparte-artifact-redownload", {
3186
- bubbles: true,
3187
- composed: true,
3188
- detail: {
3189
- segmentId: segment.id,
3190
- mimeType: segment.mimeType,
3191
- artifactType: segment.artifactType,
3192
- title: segment.title,
3193
- content: stripCodeFences(segment.content || "")
3194
- }
3195
- }));
3196
- return;
3197
- }
3198
- downloadTextArtifact(segment);
3199
- });
3200
- }
3201
- },
3202
- update: (element, segment) => {
3203
- const previous = latestSegment.get(element);
3204
- latestSegment.set(element, segment);
3205
- if (previous && previous.content !== segment.content) {
3206
- element.querySelector('.aparte-art-card__pane[data-pane="preview"] iframe')?.remove();
3207
- }
3208
- const isStreaming = !!segment.isStreaming;
3209
- const kind = (segment.artifactType || "").toLowerCase();
3210
- if (BINARY_FILE_KINDS.has(kind)) {
3211
- updateBinaryFileArtifact(element, segment, isStreaming);
3212
- return;
3213
- }
3214
- const wasStreaming = element.getAttribute("data-streaming") === "true";
3215
- const cleanContent = stripCodeFences(segment.content || "");
3216
- if (isStreaming) {
3217
- const segId = element.getAttribute("data-segment-id") ?? segment.id;
3218
- streamHighlight(element, ".aparte-code-content-wrapper", cleanContent, languageForKind(kind), segId);
3219
- } else {
3220
- const codeEl = element.querySelector(".aparte-code-content-wrapper code");
3221
- if (codeEl) {
3222
- codeEl.textContent = cleanContent;
3223
- } else {
3224
- const wrapper = element.querySelector(".aparte-code-content-wrapper");
3225
- if (wrapper) {
3226
- const displayLang = languageForKind(kind);
3227
- wrapper.innerHTML = `<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>`;
3228
- }
3229
- }
3230
- }
3231
- if (wasStreaming && !isStreaming) {
3232
- element.setAttribute("data-streaming", "false");
3233
- element.querySelector(".aparte-art-card__pulse")?.remove();
3234
- const wrapper = element.querySelector(".aparte-code-content-wrapper");
3235
- if (wrapper) {
3236
- const displayLang = languageForKind(kind);
3237
- void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
3238
- wrapper.innerHTML = html;
3239
- }).catch(() => {
3240
- });
3241
- }
3242
- element.querySelectorAll("button[disabled]").forEach((b) => {
3243
- b.disabled = false;
3244
- });
3245
- }
3246
- },
3247
- // CSS lives in styles/segment/ — see that folder for why a
3248
- // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
3249
- // has no other way onto the page.
3250
- getStyles: () => ""
3251
- };
3252
- function languageForKind(kind) {
3253
- if (kind === "react") return "jsx";
3254
- if (kind === "markdown") return "md";
3255
- if (kind === "pdf" || kind === "xlsx" || kind === "docx") return "js";
3256
- return kind || "text";
3257
- }
3258
- function downloadTextArtifact(segment) {
3259
- const content = stripCodeFences(segment.content || "");
3260
- const kind = (segment.artifactType || "").toLowerCase();
3261
- const ext = {
3262
- react: "jsx",
3263
- html: "html",
3264
- svg: "svg",
3265
- js: "js",
3266
- css: "css",
3267
- json: "json",
3268
- markdown: "md",
3269
- csv: "csv",
3270
- text: "txt",
3271
- python: "py",
3272
- typescript: "ts",
3273
- bash: "sh",
3274
- sql: "sql"
3275
- }[kind] ?? "txt";
3276
- const baseTitle = (segment.title ?? labelForKind(kind)).trim();
3277
- const safeBase = slugifyForFilename(baseTitle) || "artifact";
3278
- const filename = `${safeBase}.${ext}`;
3279
- const mime = segment.mimeType || "text/plain";
3280
- const blob = new Blob([content], { type: mime });
3281
- const url = URL.createObjectURL(blob);
3282
- const a = document.createElement("a");
3283
- a.href = url;
3284
- a.download = filename;
3285
- a.style.display = "none";
3286
- document.body.appendChild(a);
3287
- a.click();
3288
- document.body.removeChild(a);
3289
- setTimeout(() => URL.revokeObjectURL(url), 1e3);
3290
- }
3291
- function slugifyForFilename(text) {
3292
- const lower = text.trim().toLowerCase();
3293
- let out = "";
3294
- let prevDash = false;
3295
- for (let i = 0; i < lower.length && out.length < 40; i++) {
3296
- const ch = lower[i];
3297
- const isAlnum = ch >= "a" && ch <= "z" || ch >= "0" && ch <= "9";
3298
- if (isAlnum) {
3299
- out += ch;
3300
- prevDash = false;
3301
- continue;
3302
- }
3303
- if (!prevDash && out.length > 0) {
3304
- out += "-";
3305
- prevDash = true;
3306
- }
3307
- }
3308
- if (out.endsWith("-")) out = out.slice(0, -1);
3309
- return out;
3310
- }
3311
- const latestSegment = /* @__PURE__ */ new WeakMap();
3312
- function mountPreviewFrame(element, fallback) {
3313
- const pane = element.querySelector('.aparte-art-card__pane[data-pane="preview"]');
3314
- if (!pane || pane.querySelector("iframe")) return;
3315
- const segment = latestSegment.get(element) ?? fallback;
3316
- const kind = segment.artifactType || deriveArtifactKind(segment.mimeType ?? "", "text");
3317
- if (!PREVIEWABLE_KINDS.has(kind)) return;
3318
- const title = segment.title?.trim() || labelForKind(kind);
3319
- const build = contextConfig().getArtifactPreviewBuilder() ?? buildSafePreviewDocument;
3320
- const srcdoc = build(kind, stripCodeFences(segment.content || ""), title);
3321
- pane.innerHTML = `<iframe class="aparte-art-card__frame" sandbox="allow-scripts" csp="${escapeAttr(PREVIEW_CSP)}" referrerpolicy="no-referrer" loading="lazy" title="${escapeAttr(title)}" srcdoc="${escapeAttr(srcdoc)}"></iframe>`;
3322
- }
3323
2883
  const registries = /* @__PURE__ */ new WeakMap();
3324
2884
  function registryFor(config) {
3325
2885
  let reg = registries.get(config);
@@ -3352,7 +2912,6 @@ function installDefaultRenderersOnce(config = contextConfig()) {
3352
2912
  if (!reg.renderers.has(renderer.type)) reg.renderers.set(renderer.type, renderer);
3353
2913
  }
3354
2914
  injectRendererStyles(config);
3355
- installArtifactReadyHook();
3356
2915
  }
3357
2916
  function unregisterSegmentRenderer(type, config = contextConfig()) {
3358
2917
  registryFor(config).renderers.delete(type);
@@ -3402,15 +2961,13 @@ function registerDefaultRenderers(config = contextConfig()) {
3402
2961
  if (!reg.renderers.has(renderer.type)) registerSegmentRenderer(renderer, config);
3403
2962
  }
3404
2963
  injectRendererStyles(config);
3405
- installArtifactReadyHook();
3406
2964
  }
3407
2965
  const DEFAULT_RENDERERS = [
3408
2966
  textRenderer,
3409
2967
  thinkingRenderer,
3410
2968
  codeRenderer,
3411
2969
  errorRenderer,
3412
- toolCallRenderer,
3413
- artifactRenderer
2970
+ toolCallRenderer
3414
2971
  ];
3415
2972
  const OPEN_TOOL_STATUSES = /* @__PURE__ */ new Set(["pending", "awaiting-approval"]);
3416
2973
  const ACTIVITY_KEYS = ["content", "output", "result"];
@@ -3733,6 +3290,37 @@ class AparteConversationManager {
3733
3290
  }
3734
3291
  this._notify();
3735
3292
  }
3293
+ /**
3294
+ * Pin a conversation, which the list shows in its "Pinned" group first. Metadata
3295
+ * only, like archive: `updatedAt` keeps the conversation's real chronological slot
3296
+ * for the day it is unpinned. An adapter that has `pin()` gets the call; one that
3297
+ * does not gets the whole record through `save()`.
3298
+ */
3299
+ async pin(id) {
3300
+ const conv = this._find(id);
3301
+ if (!conv) return;
3302
+ const updated = { ...conv, pinnedAt: Date.now() };
3303
+ this._replace(updated);
3304
+ if (this._adapter.pin) {
3305
+ await this._adapter.pin(id);
3306
+ } else {
3307
+ await this._adapter.save(updated);
3308
+ }
3309
+ this._notify();
3310
+ }
3311
+ /** Unpin a conversation. Metadata only — see `pin()`. */
3312
+ async unpin(id) {
3313
+ const conv = this._find(id);
3314
+ if (!conv) return;
3315
+ const updated = { ...conv, pinnedAt: void 0 };
3316
+ this._replace(updated);
3317
+ if (this._adapter.unpin) {
3318
+ await this._adapter.unpin(id);
3319
+ } else {
3320
+ await this._adapter.save(updated);
3321
+ }
3322
+ this._notify();
3323
+ }
3736
3324
  /** Update the title manually. The full input is preserved — UI surfaces
3737
3325
  * (sidebar list, topbar) are responsible for visual truncation via CSS
3738
3326
  * (`min-w-0` + `truncate`). Auto-titles produced internally by
@@ -3807,6 +3395,32 @@ class AparteConversationController {
3807
3395
  _onMessageAborted = null;
3808
3396
  _onSelectConversation = null;
3809
3397
  _unsubscribeManager = null;
3398
+ /** The manager the subscription above is on, so a replaced manager is re-subscribed. */
3399
+ _subscribedTo = null;
3400
+ /** Between bind() and unbind(): the only time a manager subscription may be installed. */
3401
+ _bound = false;
3402
+ /**
3403
+ * React to an external mutation of the manager (another component deleting or
3404
+ * archiving the active conversation). Without this the binding would keep
3405
+ * showing a phantom history for an id the manager no longer knows about.
3406
+ */
3407
+ _onManagerChanged = (convs) => {
3408
+ if (!this._activeId) return;
3409
+ const stillExists = convs.some((c) => c.id === this._activeId && !c.archivedAt);
3410
+ if (!stillExists) {
3411
+ if (this._isStreaming) {
3412
+ try {
3413
+ window.dispatchEvent(new CustomEvent("aparte-abort", {
3414
+ detail: { targetId: this._binding.hostId }
3415
+ }));
3416
+ } catch {
3417
+ }
3418
+ this._isStreaming = false;
3419
+ }
3420
+ this._activeId = null;
3421
+ this._binding.clearMessages();
3422
+ }
3423
+ };
3810
3424
  constructor(binding, options = {}) {
3811
3425
  this._binding = binding;
3812
3426
  this._options = options;
@@ -3823,10 +3437,17 @@ class AparteConversationController {
3823
3437
  * conversation lifecycle yet.
3824
3438
  */
3825
3439
  _manager() {
3826
- if (this._options.manager) return this._options.manager;
3827
- const scoped = resolveConfig(this._binding.host);
3828
- const manager = scoped.getConversationManager();
3829
- if (!manager) this._warnManagerOnTheWrongConfig(scoped);
3440
+ const manager = this._options.manager ?? (() => {
3441
+ const scoped = resolveConfig(this._binding.host);
3442
+ const m = scoped.getConversationManager();
3443
+ if (!m) this._warnManagerOnTheWrongConfig(scoped);
3444
+ return m;
3445
+ })();
3446
+ if (manager && this._bound && manager !== this._subscribedTo) {
3447
+ this._unsubscribeManager?.();
3448
+ this._subscribedTo = manager;
3449
+ this._unsubscribeManager = manager.subscribe(this._onManagerChanged);
3450
+ }
3830
3451
  return manager;
3831
3452
  }
3832
3453
  /**
@@ -3905,32 +3526,16 @@ class AparteConversationController {
3905
3526
  if (typeof window !== "undefined") {
3906
3527
  window.addEventListener("aparte-select-conversation", this._onSelectConversation);
3907
3528
  }
3908
- const manager = this._manager();
3909
- if (manager) {
3910
- this._unsubscribeManager = manager.subscribe((convs) => {
3911
- if (!this._activeId) return;
3912
- const stillExists = convs.some((c) => c.id === this._activeId && !c.archivedAt);
3913
- if (!stillExists) {
3914
- if (this._isStreaming) {
3915
- try {
3916
- window.dispatchEvent(new CustomEvent("aparte-abort", {
3917
- detail: { targetId: this._binding.hostId }
3918
- }));
3919
- } catch {
3920
- }
3921
- this._isStreaming = false;
3922
- }
3923
- this._activeId = null;
3924
- this._binding.clearMessages();
3925
- }
3926
- });
3927
- }
3529
+ this._bound = true;
3530
+ this._manager();
3928
3531
  return () => this.unbind();
3929
3532
  }
3930
3533
  /** Detach event listeners. */
3931
3534
  /** Release handle for the hydration-retry subscription (see setConversationId). */
3932
3535
  _stopHydrationRetry = null;
3933
3536
  unbind() {
3537
+ this._bound = false;
3538
+ this._subscribedTo = null;
3934
3539
  this._persistActive();
3935
3540
  this._stopHydrationRetry?.();
3936
3541
  this._stopHydrationRetry = null;
@@ -4083,6 +3688,11 @@ class AparteConversationController {
4083
3688
  }
4084
3689
  const wasActiveId = this._activeId;
4085
3690
  let convId = wasActiveId ?? manager.activeId;
3691
+ if (convId && this._activeId === null && this._ensureInFlight) {
3692
+ await this._ensureInFlight.catch(() => {
3693
+ });
3694
+ convId = this._activeId ?? convId;
3695
+ }
4086
3696
  if (!convId) {
4087
3697
  if (!this._ensureInFlight) {
4088
3698
  const text = (userMsg.content ?? "").toString().trim() || "New Chat";
@@ -4106,7 +3716,7 @@ class AparteConversationController {
4106
3716
  }
4107
3717
  }
4108
3718
  function cssEscape(value) {
4109
- return value.replace(/["\\]/g, "\\$&");
3719
+ return value.replace(/["\\]/g, "\\$&").replace(/[\n\r\f]/g, (c) => `\\${c.charCodeAt(0).toString(16)} `);
4110
3720
  }
4111
3721
  const MAX_SIBLING_RETRIES = 6;
4112
3722
  class AparteChatHost {
@@ -4205,6 +3815,7 @@ class AparteChatHost {
4205
3815
  if (this._streamingId === id) return;
4206
3816
  this._streamingId = id;
4207
3817
  this.binding.onStreamingChange?.(id);
3818
+ this._vp()?.setTranscriptBusy?.(id !== null);
4208
3819
  }
4209
3820
  // ── conversation ───────────────────────────────────────────────────────
4210
3821
  /** Load / switch the active conversation via the controller. */
@@ -4231,10 +3842,10 @@ class AparteChatHost {
4231
3842
  },
4232
3843
  appendMessage: (msg) => this.appendMessage(msg),
4233
3844
  getMessages: () => this.binding.getMessages(),
4234
- clearMessages: () => {
3845
+ clearMessages: (options) => {
4235
3846
  this._beginConversationSwap();
4236
3847
  this.binding.setMessages([]);
4237
- this._vp()?.clearAll?.();
3848
+ this._vp()?.clearAll?.(options);
4238
3849
  },
4239
3850
  exportTree: () => {
4240
3851
  this.syncRepoFromMessages();
@@ -4293,6 +3904,7 @@ class AparteChatHost {
4293
3904
  return acc;
4294
3905
  }, [])
4295
3906
  } : message;
3907
+ this._vp()?.addMessage?.(stored);
4296
3908
  this.binding.setMessages([...this.binding.getMessages(), stored]);
4297
3909
  this.binding.onMessageAppended?.(stored);
4298
3910
  }
@@ -4480,11 +4092,17 @@ class AparteChatHost {
4480
4092
  getMessages() {
4481
4093
  return this.binding.getMessages();
4482
4094
  }
4483
- /** Clear all messages + reset viewport state. */
4484
- clearMessages() {
4095
+ /**
4096
+ * Clear all messages + reset viewport state. The option travels to the viewport
4097
+ * untouched: a compaction passes `{ revokeAttachments: false }` because it re-appends
4098
+ * the turns it keeps and revokes the dropped ones' object URLs itself — under a
4099
+ * framework wrapper this bridge used to drop the argument, and every surviving
4100
+ * attachment came back broken.
4101
+ */
4102
+ clearMessages(options) {
4485
4103
  this._beginConversationSwap();
4486
4104
  this.binding.setMessages([]);
4487
- this._vp()?.clearAll?.();
4105
+ this._vp()?.clearAll?.(options);
4488
4106
  }
4489
4107
  /** Create a new branch from a message (returns the new sibling index). */
4490
4108
  addBranch(messageId) {
@@ -4698,6 +4316,7 @@ class AparteChatHost {
4698
4316
  host["addSiblingOf"] = (id, m) => this.addSiblingOf(id, m);
4699
4317
  host["truncateFrom"] = (id) => this.truncateFrom(id);
4700
4318
  host["truncateResponsesAfter"] = (id) => this.truncateResponsesAfter(id);
4319
+ host["clearAll"] = (options) => this.clearMessages(options);
4701
4320
  }
4702
4321
  _installLifecycleListeners(host) {
4703
4322
  const onStart = (e) => {
@@ -4765,69 +4384,24 @@ function dispatchLifecycleEvent(target, name, detail) {
4765
4384
  const stamped = detail && typeof detail === "object" ? { targetId: id, ...detail } : detail;
4766
4385
  target.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail: stamped }));
4767
4386
  }
4768
- function dispatchArtifactLifecycle(target, messageId, segment, progress, isFinal) {
4769
- const id = segment.id;
4770
- const content = segment.content ?? "";
4771
- if (progress.get(id) === void 0) {
4772
- target.dispatchEvent(new CustomEvent("aparte-artifact-start", {
4773
- bubbles: true,
4774
- composed: true,
4775
- detail: {
4776
- messageId,
4777
- segmentId: id,
4778
- mimeType: segment.mimeType,
4779
- artifactType: segment.artifactType,
4780
- title: segment.title
4781
- }
4782
- }));
4783
- progress.set(id, 0);
4784
- }
4785
- const lastLen = progress.get(id) ?? 0;
4786
- if (content.length > lastLen) {
4787
- target.dispatchEvent(new CustomEvent("aparte-artifact-delta", {
4788
- bubbles: true,
4789
- composed: true,
4790
- detail: { segmentId: id, chunk: content.slice(lastLen) }
4791
- }));
4792
- progress.set(id, content.length);
4793
- }
4794
- if (isFinal) {
4795
- target.dispatchEvent(new CustomEvent("aparte-artifact-ready", {
4796
- bubbles: true,
4797
- composed: true,
4798
- detail: {
4799
- messageId,
4800
- segmentId: id,
4801
- mimeType: segment.mimeType,
4802
- artifactType: segment.artifactType,
4803
- title: segment.title,
4804
- content
4805
- }
4806
- }));
4807
- }
4808
- }
4809
4387
  function createStreamAdapter(opts) {
4810
- const { target, config, messageId, artifactHint } = opts;
4811
- let parser = new AparteStreamParser();
4388
+ const { target, config, messageId } = opts;
4389
+ let parser = new AparteStreamParser({ blocks: config.getStreamBlocks() });
4812
4390
  let streaming = /* @__PURE__ */ new Set();
4813
4391
  let thinkingId = null;
4814
4392
  let thinkingContent = "";
4815
4393
  let thinkingCollapsed = false;
4816
- let artifactProgress = /* @__PURE__ */ new Map();
4817
- let artifactPromoted = false;
4818
4394
  return (e) => {
4819
4395
  switch (e.type) {
4820
4396
  case "run-start":
4821
4397
  target.updateMessage?.(messageId, { status: "streaming" });
4822
4398
  break;
4823
4399
  case "turn-start":
4824
- parser = new AparteStreamParser();
4400
+ parser = new AparteStreamParser({ blocks: config.getStreamBlocks() });
4825
4401
  streaming = /* @__PURE__ */ new Set();
4826
4402
  thinkingId = null;
4827
4403
  thinkingContent = "";
4828
4404
  thinkingCollapsed = false;
4829
- artifactProgress = /* @__PURE__ */ new Map();
4830
- artifactPromoted = false;
4831
4405
  break;
4832
4406
  case "thinking-delta": {
4833
4407
  thinkingContent += e.delta;
@@ -4853,90 +4427,33 @@ function createStreamAdapter(opts) {
4853
4427
  thinkingCollapsed = true;
4854
4428
  }
4855
4429
  const result = parser.parse(e.delta);
4856
- for (let segment of result.segments) {
4857
- if (artifactHint && !artifactPromoted && segment.type === "code") {
4858
- const codeSeg = segment;
4859
- const promoted = {
4860
- id: codeSeg.id,
4861
- type: "artifact",
4862
- mimeType: artifactHint.mimeType,
4863
- artifactType: artifactHint.kind,
4864
- title: codeSeg.filename ?? artifactHint.kind,
4865
- content: codeSeg.content
4866
- };
4867
- segment = promoted;
4868
- artifactPromoted = true;
4869
- }
4430
+ for (const segment of result.segments) {
4870
4431
  if (!streaming.has(segment.id)) {
4871
4432
  target.addSegment?.(segment);
4872
4433
  streaming.add(segment.id);
4873
4434
  } else if ("content" in segment) {
4874
4435
  target.updateSegment?.(segment.id, segmentContentUpdate(segment));
4875
4436
  }
4876
- if (segment.type === "artifact") {
4877
- dispatchArtifactLifecycle(target, messageId, segment, artifactProgress, true);
4878
- }
4879
4437
  }
4880
4438
  const active = parser.getState().activeSegment;
4881
4439
  if (active) {
4882
4440
  if (!streaming.has(active.id)) {
4883
4441
  target.addSegment?.(active);
4884
4442
  streaming.add(active.id);
4885
- if (active.type === "artifact") {
4886
- dispatchArtifactLifecycle(target, messageId, active, artifactProgress, false);
4887
- }
4888
4443
  } else {
4889
4444
  target.updateSegment?.(active.id, segmentContentUpdate(active));
4890
- if (active.type === "artifact") {
4891
- dispatchArtifactLifecycle(target, messageId, active, artifactProgress, false);
4892
- }
4893
4445
  }
4894
4446
  } else if (result.segments.length === 0) ;
4895
4447
  break;
4896
4448
  }
4897
4449
  case "text-flush": {
4898
4450
  const finals = parser.finalize();
4899
- if (artifactHint && !artifactPromoted) {
4900
- const codeIdx = finals.findIndex((s) => s.type === "code");
4901
- if (codeIdx !== -1) {
4902
- const codeSeg = finals[codeIdx];
4903
- const promoted = {
4904
- id: codeSeg.id,
4905
- type: "artifact",
4906
- mimeType: artifactHint.mimeType,
4907
- artifactType: artifactHint.kind,
4908
- title: codeSeg.filename ?? artifactHint.kind,
4909
- content: codeSeg.content
4910
- };
4911
- finals[codeIdx] = promoted;
4912
- artifactPromoted = true;
4913
- if (streaming.has(promoted.id)) {
4914
- target.updateSegment?.(promoted.id, promoted);
4915
- }
4916
- }
4917
- }
4918
4451
  for (const s of finals) {
4919
4452
  if (!streaming.has(s.id)) target.addSegment?.(s);
4920
4453
  else if ("content" in s) target.updateSegment?.(s.id, segmentContentUpdate(s));
4921
- if (s.type === "artifact") {
4922
- dispatchArtifactLifecycle(target, messageId, s, artifactProgress, true);
4923
- }
4924
4454
  }
4925
4455
  break;
4926
4456
  }
4927
- case "artifact-ready": {
4928
- const seg = {
4929
- id: e.id,
4930
- type: "artifact",
4931
- mimeType: e.mimeType,
4932
- artifactType: e.kind,
4933
- title: e.title,
4934
- content: e.content
4935
- };
4936
- target.addSegment?.(seg);
4937
- dispatchArtifactLifecycle(target, messageId, seg, artifactProgress, true);
4938
- break;
4939
- }
4940
4457
  case "tool-start": {
4941
4458
  const toolSeg = {
4942
4459
  id: `tool-${e.toolCallId}`,
@@ -4965,11 +4482,18 @@ function createStreamAdapter(opts) {
4965
4482
  target.updateSegment?.(`tool-${e.toolCallId}`, { status: "rejected", result: e.reason });
4966
4483
  break;
4967
4484
  case "tool-resolved":
4968
- target.updateSegment?.(`tool-${e.toolCallId}`, { status: "resolved", result: e.result });
4485
+ target.updateSegment?.(`tool-${e.toolCallId}`, {
4486
+ status: "resolved",
4487
+ result: e.result,
4488
+ ...e.structuredResult !== void 0 ? { structuredResult: e.structuredResult } : {}
4489
+ });
4969
4490
  break;
4970
4491
  case "tool-aborted":
4971
4492
  target.updateSegment?.(`tool-${e.toolCallId}`, { status: "aborted" });
4972
4493
  break;
4494
+ case "tool-failed":
4495
+ target.updateSegment?.(`tool-${e.toolCallId}`, { status: "failed", result: e.error });
4496
+ break;
4973
4497
  case "turn-limit-exceeded":
4974
4498
  if (e.scope === "global") {
4975
4499
  target.addSegment?.({
@@ -5042,16 +4566,25 @@ function el$1(tag, className, text) {
5042
4566
  if (text != null) node.textContent = text;
5043
4567
  return node;
5044
4568
  }
5045
- function optionBody(label, description) {
4569
+ function optionBody(label, description, recommended) {
5046
4570
  const body = el$1("span", "aparte-elic-option-body");
5047
- body.appendChild(el$1("span", "aparte-elic-option-title", label));
4571
+ const title = el$1("span", "aparte-elic-option-title", label);
4572
+ if (recommended) {
4573
+ const badge = el$1(
4574
+ "span",
4575
+ "aparte-tag aparte-tag--sm aparte-elic-option-badge",
4576
+ contextConfig().t("elicitationRecommended") || "Recommended"
4577
+ );
4578
+ title.append(" ", badge);
4579
+ }
4580
+ body.appendChild(title);
5048
4581
  if (description) body.appendChild(el$1("span", "aparte-elic-option-desc", description));
5049
4582
  return body;
5050
4583
  }
5051
4584
  function commandOption(label, description, recommended) {
5052
4585
  const button = el$1("button", "aparte-btn aparte-btn--block aparte-btn--surface aparte-elic-option aparte-elic-option--command" + (recommended ? " aparte-elic-option--recommended" : ""));
5053
4586
  button.type = "button";
5054
- button.appendChild(optionBody(label, description));
4587
+ button.appendChild(optionBody(label, description, recommended));
5055
4588
  return button;
5056
4589
  }
5057
4590
  function fieldHeader(parent, field, skipTitle) {
@@ -5093,7 +4626,7 @@ function buildEnumField(field, onChange, fallbackLabel, settle) {
5093
4626
  control.name = name;
5094
4627
  control.value = value;
5095
4628
  if (defaults.has(value)) control.checked = true;
5096
- row.append(control, optionBody(label, description));
4629
+ row.append(control, optionBody(label, description, recommended));
5097
4630
  return row;
5098
4631
  };
5099
4632
  for (const opt of field.options) {
@@ -5416,27 +4949,27 @@ function buildPanel(message, schema, onChange) {
5416
4949
  chips.forEach((chip, i) => {
5417
4950
  chip.setAttribute("aria-selected", String(i === current));
5418
4951
  chip.tabIndex = i === current ? 0 : -1;
5419
- chip.toggleAttribute("data-answered", fields[i].field.isComplete());
4952
+ const answered = fields[i].field.isComplete();
4953
+ chip.toggleAttribute("data-answered", answered);
4954
+ const mark = chip.querySelector(".aparte-elic-step__mark");
4955
+ if (answered && !mark) {
4956
+ const glyph = el$1("span", "aparte-elic-step__mark");
4957
+ glyph.setAttribute("aria-hidden", "true");
4958
+ glyph.innerHTML = cfg.getIcon("check");
4959
+ chip.prepend(glyph);
4960
+ } else if (!answered && mark) {
4961
+ mark.remove();
4962
+ }
5420
4963
  });
5421
4964
  };
5422
4965
  show(0);
5423
- const last = () => current === fields.length - 1;
5424
4966
  return {
5425
4967
  ...api,
5426
- focus: () => fields[current]?.field.focus(),
5427
- mode: () => last() ? "submit" : "advance",
5428
- // Advancing needs only THIS question answered; submitting needs them all,
5429
- // which is what stops the last question from accepting a form with a hole
5430
- // in it.
5431
- canProceed: () => last() ? isComplete() : !!fields[current]?.field.isComplete(),
5432
- proceed: () => {
5433
- if (last()) return;
5434
- show(current + 1);
5435
- fields[current]?.field.focus();
5436
- }
4968
+ focus: () => fields[current]?.field.focus()
5437
4969
  };
5438
4970
  }
5439
- const field = buildField(schema, onChange, asked, void 0, settle);
4971
+ const answerOnClick = contextConfig().getElicitationOptions().answerOnClick;
4972
+ const field = buildField(schema, onChange, asked, void 0, answerOnClick ? settle : void 0);
5440
4973
  body.appendChild(field.el);
5441
4974
  return {
5442
4975
  el: panel,
@@ -5451,18 +4984,30 @@ function buildPanel(message, schema, onChange) {
5451
4984
  onSettle
5452
4985
  };
5453
4986
  }
4987
+ let labelSeq = 0;
5454
4988
  function el(tag, className, text) {
5455
4989
  const node = document.createElement(tag);
5456
4990
  if (className) node.className = className;
5457
4991
  if (text != null) node.textContent = text;
5458
4992
  return node;
5459
4993
  }
5460
- function buildApprovalPanel(message, options, onChange) {
4994
+ function buildApprovalPanel(message, options, onChange, details) {
5461
4995
  const cfg = contextConfig();
5462
4996
  const panel = el("div", "aparte-elic-panel aparte-approval-panel");
5463
4997
  const askText = () => typeof message === "function" ? message() : message;
5464
4998
  const ask = askText() ? el("p", "aparte-elic-message", askText()) : null;
5465
4999
  if (ask) panel.appendChild(ask);
5000
+ let argsLabel = null;
5001
+ if (details) {
5002
+ argsLabel = el("div", "aparte-approval-args-label", cfg.t("approvalArgsLabel"));
5003
+ argsLabel.id = "aparte-approval-args-" + ++labelSeq;
5004
+ const pre = el("pre", "aparte-approval-args");
5005
+ pre.textContent = details;
5006
+ pre.setAttribute("role", "group");
5007
+ pre.setAttribute("aria-labelledby", argsLabel.id);
5008
+ pre.tabIndex = 0;
5009
+ panel.append(argsLabel, pre);
5010
+ }
5466
5011
  const dismiss = el("div", "aparte-elic-dismiss");
5467
5012
  panel.appendChild(dismiss);
5468
5013
  let settle = null;
@@ -5477,11 +5022,13 @@ function buildApprovalPanel(message, options, onChange) {
5477
5022
  row.setAttribute("aria-label", cfg.t("approvalOptionsLabel"));
5478
5023
  const labelled = [];
5479
5024
  const textOf = (option) => typeof option.label === "function" ? option.label() : option.label;
5025
+ const descriptionOf = (option) => typeof option.description === "function" ? option.description() : option.description;
5026
+ const bodyOf = (option) => optionBody(textOf(option), descriptionOf(option));
5480
5027
  for (const option of options) {
5481
5028
  const tone = option.tone ?? "affirm";
5482
5029
  const button = el("button", `aparte-btn aparte-btn--block aparte-btn--surface aparte-approval-option aparte-approval-option--${tone}`);
5483
5030
  button.type = "button";
5484
- button.textContent = textOf(option);
5031
+ button.appendChild(bodyOf(option));
5485
5032
  button.addEventListener("click", () => fire({ option: option.value }));
5486
5033
  row.appendChild(button);
5487
5034
  labelled.push({ button, option });
@@ -5512,26 +5059,26 @@ function buildApprovalPanel(message, options, onChange) {
5512
5059
  instruction.setAttribute("aria-label", label);
5513
5060
  row.setAttribute("aria-label", now.t("approvalOptionsLabel"));
5514
5061
  if (ask) ask.textContent = askText();
5515
- for (const { button, option } of labelled) button.textContent = textOf(option);
5062
+ if (argsLabel) argsLabel.textContent = now.t("approvalArgsLabel");
5063
+ for (const { button, option } of labelled) button.replaceChildren(bodyOf(option));
5516
5064
  }
5517
5065
  };
5518
5066
  }
5519
5067
  function requestUserInput(request) {
5520
5068
  return resolveConfig(request.target ?? null).requestUserInput(request);
5521
5069
  }
5070
+ const COMPACTION_PREAMBLE = "The earlier part of this conversation was compacted. This is a summary of what came before; continue from it.";
5522
5071
  class AparteClient {
5523
5072
  _boundHandler = null;
5524
5073
  _boundAbortHandler = null;
5525
- _boundCompactHandler = null;
5526
5074
  _boundRetryHandler = null;
5527
5075
  _boundEditHandler = null;
5528
- _activeToolControllers = /* @__PURE__ */ new Set();
5529
5076
  _isAborted = false;
5530
5077
  /**
5531
5078
  * Does this window event belong to this client?
5532
5079
  *
5533
- * One rule for all five handlers, because it used to be four near-copies and
5534
- * one omission: `aparte-compact` had no guard at all, so in the two-client
5080
+ * One rule for all four handlers, because it used to be near-copies and
5081
+ * one omission: `aparte-compact` (answered here until 0.16.0) had no guard at all, so in the two-client
5535
5082
  * layout the JSDoc documents, a single compact event made BOTH clients run —
5536
5083
  * two paid summarisation calls against whichever chat the DOM scan found
5537
5084
  * first, and a global reset that wiped the other conversation.
@@ -5553,8 +5100,6 @@ class AparteClient {
5553
5100
  }
5554
5101
  /** Aborts the in-flight vendor/transport fetch when the user stops a stream. */
5555
5102
  _streamController = null;
5556
- /** Aborts an in-flight `compact()` summarisation — its own slot, see `compact`. */
5557
- _compactController = null;
5558
5103
  options;
5559
5104
  /** Config read by this client — an instance config, or the global default. */
5560
5105
  _config;
@@ -5579,18 +5124,15 @@ class AparteClient {
5579
5124
  * erased an abort that arrived while the user was still waiting for a large attachment
5580
5125
  * to be read.
5581
5126
  *
5582
- * Cancelling the previous turn's tool controllers used to happen only on `aparte-send`.
5583
- * Retry and edit reset the flag and left them running, so a tool handler from the turn
5584
- * being superseded stayed alive with its timeout still counting — while its reply was
5585
- * already off the active path. A cold audit found the asymmetry; three callers, one
5586
- * rule, no third copy to fall out of step.
5127
+ * The superseded turn's TOOL handlers are not cancelled here, and do not need a
5128
+ * registry of their own: `_streamTurn` aborts the previous `_streamController`, and
5129
+ * the engine links every tool handler's controller to that run signal
5130
+ * (`invokeToolHandler`), so cutting the stream cuts its tools. A `Set<AbortController>`
5131
+ * used to sit here for that job and was never added to three lines of loop over an
5132
+ * empty set, plus a paragraph describing a cancellation the method did not perform.
5587
5133
  */
5588
5134
  _beginUserTurn() {
5589
5135
  this._isAborted = false;
5590
- for (const controller of this._activeToolControllers) {
5591
- controller.abort();
5592
- }
5593
- this._activeToolControllers.clear();
5594
5136
  }
5595
5137
  /**
5596
5138
  * Sets up the event listeners.
@@ -5623,14 +5165,6 @@ class AparteClient {
5623
5165
  };
5624
5166
  }
5625
5167
  window.addEventListener("aparte-abort", this._boundAbortHandler);
5626
- if (!this._boundCompactHandler) {
5627
- this._boundCompactHandler = (e) => {
5628
- if (!this._isForThisInstance(e)) return;
5629
- const detail = e.detail;
5630
- void this.compact(detail?.targetId);
5631
- };
5632
- }
5633
- window.addEventListener("aparte-compact", this._boundCompactHandler);
5634
5168
  if (!this._boundRetryHandler) {
5635
5169
  this._boundRetryHandler = (e) => {
5636
5170
  const evt = e;
@@ -5660,10 +5194,6 @@ class AparteClient {
5660
5194
  window.removeEventListener("aparte-abort", this._boundAbortHandler);
5661
5195
  this._boundAbortHandler = null;
5662
5196
  }
5663
- if (this._boundCompactHandler) {
5664
- window.removeEventListener("aparte-compact", this._boundCompactHandler);
5665
- this._boundCompactHandler = null;
5666
- }
5667
5197
  if (this._boundRetryHandler) {
5668
5198
  window.removeEventListener("aparte-retry", this._boundRetryHandler);
5669
5199
  this._boundRetryHandler = null;
@@ -5680,11 +5210,6 @@ class AparteClient {
5680
5210
  abort() {
5681
5211
  this._isAborted = true;
5682
5212
  this._streamController?.abort();
5683
- this._compactController?.abort();
5684
- for (const controller of this._activeToolControllers) {
5685
- controller.abort();
5686
- }
5687
- this._activeToolControllers.clear();
5688
5213
  }
5689
5214
  /*
5690
5215
  * Two docblocks stood here, both describing `_awaitToolDecision` — the method
@@ -5707,9 +5232,14 @@ class AparteClient {
5707
5232
  * one queue, one panel slot, one teardown, and the same behaviour whether the
5708
5233
  * request came from a tool handler or from this gate.
5709
5234
  *
5710
- * The tool NAME is the question; the arguments stay in the transcript, on the pill
5711
- * this request is anchored to. A panel capped at 50vh cannot hold a diff or a plan,
5712
- * and the transcript is already scrollable, copyable and persisted.
5235
+ * The tool NAME is the question and its ARGUMENTS are the body, because the
5236
+ * arguments are what is being approved a decision surface that names the act and
5237
+ * hides its subject asks for a signature on a blank page. This paragraph used to
5238
+ * say the opposite ("the arguments stay in the transcript"), and the guide beside
5239
+ * it had always promised what the panel did not do. The transcript row stays the
5240
+ * anchor; it is no longer the only place the call can be read. The panel is capped
5241
+ * at 50vh and the block inside it scrolls, so a long input cannot push the options
5242
+ * off the surface they are asked on.
5713
5243
  *
5714
5244
  * `aparte-tool-decision` is GONE with the buttons that dispatched it, and so is the
5715
5245
  * `document` listener that answered it. It existed only because a segment renderer
@@ -5732,6 +5262,11 @@ class AparteClient {
5732
5262
  const loc = this._config.getLocale();
5733
5263
  return (loc.approvalAsk ?? "Run {tool}?").replace("{tool}", event.name);
5734
5264
  },
5265
+ // A plain string, not a function: the arguments are wire format — the JSON
5266
+ // the model sent — and re-reading them on a language switch would produce
5267
+ // exactly the same bytes. Same text the transcript row shows, from the same
5268
+ // function, so the two surfaces cannot disagree about what is being run.
5269
+ details: describeToolInput(event.input),
5735
5270
  // Functions, not strings, same as the question: these follow a language
5736
5271
  // switch while the request is open, which a resolved string cannot.
5737
5272
  options: [
@@ -5792,15 +5327,16 @@ class AparteClient {
5792
5327
  return;
5793
5328
  }
5794
5329
  dispatchLifecycleEvent(targetElement, "aparte-message-start", { messageId, role: "assistant" });
5330
+ const streamController = new AbortController();
5795
5331
  try {
5796
- const usage = await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig);
5797
- if (this._isAborted) {
5332
+ const usage = await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController);
5333
+ if (this._isAborted || streamController.signal.aborted) {
5798
5334
  this._updateMessage(targetElement, messageId, { status: "completed" });
5799
5335
  } else {
5800
5336
  dispatchLifecycleEvent(targetElement, "aparte-message-done", { messageId, role: "assistant", usage });
5801
5337
  }
5802
5338
  } catch (error) {
5803
- if (this._isAborted) {
5339
+ if (this._isAborted || streamController.signal.aborted) {
5804
5340
  dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
5805
5341
  this._updateMessage(targetElement, messageId, { status: "completed" });
5806
5342
  return;
@@ -5809,116 +5345,6 @@ class AparteClient {
5809
5345
  this._handleLifecycleError(targetElement, messageId, aparteError);
5810
5346
  }
5811
5347
  }
5812
- /**
5813
- * Compact the current conversation: summarize all messages via the AI,
5814
- * clear the viewport, then inject the summary as a single context message.
5815
- *
5816
- * Triggered programmatically or by dispatching `window.dispatchEvent(new CustomEvent('aparte-compact'))`.
5817
- * Dispatches `aparte-compact-done` on window when complete, or `aparte-compact-error` on failure.
5818
- */
5819
- async compact(targetId) {
5820
- let target = this._resolveTarget(targetId);
5821
- if (target && typeof target.getMessages !== "function") target = null;
5822
- if (!target) {
5823
- window.dispatchEvent(new CustomEvent("aparte-compact-error", { detail: { error: "No aparte-chat target found" } }));
5824
- return;
5825
- }
5826
- const messages = target.getMessages?.() ?? [];
5827
- if (messages.length === 0) {
5828
- window.dispatchEvent(new CustomEvent("aparte-compact-done", { detail: { skipped: true } }));
5829
- return;
5830
- }
5831
- const selector = this.options.compactionSelector ?? ((m) => ({ keep: [], drop: m }));
5832
- const { keep, drop } = selector(messages);
5833
- if (drop.length === 0) {
5834
- window.dispatchEvent(new CustomEvent("aparte-compact-done", { detail: { skipped: true } }));
5835
- return;
5836
- }
5837
- const config = this._config.getModelConfig();
5838
- const providerId = config.defaultProvider;
5839
- if (!providerId) {
5840
- window.dispatchEvent(new CustomEvent("aparte-compact-error", { detail: { error: "No provider configured" } }));
5841
- return;
5842
- }
5843
- const provider = this._config.getAIProvider(providerId);
5844
- if (!provider) {
5845
- window.dispatchEvent(new CustomEvent("aparte-compact-error", { detail: { error: `Provider '${providerId}' not found` } }));
5846
- return;
5847
- }
5848
- window.dispatchEvent(new CustomEvent("aparte-compact-start"));
5849
- try {
5850
- const authConfig = await this._resolveAuth(providerId);
5851
- const historyMessages = drop.filter((m) => m.role === "user" || m.role === "assistant" && m.status === "completed").map((m) => ({
5852
- role: m.role,
5853
- content: this._extractText(m)
5854
- })).filter((m) => contentToText(m.content).length > 0);
5855
- const summarizeRequest = {
5856
- messages: [
5857
- {
5858
- role: "system",
5859
- content: "You are a conversation summarizer. Create a concise but complete summary of the conversation below. Capture: key topics, conclusions, decisions, ongoing tasks, and any context needed to continue the conversation. Write in third person. Be factual and brief. No preamble, just the summary."
5860
- },
5861
- ...historyMessages,
5862
- {
5863
- role: "user",
5864
- content: "Please summarize this conversation."
5865
- }
5866
- ],
5867
- modelId: config.defaultModel || "",
5868
- stream: false
5869
- };
5870
- const compactController = new AbortController();
5871
- this._compactController = compactController;
5872
- const response = await this._config.getTransport().chat(
5873
- provider,
5874
- summarizeRequest,
5875
- authConfig,
5876
- { providerId: provider.id, signal: compactController.signal }
5877
- );
5878
- let summary;
5879
- if (typeof response === "string") {
5880
- summary = response;
5881
- } else {
5882
- const reader = response.getReader();
5883
- const chunks = [];
5884
- while (true) {
5885
- const { done, value } = await reader.read();
5886
- if (done) break;
5887
- if (value.type === "text") chunks.push(value.delta);
5888
- }
5889
- reader.releaseLock();
5890
- summary = chunks.join("");
5891
- }
5892
- if (!summary.trim()) {
5893
- throw new Error("Empty summary returned by model");
5894
- }
5895
- const clearAll = target.clearAll;
5896
- if (typeof clearAll === "function") clearAll.call(target);
5897
- else window.dispatchEvent(new CustomEvent("aparte-reset"));
5898
- await new Promise((resolve) => setTimeout(resolve, 50));
5899
- target.appendMessage?.({
5900
- id: uuid(),
5901
- role: "assistant",
5902
- // Through the locale, like every string the user reads — this was a
5903
- // hardcoded English title (with an emoji) in an otherwise localised
5904
- // transcript. The engine compactor's `summaryLabel` is a separate,
5905
- // per-call knob; this is the UI's own title.
5906
- content: `**${this._config.t("compactionSummaryTitle")}**
5907
-
5908
- ${summary}`,
5909
- timestamp: Date.now(),
5910
- status: "completed"
5911
- });
5912
- for (const kept of keep) {
5913
- target.appendMessage?.(kept);
5914
- }
5915
- window.dispatchEvent(new CustomEvent("aparte-compact-done", { detail: { summary, kept: keep.length } }));
5916
- } catch (err) {
5917
- console.error("[AparteClient] compact() failed:", err);
5918
- const message = err instanceof Error ? err.message : String(err);
5919
- window.dispatchEvent(new CustomEvent("aparte-compact-error", { detail: { error: message } }));
5920
- }
5921
- }
5922
5348
  /**
5923
5349
  * Handle aparte-retry — add a sibling branch to the assistant message and re-stream
5924
5350
  * using the same conversation history minus the retried reply.
@@ -6104,9 +5530,11 @@ ${summary}`,
6104
5530
  _messagesToChatMessages(messages) {
6105
5531
  return messages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({
6106
5532
  role: m.role,
6107
- content: this._extractText(m)
5533
+ content: this._wireText(m)
6108
5534
  }));
6109
5535
  }
5536
+ /** The "no model selected" warning is said once per client, not once per dropped send. */
5537
+ _warnedNoModel = false;
6110
5538
  async _handleSend(event) {
6111
5539
  const { content, modelId, providerId: explicitProviderId } = event.detail;
6112
5540
  if (this._config.getRequireModelSelection() && !this._config.hasSelectedModel() && !modelId) {
@@ -6161,6 +5589,12 @@ ${summary}`,
6161
5589
  const config = this._config.getModelConfig();
6162
5590
  const providerId = explicitProviderId || config.defaultProvider;
6163
5591
  if (!providerId) {
5592
+ if (!this._warnedNoModel) {
5593
+ this._warnedNoModel = true;
5594
+ console.warn(
5595
+ "[AparteClient] Send dropped: no model is selected. Call aparteGlobalConfig.setModelConfig({ defaultProvider, defaultModel }), mount <aparte-model-selector auto-select>, or register one provider with a single model — that one is selected on its own."
5596
+ );
5597
+ }
6164
5598
  this._handleLifecycleError(targetElement, messageId, new AparteError(
6165
5599
  "No provider selected. Please configure a provider.",
6166
5600
  AparteErrorCode.CONFIG_NO_PROVIDER
@@ -6234,9 +5668,21 @@ ${summary}`,
6234
5668
  return false;
6235
5669
  }).map((m) => ({
6236
5670
  role: m.role,
6237
- content: this._extractText(m)
5671
+ content: this._wireText(m)
6238
5672
  })).filter((m) => m.content.length > 0);
6239
5673
  }
5674
+ /**
5675
+ * A message's text as the MODEL should read it — the one mapper both history
5676
+ * paths use, so the compaction preamble cannot be applied on retry and edit and
5677
+ * forgotten on the ordinary send, which is what the first review of this feature
5678
+ * caught: the summary went out on every normal turn as something the user said.
5679
+ */
5680
+ _wireText(message) {
5681
+ const text = this._extractText(message);
5682
+ return message.compaction ? `${COMPACTION_PREAMBLE}
5683
+
5684
+ ${text}` : text;
5685
+ }
6240
5686
  /**
6241
5687
  * What the model is told it said last turn — reconstructed from what was
6242
5688
  * RENDERED, not from the raw-text field.
@@ -6270,33 +5716,40 @@ ${summary}`,
6270
5716
  *
6271
5717
  * Fences and the language tag are kept. Without them the model re-reads its own
6272
5718
  * code as prose — which is how it starts explaining a snippet it believes it
6273
- * wrote in English. Artifacts are included for the same reason: dropping them
6274
- * made an artifact the model had just produced invisible on the very next turn,
6275
- * so it could not be asked to change the thing it had built.
5719
+ * wrote in English.
5720
+ *
5721
+ * A type core does not know a registered block grammar's (an artifact, a
5722
+ * citation, a file), a consumer's custom segment — follows one rule: its
5723
+ * `content` if it has one, else its `fallback`, else nothing. Dropping such a
5724
+ * segment made a document the model had just produced invisible on the very next
5725
+ * turn, so it could not be asked to change the thing it had built; there used to
5726
+ * be an `artifact` branch here for exactly that, and the rule is what replaces it
5727
+ * now that the artifact is a plugin's type like any other.
6276
5728
  *
6277
- * `thinking` stays out on purpose — it is the model's own scratchpad, and most
6278
- * APIs neither want it back nor bill for it kindly.
5729
+ * `thinking` and `tool_call` stay out on purpose — the first is the model's own
5730
+ * scratchpad, and most APIs neither want it back nor bill for it kindly; the
5731
+ * second is already in the history as a call and a result.
6279
5732
  */
6280
5733
  _segmentsToText(segments) {
6281
5734
  if (!segments?.length) return "";
6282
5735
  const fence = "```";
6283
5736
  const parts = [];
6284
5737
  for (const segment of segments) {
6285
- const content = segment.content ?? "";
5738
+ const content = segment.content;
6286
5739
  if (segment.type === "text") {
6287
5740
  if (content) parts.push(content);
6288
5741
  } else if (segment.type === "code") {
6289
5742
  const lang = segment.language ?? "";
6290
5743
  parts.push(`${fence}${lang}
6291
- ${content}
6292
- ${fence}`);
6293
- } else if (segment.type === "artifact") {
6294
- const title = segment.title ?? "artifact";
6295
- const kind = segment.artifactType ?? "";
6296
- parts.push(`${fence}${kind}
6297
- <!-- artifact: ${title} -->
6298
- ${content}
5744
+ ${content ?? ""}
6299
5745
  ${fence}`);
5746
+ } else if (segment.type === "thinking" || segment.type === "tool_call" || segment.type === "error") {
5747
+ continue;
5748
+ } else if (typeof content === "string" && content) {
5749
+ parts.push(content);
5750
+ } else {
5751
+ const fallback = segment.fallback;
5752
+ if (fallback) parts.push(fallback);
6300
5753
  }
6301
5754
  }
6302
5755
  return parts.join("\n").trim();
@@ -6372,9 +5825,8 @@ ${content}
6372
5825
  * The turn's agent loop — the engine's `runStreamAgent` rendered through the core
6373
5826
  * adapter, or the runner a host injected — after the fetch-level abort bookkeeping.
6374
5827
  */
6375
- async _streamLoop(targetElement, messageId, provider, baseRequest, authConfig) {
5828
+ async _streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController = new AbortController()) {
6376
5829
  this._streamController?.abort();
6377
- const streamController = new AbortController();
6378
5830
  this._streamController = streamController;
6379
5831
  const runner = this.options.streamRunner ?? runStreamAgent;
6380
5832
  return this._runViaStreamRunner(runner, targetElement, messageId, provider, baseRequest, authConfig, streamController);
@@ -6390,12 +5842,10 @@ ${content}
6390
5842
  */
6391
5843
  async _runViaStreamRunner(streamRunner, targetElement, messageId, provider, baseRequest, authConfig, streamController) {
6392
5844
  const signal = streamController.signal;
6393
- const artifactHint = baseRequest._meta?.artifactHint;
6394
5845
  const adapter = createStreamAdapter({
6395
5846
  target: targetElement,
6396
5847
  config: this._config,
6397
- messageId,
6398
- artifactHint
5848
+ messageId
6399
5849
  });
6400
5850
  const prefixSegments = baseRequest._meta?.["prefixSegments"];
6401
5851
  const emitter = (event) => {
@@ -6414,11 +5864,26 @@ ${content}
6414
5864
  const context = { target: targetElement, config: this._config };
6415
5865
  return (call, sig) => handler(call, sig, context);
6416
5866
  };
5867
+ const hostOwnsDecision = Boolean(this.options.approvalResolver);
6417
5868
  const toolConfigLookup = (name) => {
6418
5869
  const tool = this._config.getTools().find((t) => t.name === name);
6419
- return tool ? { maxTurns: tool.maxTurns, needsApproval: tool.needsApproval } : void 0;
5870
+ if (!tool) return void 0;
5871
+ return {
5872
+ maxTurns: tool.maxTurns,
5873
+ needsApproval: hostOwnsDecision ? tool.needsApproval : (call) => {
5874
+ const verdict = this._config.ruleOnToolCall(call).verdict;
5875
+ return verdict === "allow" ? false : verdict;
5876
+ }
5877
+ };
6420
5878
  };
6421
- const approvalResolver = this.options.approvalResolver ?? ((call, sig) => this._askForApproval(call, sig, targetElement));
5879
+ const approvalResolver = this.options.approvalResolver ?? (async (call, sig) => {
5880
+ const ruling = this._config.ruleOnToolCall(call);
5881
+ if (ruling.verdict === "allow") return { approved: true };
5882
+ if (ruling.verdict === "deny") {
5883
+ return { approved: false, reason: ruling.reason?.trim() || "Tool execution was refused by the approval policy." };
5884
+ }
5885
+ return this._askForApproval(call, sig, targetElement);
5886
+ });
6422
5887
  const usage = await streamRunner({
6423
5888
  messageId,
6424
5889
  baseRequest,
@@ -6652,6 +6117,12 @@ class AparteMessageRepository {
6652
6117
  );
6653
6118
  continue;
6654
6119
  }
6120
+ if (this._nodes.has(message.id)) {
6121
+ console.warn(
6122
+ `[AparteMessageRepository] import: message "${message.id}" repeats an id already in the snapshot — skipped.`
6123
+ );
6124
+ continue;
6125
+ }
6655
6126
  this.addOrUpdateMessage(parentId, message);
6656
6127
  }
6657
6128
  if (exported.headId) {
@@ -6744,6 +6215,7 @@ const APARTE_DEFAULT_UI_EVENTS = [
6744
6215
  "aparte-feedback",
6745
6216
  "aparte-message-info",
6746
6217
  "aparte-branch-navigate",
6218
+ "aparte-link-click",
6747
6219
  // <aparte-chat-viewport>
6748
6220
  "aparte-segment-update",
6749
6221
  "aparte-reset-done",
@@ -6753,11 +6225,26 @@ const APARTE_DEFAULT_UI_EVENTS = [
6753
6225
  "aparte-delete-conversation",
6754
6226
  "aparte-archive-conversation",
6755
6227
  "aparte-unarchive-conversation",
6228
+ "aparte-rename-conversation",
6229
+ "aparte-pin-conversation",
6230
+ "aparte-unpin-conversation",
6756
6231
  // the select primitives
6757
6232
  "aparte-select-change",
6758
6233
  "aparte-select-open",
6759
6234
  "aparte-select-close",
6760
- "aparte-optgroup-toggle"
6235
+ "aparte-optgroup-toggle",
6236
+ // the shell and the transcript's own elements — the five the sweep found missing
6237
+ "aparte-suggestion",
6238
+ "aparte-context-threshold",
6239
+ "aparte-scroll-rail-jump",
6240
+ "aparte-sidebar-toggle",
6241
+ "aparte-split-resize",
6242
+ // the turn's lifecycle, dispatched on the HOST element by `dispatchLifecycleEvent`
6243
+ "aparte-message-start",
6244
+ "aparte-message-done",
6245
+ "aparte-message-error",
6246
+ "aparte-message-aborted",
6247
+ "aparte-tool-approval-request"
6761
6248
  ];
6762
6249
  function applyElementProps(el2, props, transformValue = (value) => value) {
6763
6250
  for (const [key, value] of Object.entries(props)) {
@@ -6784,50 +6271,99 @@ function isAwaitingReply(message) {
6784
6271
  if (message.status === "streaming" || message.status === "pending") return true;
6785
6272
  return message.status === void 0 && message.role === "assistant" && !message.content?.trim() && !message.segments?.length;
6786
6273
  }
6274
+ const INSTALLED = Symbol.for("aparte.dialogTriggers");
6275
+ let pressTarget = null;
6276
+ let releaseTarget = null;
6277
+ function onPointerDown(event) {
6278
+ pressTarget = event.target;
6279
+ releaseTarget = null;
6280
+ }
6281
+ function onPointerUp(event) {
6282
+ releaseTarget = event.target;
6283
+ }
6284
+ function onClick(event) {
6285
+ const target = event.target;
6286
+ if (!target?.closest) return;
6287
+ const opener = target.closest("[data-aparte-dialog-open]");
6288
+ if (opener) {
6289
+ const id = opener.getAttribute("data-aparte-dialog-open");
6290
+ const dialog = id ? document.getElementById(id) : null;
6291
+ if (dialog instanceof HTMLDialogElement && !dialog.open) {
6292
+ event.preventDefault();
6293
+ dialog.showModal();
6294
+ }
6295
+ return;
6296
+ }
6297
+ const closer = target.closest("[data-aparte-dialog-close]");
6298
+ if (closer) {
6299
+ const dialog = closer.closest("dialog");
6300
+ if (dialog instanceof HTMLDialogElement && dialog.open) {
6301
+ event.preventDefault();
6302
+ dialog.close(closer.getAttribute("data-aparte-dialog-close") ?? "");
6303
+ }
6304
+ return;
6305
+ }
6306
+ if (target instanceof HTMLDialogElement && target.open && target.classList.contains("aparte-dialog") && !target.hasAttribute("data-aparte-dialog-static")) {
6307
+ const deliberate = pressTarget === target && releaseTarget === target;
6308
+ pressTarget = null;
6309
+ releaseTarget = null;
6310
+ if (deliberate) target.close();
6311
+ }
6312
+ }
6313
+ function installDialogTriggersOnce(doc = document) {
6314
+ const marked = doc;
6315
+ if (marked[INSTALLED]) return;
6316
+ marked[INSTALLED] = true;
6317
+ doc.addEventListener("pointerdown", onPointerDown, true);
6318
+ doc.addEventListener("pointerup", onPointerUp, true);
6319
+ doc.addEventListener("click", onClick);
6320
+ }
6787
6321
  export {
6788
- runWithConfig as $,
6322
+ resolveConfig as $,
6789
6323
  APARTE_CONFIG_CHANGE as A,
6790
6324
  contextConfig as B,
6791
- createAparteChatHandler as C,
6792
- createStreamAdapter as D,
6793
- cssEscape as E,
6794
- declineDefaultRenderers as F,
6795
- defaultSanitizer as G,
6796
- detachConfig as H,
6797
- escapeAttr as I,
6798
- escapeHtml as J,
6799
- filesToAttachments as K,
6800
- getAllRenderers as L,
6801
- getSegmentRenderer as M,
6802
- installDefaultRenderersOnce as N,
6803
- isAwaitingReply as O,
6804
- isFormatAdapter as P,
6805
- isSafeUrl as Q,
6806
- isSegmentSettled as R,
6807
- parseAparteEventStream as S,
6808
- parseMarkdownToSegments as T,
6809
- populateBubbleFromMessage as U,
6810
- readableToAsyncIterable as V,
6811
- registerDefaultRenderers as W,
6812
- registerSegmentRenderer as X,
6813
- requestUserInput as Y,
6814
- resolveConfig as Z,
6815
- revokeAttachmentUrls as _,
6325
+ copyText as C,
6326
+ createAparteChatHandler as D,
6327
+ createStreamAdapter as E,
6328
+ cssEscape as F,
6329
+ declineDefaultRenderers as G,
6330
+ defaultSanitizer as H,
6331
+ detachConfig as I,
6332
+ escapeAttr as J,
6333
+ escapeHtml as K,
6334
+ filesToAttachments as L,
6335
+ getAllRenderers as M,
6336
+ getSegmentRenderer as N,
6337
+ installDefaultRenderersOnce as O,
6338
+ installDialogTriggersOnce as P,
6339
+ isAwaitingReply as Q,
6340
+ isFormatAdapter as R,
6341
+ isSafeUrl as S,
6342
+ isSegmentSettled as T,
6343
+ parseAparteEventStream as U,
6344
+ parseMarkdownToSegments as V,
6345
+ populateBubbleFromMessage as W,
6346
+ readableToAsyncIterable as X,
6347
+ registerDefaultRenderers as Y,
6348
+ registerSegmentRenderer as Z,
6349
+ requestUserInput as _,
6816
6350
  APARTE_CONVERSATION_SCHEMA_VERSION as a,
6817
- segmentDuration as a0,
6818
- segmentTiming as a1,
6819
- subscribeConfigChange as a2,
6820
- unregisterSegmentRenderer as a3,
6821
- uuid as a4,
6822
- mergeSegmentUpdate as a5,
6823
- writeStreamedMarkdown as a6,
6824
- stampSegmentActivity as a7,
6825
- stampSegmentOnInsert as a8,
6826
- stampSegmentOnUpdate as a9,
6827
- renumberSegments as aa,
6828
- openSegmentIds as ab,
6829
- isTerminalStatus as ac,
6830
- adoptMessageSegments as ad,
6351
+ revokeAttachmentUrls as a0,
6352
+ runWithConfig as a1,
6353
+ segmentDuration as a2,
6354
+ segmentTiming as a3,
6355
+ subscribeConfigChange as a4,
6356
+ unregisterSegmentRenderer as a5,
6357
+ uuid as a6,
6358
+ mergeSegmentUpdate as a7,
6359
+ writeStreamedMarkdown as a8,
6360
+ stampSegmentActivity as a9,
6361
+ stampSegmentOnInsert as aa,
6362
+ stampSegmentOnUpdate as ab,
6363
+ renumberSegments as ac,
6364
+ openSegmentIds as ad,
6365
+ isTerminalStatus as ae,
6366
+ adoptMessageSegments as af,
6831
6367
  APARTE_DEFAULT_BUBBLE_ACTIONS as b,
6832
6368
  APARTE_DEFAULT_HOST_HANDLERS as c,
6833
6369
  APARTE_DEFAULT_ICON_FALLBACKS as d,
@@ -6854,4 +6390,4 @@ export {
6854
6390
  collectRendererStyles as y,
6855
6391
  contentToText as z
6856
6392
  };
6857
- //# sourceMappingURL=is-awaiting-reply-BZ_iJ3s3.js.map
6393
+ //# sourceMappingURL=dialog-triggers-CVCxqKg3.js.map