@aparte/core 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/aparte-client.d.ts +53 -51
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/client/lifecycle-events.d.ts +0 -12
- package/dist/client/lifecycle-events.d.ts.map +1 -1
- package/dist/client/stream-adapter.d.ts +2 -12
- package/dist/client/stream-adapter.d.ts.map +1 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts +45 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-attachments.d.ts +0 -1
- package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-send.d.ts +11 -11
- package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer.d.ts +20 -11
- package/dist/components/composer/aparte-composer.d.ts.map +1 -1
- package/dist/components/context/aparte-context.d.ts +31 -8
- package/dist/components/context/aparte-context.d.ts.map +1 -1
- package/dist/components/conversation-list/aparte-conversation-list.d.ts +93 -36
- package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
- package/dist/components/conversation-list/index.d.ts +1 -1
- package/dist/components/conversation-list/index.d.ts.map +1 -1
- package/dist/components/elicitation/aparte-elicitation.d.ts +19 -1
- package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
- package/dist/components/index.d.ts +7 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/scroll-rail/aparte-scroll-rail.d.ts +105 -0
- package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -0
- package/dist/components/scroll-rail/index.d.ts +3 -0
- package/dist/components/scroll-rail/index.d.ts.map +1 -0
- package/dist/components/sidebar/aparte-sidebar.d.ts +186 -0
- package/dist/components/sidebar/aparte-sidebar.d.ts.map +1 -0
- package/dist/components/sidebar/index.d.ts +3 -0
- package/dist/components/sidebar/index.d.ts.map +1 -0
- package/dist/components/split/aparte-split.d.ts +354 -0
- package/dist/components/split/aparte-split.d.ts.map +1 -0
- package/dist/components/split/geometry.d.ts +65 -0
- package/dist/components/split/geometry.d.ts.map +1 -0
- package/dist/components/split/index.d.ts +3 -0
- package/dist/components/split/index.d.ts.map +1 -0
- package/dist/components/suggestions/aparte-suggestions.d.ts +7 -1
- package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +52 -2
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +95 -33
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/icon-provider.d.ts +6 -0
- package/dist/config/icon-provider.d.ts.map +1 -1
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/locale.d.ts +140 -27
- package/dist/config/locale.d.ts.map +1 -1
- package/dist/config/sanitize.d.ts +9 -0
- package/dist/config/sanitize.d.ts.map +1 -1
- package/dist/conversations/conversation-controller.d.ts +13 -1
- package/dist/conversations/conversation-controller.d.ts.map +1 -1
- package/dist/conversations/conversation-manager.d.ts +9 -0
- package/dist/conversations/conversation-manager.d.ts.map +1 -1
- package/dist/custom-elements.json +9838 -7333
- package/dist/{is-awaiting-reply-BZ_iJ3s3.js → dialog-triggers-CVCxqKg3.js} +718 -1182
- package/dist/dialog-triggers-CVCxqKg3.js.map +1 -0
- package/dist/elicitation/approval-panel.d.ts +1 -1
- package/dist/elicitation/approval-panel.d.ts.map +1 -1
- package/dist/elicitation/panel.d.ts +24 -16
- package/dist/elicitation/panel.d.ts.map +1 -1
- package/dist/elicitation/types.d.ts +31 -0
- package/dist/elicitation/types.d.ts.map +1 -1
- package/dist/generated/element-attributes.d.ts +47 -1
- package/dist/generated/element-attributes.d.ts.map +1 -1
- package/dist/{glyphs-B3BZ2TIb.js → glyphs-DQTeOj4w.js} +22 -13
- package/dist/{glyphs-B3BZ2TIb.js.map → glyphs-DQTeOj4w.js.map} +1 -1
- package/dist/host/aparte-chat-host.d.ts +10 -2
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/icons/extended.d.ts +4 -2
- package/dist/icons/extended.d.ts.map +1 -1
- package/dist/icons/glyphs.d.ts +9 -0
- package/dist/icons/glyphs.d.ts.map +1 -1
- package/dist/icons.js +16 -15
- package/dist/icons.js.map +1 -1
- package/dist/index.css +1217 -396
- package/dist/index.d.ts +26 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2097 -295
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +21 -5
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +39 -34
- package/dist/index.node.js.map +1 -1
- package/dist/interop/dialog-triggers.d.ts +27 -0
- package/dist/interop/dialog-triggers.d.ts.map +1 -0
- package/dist/interop/element-props.d.ts +16 -8
- package/dist/interop/element-props.d.ts.map +1 -1
- package/dist/parsers/aparte-stream-parser.d.ts +52 -22
- package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
- package/dist/parsers/index.d.ts +1 -1
- package/dist/parsers/index.d.ts.map +1 -1
- package/dist/primitives/select/aparte-select.d.ts.map +1 -1
- package/dist/renderers/segment-renderers.d.ts.map +1 -1
- package/dist/renderers/segments/code.d.ts.map +1 -1
- package/dist/renderers/segments/tool-call.d.ts.map +1 -1
- package/dist/runtime/message-repository.d.ts.map +1 -1
- package/dist/transport/backend-handler.d.ts.map +1 -1
- package/dist/transport/backend-transport.d.ts +6 -1
- package/dist/transport/backend-transport.d.ts.map +1 -1
- package/dist/types/chat.d.ts +8 -14
- package/dist/types/chat.d.ts.map +1 -1
- package/dist/types/element-map.d.ts +54 -4
- package/dist/types/element-map.d.ts.map +1 -1
- package/dist/types/event-map.d.ts +16 -10
- package/dist/types/event-map.d.ts.map +1 -1
- package/dist/types/events.d.ts +59 -117
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/imperative-api.d.ts +3 -1
- package/dist/types/imperative-api.d.ts.map +1 -1
- package/dist/types/index.d.ts +5 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/models.d.ts +11 -31
- package/dist/types/models.d.ts.map +1 -1
- package/dist/types/segments.d.ts +11 -47
- package/dist/types/segments.d.ts.map +1 -1
- package/dist/types/stream-blocks.d.ts +57 -0
- package/dist/types/stream-blocks.d.ts.map +1 -0
- package/dist/types/tools.d.ts +57 -0
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/utils/copy-text.d.ts +24 -0
- package/dist/utils/copy-text.d.ts.map +1 -0
- package/dist/utils/css-escape.d.ts +5 -0
- package/dist/utils/css-escape.d.ts.map +1 -1
- package/dist/utils/tool-input.d.ts +20 -0
- package/dist/utils/tool-input.d.ts.map +1 -0
- package/package.json +10 -3
- package/dist/is-awaiting-reply-BZ_iJ3s3.js.map +0 -1
- package/dist/renderers/segments/artifact/binary-file.d.ts +0 -45
- package/dist/renderers/segments/artifact/binary-file.d.ts.map +0 -1
- package/dist/renderers/segments/artifact/card.d.ts +0 -3
- package/dist/renderers/segments/artifact/card.d.ts.map +0 -1
- package/dist/renderers/segments/artifact/preview-document.d.ts +0 -27
- package/dist/renderers/segments/artifact/preview-document.d.ts.map +0 -1
- package/dist/renderers/segments/artifact/shared.d.ts +0 -29
- package/dist/renderers/segments/artifact/shared.d.ts.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as APARTE_ICON_GLYPHS } from "./glyphs-
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
|
145
|
-
archiveConversation: "Archive
|
|
146
|
-
unarchiveConversation: "Unarchive
|
|
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 "
|
|
323
|
-
return this.
|
|
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
|
-
|
|
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 (
|
|
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)) ||
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
480
|
-
|
|
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
|
|
484
|
-
const
|
|
485
|
-
const
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
this.
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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 &&
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
911
|
-
//
|
|
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"
|
|
996
|
-
dest.
|
|
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
|
-
|
|
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 }) //
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
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
|
-
|
|
3909
|
-
|
|
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
|
-
/**
|
|
4484
|
-
|
|
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
|
|
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 (
|
|
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}`, {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
5583
|
-
*
|
|
5584
|
-
*
|
|
5585
|
-
*
|
|
5586
|
-
*
|
|
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
|
|
5711
|
-
*
|
|
5712
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
6274
|
-
*
|
|
6275
|
-
*
|
|
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`
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
6322
|
+
resolveConfig as $,
|
|
6789
6323
|
APARTE_CONFIG_CHANGE as A,
|
|
6790
6324
|
contextConfig as B,
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
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
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
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=
|
|
6393
|
+
//# sourceMappingURL=dialog-triggers-CVCxqKg3.js.map
|