@aparte/core 0.24.0 → 0.26.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/agent-runner.d.ts.map +1 -1
- package/dist/client/aparte-client.d.ts +38 -76
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/client/stream-adapter.d.ts.map +1 -1
- package/dist/client/wire-history.d.ts +78 -0
- package/dist/client/wire-history.d.ts.map +1 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts +152 -2
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/bubble/bubble-sync.d.ts +5 -1
- package/dist/components/bubble/bubble-sync.d.ts.map +1 -1
- package/dist/components/bubble/checkpoint-separator.d.ts +40 -0
- package/dist/components/bubble/checkpoint-separator.d.ts.map +1 -0
- package/dist/components/chat/aparte-chat.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-toolbar.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer.d.ts.map +1 -1
- package/dist/components/context/aparte-context.d.ts +157 -25
- package/dist/components/context/aparte-context.d.ts.map +1 -1
- package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
- package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
- package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -1
- package/dist/components/sidebar/aparte-sidebar.d.ts.map +1 -1
- package/dist/components/split/aparte-split.d.ts.map +1 -1
- package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
- package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +7 -0
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +47 -1
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/locale.d.ts +62 -4
- package/dist/config/locale.d.ts.map +1 -1
- package/dist/context/checkpoint-state.d.ts +70 -0
- package/dist/context/checkpoint-state.d.ts.map +1 -0
- package/dist/context/checkpoint.d.ts +28 -0
- package/dist/context/checkpoint.d.ts.map +1 -0
- package/dist/context/measure.d.ts +148 -0
- package/dist/context/measure.d.ts.map +1 -0
- package/dist/context/restore.d.ts +24 -0
- package/dist/context/restore.d.ts.map +1 -0
- package/dist/conversations/conversation-controller.d.ts +13 -4
- package/dist/conversations/conversation-controller.d.ts.map +1 -1
- package/dist/conversations/conversation-manager.d.ts +6 -0
- package/dist/conversations/conversation-manager.d.ts.map +1 -1
- package/dist/custom-elements.json +15347 -12878
- package/dist/{dialog-triggers-BqXRdHWZ.js → defer-hydration-8vI8xNNx.js} +1179 -374
- package/dist/defer-hydration-8vI8xNNx.js.map +1 -0
- package/dist/generated/element-attributes.d.ts +8 -2
- package/dist/generated/element-attributes.d.ts.map +1 -1
- package/dist/{glyphs-C8z2oJUg.js → glyphs-2bym5jl5.js} +1 -1
- package/dist/{glyphs-C8z2oJUg.js.map → glyphs-2bym5jl5.js.map} +1 -1
- package/dist/host/aparte-chat-host.d.ts +41 -2
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/icons.js +2 -2
- package/dist/index.css +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1059 -95
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +6 -2
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +39 -29
- package/dist/index.node.js.map +1 -1
- package/dist/interop/defer-hydration.d.ts +34 -0
- package/dist/interop/defer-hydration.d.ts.map +1 -0
- package/dist/primitives/icon/aparte-icon.d.ts.map +1 -1
- package/dist/primitives/progress-spinner/aparte-progress-spinner.d.ts.map +1 -1
- package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
- package/dist/primitives/select/aparte-option.d.ts.map +1 -1
- package/dist/primitives/select/aparte-select.d.ts.map +1 -1
- package/dist/renderers/index.d.ts +2 -1
- package/dist/renderers/index.d.ts.map +1 -1
- package/dist/renderers/mounted-renderer.d.ts +34 -0
- package/dist/renderers/mounted-renderer.d.ts.map +1 -0
- package/dist/renderers/segment-renderers.d.ts +14 -1
- package/dist/renderers/segment-renderers.d.ts.map +1 -1
- package/dist/renderers/segments/tool-call.d.ts +2 -1
- package/dist/renderers/segments/tool-call.d.ts.map +1 -1
- package/dist/transport/agent-handler.d.ts.map +1 -1
- package/dist/transport/backend-handler.d.ts.map +1 -1
- package/dist/transport/types.d.ts +24 -4
- package/dist/transport/types.d.ts.map +1 -1
- package/dist/types/chat.d.ts +16 -1
- package/dist/types/chat.d.ts.map +1 -1
- package/dist/types/compactor.d.ts +104 -0
- package/dist/types/compactor.d.ts.map +1 -0
- package/dist/types/errors.d.ts +2 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/events.d.ts +50 -3
- package/dist/types/events.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/model-provider.d.ts +3 -0
- package/dist/types/model-provider.d.ts.map +1 -1
- package/dist/types/models.d.ts +43 -3
- package/dist/types/models.d.ts.map +1 -1
- package/dist/types/segments.d.ts +22 -4
- package/dist/types/segments.d.ts.map +1 -1
- package/dist/types/tools.d.ts +56 -0
- package/dist/types/tools.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/dialog-triggers-BqXRdHWZ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A, a, b, c, d, f, g, h, i, j, k, l, m, n, p,
|
|
1
|
+
import { K as deferHydrationOf, aa as resolveConfig, ak as eventOrigin, al as activeElementIn, af as subscribeConfigChange, ac as runWithConfig, o as AparteElicitationAbortError, w as buildApprovalPanel, x as buildElicitationPanel, am as gateGlide, O as escapeAttr, an as chatBoundaryOf, ao as toolCallRenderer, ap as toolRendererThatDrew, t as aparteGlobalConfig, aq as isMountedRenderer, ar as renderingVersionOf, H as cssEscape, as as onRenderersChange, at as mergeSegmentUpdate, P as escapeHtml, $ as isSegmentSettled, au as writeStreamedMarkdown, _ as isSafeUrl, C as copyText, U as getSegmentRenderer, W as installDefaultRenderersOnce, r as AparteMessageRepository, av as CheckpointStateWatcher, aw as chatIdOf, ax as stampSegmentActivity, ay as stampSegmentOnInsert, az as stampSegmentOnUpdate, aA as renumberSegments, aB as openSegmentIds, aC as isTerminalStatus, aD as adoptMessageSegments, Y as isAwaitingReply, a4 as populateBubbleFromMessage, aj as uuid, ab as revokeAttachmentUrls, a3 as parseMarkdownToSegments, aE as APARTE_DERIVED_SEGMENTS, aF as rememberConversationStatus, aG as tellConversationStatus, aH as conversationStatusOf, e as APARTE_DEFAULT_LOCALE, aI as usageTokens, q as AparteErrorCode, a0 as measureContext, aJ as rootOf, X as installDialogTriggersOnce } from "./defer-hydration-8vI8xNNx.js";
|
|
2
|
+
import { A, a, b, c, d, f, g, h, i, j, k, l, m, n, p, s, u, v, y, z, B, D, E, F, G, I, J, L, M, N, Q, R, S, T, V, Z, a1, a2, a5, a6, a7, a8, a9, ad, ae, ag, ah, ai } from "./defer-hydration-8vI8xNNx.js";
|
|
3
3
|
function presenceOn(value) {
|
|
4
4
|
return value === "" ? true : Boolean(value);
|
|
5
5
|
}
|
|
@@ -64,6 +64,7 @@ class AparteOption extends HTMLElement {
|
|
|
64
64
|
dot.setAttribute("data-status", status);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
deferHydrationOf(AparteOption);
|
|
67
68
|
if (!customElements.get("aparte-option")) {
|
|
68
69
|
customElements.define("aparte-option", AparteOption);
|
|
69
70
|
}
|
|
@@ -173,6 +174,7 @@ class AparteOptgroup extends HTMLElement {
|
|
|
173
174
|
});
|
|
174
175
|
}
|
|
175
176
|
}
|
|
177
|
+
deferHydrationOf(AparteOptgroup);
|
|
176
178
|
if (!customElements.get("aparte-optgroup")) {
|
|
177
179
|
customElements.define("aparte-optgroup", AparteOptgroup);
|
|
178
180
|
}
|
|
@@ -726,6 +728,7 @@ function optionLabelText(option) {
|
|
|
726
728
|
}
|
|
727
729
|
return text.trim();
|
|
728
730
|
}
|
|
731
|
+
deferHydrationOf(AparteSelect);
|
|
729
732
|
if (!customElements.get("aparte-select")) {
|
|
730
733
|
customElements.define("aparte-select", AparteSelect);
|
|
731
734
|
}
|
|
@@ -760,6 +763,7 @@ class AparteProgressSpinner extends HTMLElement {
|
|
|
760
763
|
this.innerHTML = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><circle class="aparte-spinner-track" cx="12" cy="12" r="${this._r}"/><circle class="aparte-spinner-fill" cx="12" cy="12" r="${this._r}" stroke-dasharray="${dasharray}" stroke-dashoffset="${dashoffset.toFixed(2)}"/></svg>`;
|
|
761
764
|
}
|
|
762
765
|
}
|
|
766
|
+
deferHydrationOf(AparteProgressSpinner);
|
|
763
767
|
if (!customElements.get("aparte-progress-spinner")) {
|
|
764
768
|
customElements.define("aparte-progress-spinner", AparteProgressSpinner);
|
|
765
769
|
}
|
|
@@ -786,6 +790,7 @@ class AparteIcon extends HTMLElement {
|
|
|
786
790
|
this.firstElementChild?.setAttribute("aria-hidden", "true");
|
|
787
791
|
}
|
|
788
792
|
}
|
|
793
|
+
deferHydrationOf(AparteIcon);
|
|
789
794
|
if (!customElements.get("aparte-icon")) {
|
|
790
795
|
customElements.define("aparte-icon", AparteIcon);
|
|
791
796
|
}
|
|
@@ -1100,6 +1105,7 @@ class AparteElicitation extends HTMLElement {
|
|
|
1100
1105
|
}
|
|
1101
1106
|
}
|
|
1102
1107
|
if (typeof customElements !== "undefined" && !customElements.get("aparte-elicitation")) {
|
|
1108
|
+
deferHydrationOf(AparteElicitation);
|
|
1103
1109
|
customElements.define("aparte-elicitation", AparteElicitation);
|
|
1104
1110
|
}
|
|
1105
1111
|
function isTranscriptEmpty(viewport) {
|
|
@@ -1297,9 +1303,100 @@ class AparteChat extends HTMLElement {
|
|
|
1297
1303
|
this.toggleAttribute("data-empty", !viewport || isTranscriptEmpty(viewport));
|
|
1298
1304
|
}
|
|
1299
1305
|
}
|
|
1306
|
+
deferHydrationOf(AparteChat);
|
|
1300
1307
|
if (!customElements.get("aparte-chat")) {
|
|
1301
1308
|
customElements.define("aparte-chat", AparteChat);
|
|
1302
1309
|
}
|
|
1310
|
+
const isStore = (candidate) => !!candidate && typeof candidate.getMessages === "function" && typeof candidate.updateMessage === "function";
|
|
1311
|
+
function storeOf(from) {
|
|
1312
|
+
const boundary = chatBoundaryOf(from);
|
|
1313
|
+
if (isStore(boundary)) return boundary;
|
|
1314
|
+
const viewport = from.closest("aparte-chat-viewport");
|
|
1315
|
+
return isStore(viewport) ? viewport : null;
|
|
1316
|
+
}
|
|
1317
|
+
function restoreContext(from, detail) {
|
|
1318
|
+
const store = storeOf(from);
|
|
1319
|
+
if (!store) return 0;
|
|
1320
|
+
const carrying = store.getMessages().filter((m2) => m2.contextSummary !== void 0).map((m2) => m2.id);
|
|
1321
|
+
for (const id of carrying) store.updateMessage(id, { contextSummary: void 0 });
|
|
1322
|
+
const done = { restored: true };
|
|
1323
|
+
if (detail.messageId) done.messageId = detail.messageId;
|
|
1324
|
+
if (detail.targetId) done.targetId = detail.targetId;
|
|
1325
|
+
window.dispatchEvent(new CustomEvent("aparte-compact-done", { detail: done }));
|
|
1326
|
+
return carrying.length;
|
|
1327
|
+
}
|
|
1328
|
+
function el$1(tag, className, text) {
|
|
1329
|
+
const node = document.createElement(tag);
|
|
1330
|
+
node.className = className;
|
|
1331
|
+
if (text !== void 0) node.textContent = text;
|
|
1332
|
+
return node;
|
|
1333
|
+
}
|
|
1334
|
+
function action(name, label) {
|
|
1335
|
+
const button = el$1("button", "aparte-btn aparte-btn--ghost aparte-btn--sm aparte-checkpoint__action", label);
|
|
1336
|
+
button.type = "button";
|
|
1337
|
+
button.setAttribute("data-checkpoint-action", name);
|
|
1338
|
+
return button;
|
|
1339
|
+
}
|
|
1340
|
+
function buildCheckpointSeparator(options) {
|
|
1341
|
+
const { kind, locale } = options;
|
|
1342
|
+
const root = el$1("div", "aparte-checkpoint");
|
|
1343
|
+
root.setAttribute("role", "group");
|
|
1344
|
+
root.setAttribute("aria-label", locale.checkpointGroup ?? "Context summary");
|
|
1345
|
+
root.setAttribute("data-state", kind);
|
|
1346
|
+
const row = el$1("div", "aparte-checkpoint__row aparte-divider aparte-divider--label");
|
|
1347
|
+
const label = el$1("div", "aparte-checkpoint__label");
|
|
1348
|
+
row.appendChild(label);
|
|
1349
|
+
root.appendChild(row);
|
|
1350
|
+
let sentence;
|
|
1351
|
+
if (kind === "done") {
|
|
1352
|
+
const n2 = options.count ?? 0;
|
|
1353
|
+
sentence = n2 === 1 ? locale.checkpointCountOne ?? "1 message summarised for the model" : (locale.checkpointCount ?? "{n} messages summarised for the model").replace("{n}", () => String(n2));
|
|
1354
|
+
} else if (kind === "legacy") {
|
|
1355
|
+
sentence = locale.checkpointLegacy ?? "Earlier messages were summarised";
|
|
1356
|
+
} else if (kind === "pending") {
|
|
1357
|
+
sentence = locale.checkpointPending ?? "Summarising the context…";
|
|
1358
|
+
} else {
|
|
1359
|
+
sentence = locale.checkpointFailed ?? "Couldn't summarise the context";
|
|
1360
|
+
}
|
|
1361
|
+
const text = el$1("span", "aparte-checkpoint__text", sentence);
|
|
1362
|
+
if (kind === "pending") {
|
|
1363
|
+
text.classList.add("aparte-shimmer");
|
|
1364
|
+
text.setAttribute("role", "status");
|
|
1365
|
+
} else if (kind === "failed") {
|
|
1366
|
+
text.setAttribute("role", "status");
|
|
1367
|
+
if (options.reason) {
|
|
1368
|
+
text.title = options.reason;
|
|
1369
|
+
text.appendChild(el$1("span", "aparte-sr-only", `: ${options.reason}`));
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
label.appendChild(text);
|
|
1373
|
+
if (kind === "done" || kind === "legacy") {
|
|
1374
|
+
if (options.confirming && kind === "done") {
|
|
1375
|
+
const confirm = el$1("span", "aparte-checkpoint__confirm");
|
|
1376
|
+
const question = el$1("span", "aparte-checkpoint__question", locale.checkpointRestoreConfirm ?? "Send the original messages to the model again?");
|
|
1377
|
+
question.id = `${options.summaryId}-question`;
|
|
1378
|
+
const yes = action("restore-confirm", locale.checkpointRestore ?? "Restore");
|
|
1379
|
+
yes.setAttribute("aria-describedby", question.id);
|
|
1380
|
+
confirm.append(question, yes, action("restore-cancel", locale.cancel ?? "Cancel"));
|
|
1381
|
+
label.appendChild(confirm);
|
|
1382
|
+
} else {
|
|
1383
|
+
const toggle = action("toggle", options.open ? locale.checkpointHide ?? "Hide summary" : locale.checkpointShow ?? "Show summary");
|
|
1384
|
+
toggle.setAttribute("aria-expanded", String(options.open));
|
|
1385
|
+
toggle.setAttribute("aria-controls", options.summaryId);
|
|
1386
|
+
label.appendChild(toggle);
|
|
1387
|
+
if (kind === "done") label.appendChild(action("restore", locale.checkpointRestore ?? "Restore"));
|
|
1388
|
+
}
|
|
1389
|
+
} else if (kind === "failed") {
|
|
1390
|
+
label.appendChild(action("retry", locale.retry ?? "Retry"));
|
|
1391
|
+
}
|
|
1392
|
+
if (kind === "done" || kind === "legacy") {
|
|
1393
|
+
const panel = el$1("div", "aparte-checkpoint__summary aparte-segment-content");
|
|
1394
|
+
panel.id = options.summaryId;
|
|
1395
|
+
if (!options.open) panel.hidden = true;
|
|
1396
|
+
root.appendChild(panel);
|
|
1397
|
+
}
|
|
1398
|
+
return root;
|
|
1399
|
+
}
|
|
1303
1400
|
let _warnedNoRenderer = false;
|
|
1304
1401
|
function warnMissingRenderer(type) {
|
|
1305
1402
|
if (_warnedNoRenderer) return;
|
|
@@ -1342,6 +1439,7 @@ function flushAvatarMarks() {
|
|
|
1342
1439
|
});
|
|
1343
1440
|
batch.forEach(([avatar], i2) => avatar.toggleAttribute("data-image", shown[i2]));
|
|
1344
1441
|
}
|
|
1442
|
+
let checkpointIds = 0;
|
|
1345
1443
|
class AparteChatBubble extends HTMLElement {
|
|
1346
1444
|
_contentEl = null;
|
|
1347
1445
|
_segmentsEl = null;
|
|
@@ -1355,6 +1453,23 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1355
1453
|
_role = "assistant";
|
|
1356
1454
|
_attachments = [];
|
|
1357
1455
|
_usage = null;
|
|
1456
|
+
_finishReason = null;
|
|
1457
|
+
/** The checkpoint this message carries: the separator above it is drawn from it. */
|
|
1458
|
+
_contextSummary = null;
|
|
1459
|
+
/**
|
|
1460
|
+
* This message is one an older compaction wrote (`compaction: true`), as the host told
|
|
1461
|
+
* us. `null` until told: the `data-kind="compaction"` attribute the viewport and the
|
|
1462
|
+
* wrappers stamp from the same flag answers meanwhile.
|
|
1463
|
+
*/
|
|
1464
|
+
_legacyCompaction = null;
|
|
1465
|
+
/** The summary panel under the separator is open. Kept here, not in the DOM, because a redraw rebuilds the DOM. */
|
|
1466
|
+
_checkpointOpen = false;
|
|
1467
|
+
/** Restore asked for confirmation and has not been answered. */
|
|
1468
|
+
_confirmingRestore = false;
|
|
1469
|
+
/** What the separator was last drawn from, so a re-sync that changes nothing leaves it (and a focused button) alone. */
|
|
1470
|
+
_checkpointDrawn = "";
|
|
1471
|
+
/** Names the separator's panel for `aria-controls`, unique on the page without needing a message id. */
|
|
1472
|
+
_checkpointId = `aparte-checkpoint-${++checkpointIds}`;
|
|
1358
1473
|
/** Cleanup returned by the avatar provider — called on disconnect/re-render. */
|
|
1359
1474
|
_avatarCleanup = null;
|
|
1360
1475
|
/** Whether the avatar slot holds what a provider drew, which goes when the provider does. */
|
|
@@ -1384,7 +1499,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1384
1499
|
*/
|
|
1385
1500
|
_editReturnAction = null;
|
|
1386
1501
|
static get observedAttributes() {
|
|
1387
|
-
return ["data-role", "content", "timestamp", "message-id", "streaming", "name"];
|
|
1502
|
+
return ["data-role", "content", "timestamp", "message-id", "streaming", "name", "data-checkpoint", "data-checkpoint-reason"];
|
|
1388
1503
|
}
|
|
1389
1504
|
constructor() {
|
|
1390
1505
|
super();
|
|
@@ -1399,7 +1514,11 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1399
1514
|
this._updateName();
|
|
1400
1515
|
this._updateLocalizedLabels();
|
|
1401
1516
|
this._updateWaiting();
|
|
1517
|
+
this._updateFinishNote();
|
|
1518
|
+
this._checkpointDrawn = "";
|
|
1519
|
+
this._updateCheckpoint();
|
|
1402
1520
|
this._renderAvatar();
|
|
1521
|
+
this._redrawSegmentsWhoseRendererChanged();
|
|
1403
1522
|
this._relabelSegments();
|
|
1404
1523
|
this._updateTimestamp(this.getAttribute("timestamp"));
|
|
1405
1524
|
if (!this._redrawQueued) {
|
|
@@ -1414,16 +1533,151 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1414
1533
|
_redrawQueued = false;
|
|
1415
1534
|
/** The {@link renderingVersionOf} count the content was last drawn with. */
|
|
1416
1535
|
_drawnWithVersion = 0;
|
|
1536
|
+
/** Every segment on screen, by id: see {@link DrawnSegment}. */
|
|
1537
|
+
_drawn = /* @__PURE__ */ new Map();
|
|
1538
|
+
/** The segments were released because the bubble was still out of the page at the next microtask. */
|
|
1539
|
+
_releasedWhileDetached = false;
|
|
1540
|
+
/** A {@link _scheduleReleaseCheck} is queued. */
|
|
1541
|
+
_releaseCheckQueued = false;
|
|
1542
|
+
/** Unsubscribes from the renderer registries' channel; set while connected. */
|
|
1543
|
+
_offRenderers = null;
|
|
1544
|
+
/** The tool renderer in effect for a call: none for a hidden call or another segment. */
|
|
1545
|
+
_toolRendererFor(segment) {
|
|
1546
|
+
if (segment.type !== "tool_call") return null;
|
|
1547
|
+
const cfg = this._cfg;
|
|
1548
|
+
if (resolveSegmentRenderer("tool_call", cfg) !== toolCallRenderer) return null;
|
|
1549
|
+
const name = segment.toolCall?.name;
|
|
1550
|
+
if (!name || cfg.isToolHidden(name)) return null;
|
|
1551
|
+
return cfg.getToolRenderer(name) ?? null;
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Record a segment as drawn, set it up, then read back which tool renderer is behind the
|
|
1555
|
+
* row (tool-call.ts records it as the row is set up — one record, not a second lookup).
|
|
1556
|
+
* Recorded BEFORE `setup`: a setup that throws (a framework mount) leaves an element that
|
|
1557
|
+
* will still be cleaned up. The same id drawn twice releases the first element.
|
|
1558
|
+
*/
|
|
1559
|
+
_install(segment, el2, renderer) {
|
|
1560
|
+
const previous = this._drawn.get(segment.id);
|
|
1561
|
+
if (previous && previous.el !== el2) this._release(segment.id);
|
|
1562
|
+
const drawn = { el: el2, renderer, tool: null, segment };
|
|
1563
|
+
this._drawn.set(segment.id, drawn);
|
|
1564
|
+
if (!this.isConnected) this._scheduleReleaseCheck();
|
|
1565
|
+
if (renderer?.setup) runWithConfig(this._cfg, () => renderer.setup(el2, segment));
|
|
1566
|
+
if (segment.type === "tool_call") drawn.tool = toolRendererThatDrew(el2);
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* The segment's element leaves for good: its renderer's `cleanup`, once. A cleanup that
|
|
1570
|
+
* throws is reported and costs nothing else — the element still goes, and the others
|
|
1571
|
+
* are still released.
|
|
1572
|
+
*/
|
|
1573
|
+
_release(id) {
|
|
1574
|
+
const drawn = this._drawn.get(id);
|
|
1575
|
+
if (!drawn) return;
|
|
1576
|
+
this._drawn.delete(id);
|
|
1577
|
+
const cleanup = drawn.renderer?.cleanup;
|
|
1578
|
+
if (!cleanup) return;
|
|
1579
|
+
try {
|
|
1580
|
+
runWithConfig(this._cfg, () => cleanup.call(drawn.renderer, drawn.el, drawn.segment));
|
|
1581
|
+
} catch (err) {
|
|
1582
|
+
console.error("[aparte] a segment renderer's cleanup threw", err);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
_releaseAll() {
|
|
1586
|
+
for (const id of [...this._drawn.keys()]) this._release(id);
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Gone for good, or only moved? A keyed list moving a row takes it out and puts it back
|
|
1590
|
+
* before the next microtask, and must not unmount what its segments hold: decide then.
|
|
1591
|
+
*/
|
|
1592
|
+
_scheduleReleaseCheck() {
|
|
1593
|
+
if (this._releaseCheckQueued) return;
|
|
1594
|
+
this._releaseCheckQueued = true;
|
|
1595
|
+
queueMicrotask(() => {
|
|
1596
|
+
this._releaseCheckQueued = false;
|
|
1597
|
+
if (this.isConnected || this._drawn.size === 0) return;
|
|
1598
|
+
this._releaseAll();
|
|
1599
|
+
this._releasedWhileDetached = true;
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
/** One segment drawn again in place: its renderer cleans up, the current one draws. False when it was never drawn. */
|
|
1603
|
+
_redrawSegment(segment) {
|
|
1604
|
+
const drawn = this._drawn.get(segment.id);
|
|
1605
|
+
if (!drawn) return false;
|
|
1606
|
+
const cfg = this._cfg;
|
|
1607
|
+
const renderer = resolveSegmentRenderer(segment.type, cfg);
|
|
1608
|
+
const old = drawn.el;
|
|
1609
|
+
this._release(segment.id);
|
|
1610
|
+
const el2 = renderer ? segmentRenderResultToElement(runWithConfig(cfg, () => renderer.render(segment)), segment) : unrenderedSegment(segment);
|
|
1611
|
+
if (!el2) {
|
|
1612
|
+
old.remove();
|
|
1613
|
+
return true;
|
|
1614
|
+
}
|
|
1615
|
+
old.replaceWith(el2);
|
|
1616
|
+
this._install(segment, el2, renderer);
|
|
1617
|
+
return true;
|
|
1618
|
+
}
|
|
1619
|
+
/** The renderer that drew this segment is still the one in effect (for a tool call, the tool renderer too). */
|
|
1620
|
+
_stillDrawnByCurrent(segment, drawn) {
|
|
1621
|
+
return resolveSegmentRenderer(segment.type, this._cfg) === drawn.renderer && this._toolRendererFor(segment) === drawn.tool;
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* A renderer registered, unregistered or overridden since these segments were drawn: draw
|
|
1625
|
+
* again only the segments whose renderer is no longer the one that drew them (for a tool
|
|
1626
|
+
* call, the tool renderer counts), in place. The others keep their elements — and with
|
|
1627
|
+
* them anything mounted in them, which a redraw of the whole message would unmount.
|
|
1628
|
+
*/
|
|
1629
|
+
_redrawSegmentsWhoseRendererChanged() {
|
|
1630
|
+
if (!this.isConnected || !this._segmentsEl || this._drawn.size === 0) return;
|
|
1631
|
+
for (const segment of this._segments) {
|
|
1632
|
+
const drawn = this._drawn.get(segment.id);
|
|
1633
|
+
if (!drawn || this._stillDrawnByCurrent(segment, drawn)) continue;
|
|
1634
|
+
this._redrawSegment(segment);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* The registries' channel: a renderer changed on our config — or on the global one, whose
|
|
1639
|
+
* segment renderers a chat with its own config inherits.
|
|
1640
|
+
*/
|
|
1641
|
+
_onRenderersChange = (cfg) => {
|
|
1642
|
+
if (cfg !== this._cfg && cfg !== aparteGlobalConfig) return;
|
|
1643
|
+
this._redrawSegmentsWhoseRendererChanged();
|
|
1644
|
+
};
|
|
1645
|
+
/**
|
|
1646
|
+
* A card a mounted renderer keeps (a framework component): its drawing reads none of the
|
|
1647
|
+
* providers a rendering-version bump announces, so it stays mounted through one — as long
|
|
1648
|
+
* as that renderer is still the one in effect.
|
|
1649
|
+
*/
|
|
1650
|
+
_keepsItsDrawing(segment, drawn) {
|
|
1651
|
+
const mounted = isMountedRenderer(drawn.renderer) || segment.type === "tool_call" && isMountedRenderer(drawn.tool);
|
|
1652
|
+
return mounted && this._stillDrawnByCurrent(segment, drawn);
|
|
1653
|
+
}
|
|
1417
1654
|
/**
|
|
1418
1655
|
* A Markdown or highlight provider set after this message was drawn: draw it again.
|
|
1419
1656
|
* Not mid-stream — the stream reads the providers as it goes, and its end redraws — and
|
|
1420
1657
|
* not mid-edit, where the drawn message is hidden behind the editor and drawing it would
|
|
1421
1658
|
* show it again beside the editor; leaving the edit calls this.
|
|
1659
|
+
*
|
|
1660
|
+
* Segment by segment, keeping the cards a mounted renderer holds: a highlighter that loads
|
|
1661
|
+
* after the first answer must not remount every card on the page and lose its state.
|
|
1422
1662
|
*/
|
|
1423
1663
|
_redrawIfStale() {
|
|
1424
1664
|
if (this._streaming || this._editing || renderingVersionOf(this._cfg) === this._drawnWithVersion) return;
|
|
1425
|
-
if (this._segments.length
|
|
1426
|
-
|
|
1665
|
+
if (this._segments.length === 0) {
|
|
1666
|
+
this._updateContent();
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
if (this._segments.some((s2) => !this._drawn.has(s2.id))) {
|
|
1670
|
+
this._renderSegments();
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
this._drawnWithVersion = renderingVersionOf(this._cfg);
|
|
1674
|
+
for (const segment of this._segments) {
|
|
1675
|
+
const drawn = this._drawn.get(segment.id);
|
|
1676
|
+
if (this._keepsItsDrawing(segment, drawn)) continue;
|
|
1677
|
+
this._redrawSegment(segment);
|
|
1678
|
+
}
|
|
1679
|
+
this._reflectError();
|
|
1680
|
+
this._syncCopyAction();
|
|
1427
1681
|
}
|
|
1428
1682
|
/**
|
|
1429
1683
|
* Ask every rendered segment to re-read its config-derived text.
|
|
@@ -1482,9 +1736,16 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1482
1736
|
this._render();
|
|
1483
1737
|
this._updateContent();
|
|
1484
1738
|
this._updateTimestamp(this.getAttribute("timestamp"));
|
|
1739
|
+
if (this._releasedWhileDetached) {
|
|
1740
|
+
this._releasedWhileDetached = false;
|
|
1741
|
+
if (this._segments.length > 0) this._renderSegments();
|
|
1742
|
+
}
|
|
1485
1743
|
window.addEventListener("aparte-config-change", this._onConfigChange);
|
|
1744
|
+
this._offRenderers ??= onRenderersChange(this._onRenderersChange);
|
|
1486
1745
|
this.addEventListener("click", this._onBranchPickerClick);
|
|
1487
1746
|
this.addEventListener("click", this._onLinkClick);
|
|
1747
|
+
this.addEventListener("click", this._onCheckpointClick);
|
|
1748
|
+
this.addEventListener("keydown", this._onCheckpointKeydown);
|
|
1488
1749
|
}
|
|
1489
1750
|
/**
|
|
1490
1751
|
* The boundary above this bubble changed — under every wrapper the chat's own config is
|
|
@@ -1499,7 +1760,10 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1499
1760
|
* into one.
|
|
1500
1761
|
*/
|
|
1501
1762
|
aparteConfigChanged() {
|
|
1502
|
-
queueMicrotask(() =>
|
|
1763
|
+
queueMicrotask(() => {
|
|
1764
|
+
this._redrawFrameIfChanged();
|
|
1765
|
+
this._redrawSegmentsWhoseRendererChanged();
|
|
1766
|
+
});
|
|
1503
1767
|
}
|
|
1504
1768
|
_redrawFrameIfChanged() {
|
|
1505
1769
|
if (!this.isConnected || this._editing) return;
|
|
@@ -1512,6 +1776,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1512
1776
|
this._avatarCleanup = null;
|
|
1513
1777
|
}
|
|
1514
1778
|
this._resetMarkdownStream();
|
|
1779
|
+
this._releaseAll();
|
|
1515
1780
|
this.replaceChildren();
|
|
1516
1781
|
this._render();
|
|
1517
1782
|
this._updateContent();
|
|
@@ -1519,11 +1784,16 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1519
1784
|
this._updateAttachments();
|
|
1520
1785
|
this._updateTimestamp(this.getAttribute("timestamp"));
|
|
1521
1786
|
this._updateBranchPicker();
|
|
1787
|
+
this._updateCheckpoint();
|
|
1522
1788
|
}
|
|
1523
1789
|
disconnectedCallback() {
|
|
1524
1790
|
window.removeEventListener("aparte-config-change", this._onConfigChange);
|
|
1791
|
+
this._offRenderers?.();
|
|
1792
|
+
this._offRenderers = null;
|
|
1525
1793
|
this.removeEventListener("click", this._onBranchPickerClick);
|
|
1526
1794
|
this.removeEventListener("click", this._onLinkClick);
|
|
1795
|
+
this.removeEventListener("click", this._onCheckpointClick);
|
|
1796
|
+
this.removeEventListener("keydown", this._onCheckpointKeydown);
|
|
1527
1797
|
if (this._avatarCleanup) {
|
|
1528
1798
|
try {
|
|
1529
1799
|
this._avatarCleanup();
|
|
@@ -1531,6 +1801,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1531
1801
|
}
|
|
1532
1802
|
this._avatarCleanup = null;
|
|
1533
1803
|
}
|
|
1804
|
+
this._scheduleReleaseCheck();
|
|
1534
1805
|
}
|
|
1535
1806
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
1536
1807
|
if (oldValue === newValue) return;
|
|
@@ -1549,6 +1820,12 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1549
1820
|
case "timestamp":
|
|
1550
1821
|
this._updateTimestamp(newValue);
|
|
1551
1822
|
break;
|
|
1823
|
+
case "data-checkpoint":
|
|
1824
|
+
this._updateCheckpoint();
|
|
1825
|
+
break;
|
|
1826
|
+
case "data-checkpoint-reason":
|
|
1827
|
+
if (this.getAttribute("data-checkpoint") === "failed") this._updateCheckpoint();
|
|
1828
|
+
break;
|
|
1552
1829
|
case "streaming":
|
|
1553
1830
|
this._updateStreaming(newValue !== null && newValue !== "false");
|
|
1554
1831
|
break;
|
|
@@ -1591,10 +1868,38 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1591
1868
|
}
|
|
1592
1869
|
/** Set segments for rich content */
|
|
1593
1870
|
setSegments(segments) {
|
|
1594
|
-
this.
|
|
1595
|
-
this._renderSegments();
|
|
1871
|
+
this._replaceSegments(segments);
|
|
1596
1872
|
this._updateWaiting();
|
|
1597
1873
|
}
|
|
1874
|
+
/** A whole new list, from `setSegments` or `updateMessage({ segments })`: copied in, then drawn. */
|
|
1875
|
+
_replaceSegments(segments) {
|
|
1876
|
+
const next = [...segments];
|
|
1877
|
+
if (this._resyncKeepingCards(next)) return;
|
|
1878
|
+
this._segments = next;
|
|
1879
|
+
this._renderSegments();
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* The same segments again, in the same order — the host re-syncing a message when its turn
|
|
1883
|
+
* ends — while a mounted renderer holds one of them: the cards it holds are handed the new
|
|
1884
|
+
* state, and only the other segments are drawn again, in place. Drawing everything again
|
|
1885
|
+
* unmounted every framework card at the end of every turn and lost its state. False, and
|
|
1886
|
+
* nothing touched, in every other case: those keep the full redraw.
|
|
1887
|
+
*/
|
|
1888
|
+
_resyncKeepingCards(next) {
|
|
1889
|
+
if (!this._segmentsEl || next.length !== this._segments.length) return false;
|
|
1890
|
+
if (next.some((s2, i2) => s2.id !== this._segments[i2].id || !this._drawn.has(s2.id))) return false;
|
|
1891
|
+
const kept = new Set(next.filter((s2) => this._keepsItsDrawing(s2, this._drawn.get(s2.id))).map((s2) => s2.id));
|
|
1892
|
+
if (kept.size === 0) return false;
|
|
1893
|
+
this._segments = next;
|
|
1894
|
+
this._drawnWithVersion = renderingVersionOf(this._cfg);
|
|
1895
|
+
for (const segment of next) {
|
|
1896
|
+
if (kept.has(segment.id)) this._applySegmentUpdate(segment.id, segment, segment);
|
|
1897
|
+
else this._redrawSegment(segment);
|
|
1898
|
+
}
|
|
1899
|
+
this._reflectError();
|
|
1900
|
+
this._syncCopyAction();
|
|
1901
|
+
return true;
|
|
1902
|
+
}
|
|
1598
1903
|
/** Add a segment */
|
|
1599
1904
|
addSegment(segment) {
|
|
1600
1905
|
this._segments.push(segment);
|
|
@@ -1645,6 +1950,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1645
1950
|
if (index !== -1) {
|
|
1646
1951
|
this._segments.splice(index, 1);
|
|
1647
1952
|
}
|
|
1953
|
+
this._release(segmentId);
|
|
1648
1954
|
const el2 = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
|
|
1649
1955
|
el2?.remove();
|
|
1650
1956
|
this._updateWaiting();
|
|
@@ -1669,6 +1975,38 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1669
1975
|
this._usage = usage ?? null;
|
|
1670
1976
|
this._updateActionBar();
|
|
1671
1977
|
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Say why the model stopped writing this reply (assistant only).
|
|
1980
|
+
*
|
|
1981
|
+
* Only `'length'` draws anything: a quiet note after the content that the reply was cut
|
|
1982
|
+
* off, because otherwise a truncated answer reads as a finished one. Every other reason
|
|
1983
|
+
* is the ordinary end of a turn and says nothing. The note waits for the turn to settle.
|
|
1984
|
+
*/
|
|
1985
|
+
setFinishReason(reason) {
|
|
1986
|
+
this._finishReason = reason ?? null;
|
|
1987
|
+
this._updateFinishNote();
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Say that the messages before this one were summarised for the model, or that they no
|
|
1991
|
+
* longer are (`null`).
|
|
1992
|
+
*
|
|
1993
|
+
* Draws, or removes, the separator above the message: "{n} messages summarised for the
|
|
1994
|
+
* model", with Show summary and Restore. The separator sits OUTSIDE `.aparte-message`
|
|
1995
|
+
* and inside this element, so it is not part of the message's frame and survives the
|
|
1996
|
+
* frame being drawn again. `legacy: true` says the message is itself the summary an
|
|
1997
|
+
* older compaction wrote (`compaction: true`): it is replaced by the separator, which
|
|
1998
|
+
* then has no count and no Restore. Omit `legacy` to leave what the host last said (or
|
|
1999
|
+
* the `data-kind="compaction"` attribute) in force.
|
|
2000
|
+
*/
|
|
2001
|
+
setContextSummary(summary, options) {
|
|
2002
|
+
this._contextSummary = summary ?? null;
|
|
2003
|
+
if (options?.legacy !== void 0) this._legacyCompaction = options.legacy;
|
|
2004
|
+
if (!this._contextSummary) {
|
|
2005
|
+
this._confirmingRestore = false;
|
|
2006
|
+
if (!this._isLegacyCompaction()) this._checkpointOpen = false;
|
|
2007
|
+
}
|
|
2008
|
+
this._updateCheckpoint();
|
|
2009
|
+
}
|
|
1672
2010
|
/**
|
|
1673
2011
|
* Update the branch picker UI for tree-based navigation.
|
|
1674
2012
|
* The viewport calls this after a branch switch or re-render.
|
|
@@ -1693,8 +2031,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1693
2031
|
this._updateContent();
|
|
1694
2032
|
}
|
|
1695
2033
|
if ("segments" in updates) {
|
|
1696
|
-
this.
|
|
1697
|
-
this._renderSegments();
|
|
2034
|
+
this._replaceSegments(updates.segments);
|
|
1698
2035
|
}
|
|
1699
2036
|
if ("timestamp" in updates) {
|
|
1700
2037
|
this._updateTimestamp(updates.timestamp);
|
|
@@ -1708,7 +2045,170 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1708
2045
|
this._updateAttachments();
|
|
1709
2046
|
this._updateWaiting();
|
|
1710
2047
|
}
|
|
2048
|
+
if ("finishReason" in updates) this.setFinishReason(updates.finishReason);
|
|
2049
|
+
if ("contextSummary" in updates || "compaction" in updates) {
|
|
2050
|
+
this.setContextSummary(
|
|
2051
|
+
"contextSummary" in updates ? updates.contextSummary : this._contextSummary,
|
|
2052
|
+
"compaction" in updates ? { legacy: updates.compaction === true } : void 0
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Draw, or remove, the note under a reply cut off at the model's length limit.
|
|
2058
|
+
*
|
|
2059
|
+
* Created here rather than in the shell markup: a custom shell written from the contract
|
|
2060
|
+
* has no such region, and the note is only ever wanted next to the content it qualifies.
|
|
2061
|
+
* A shell with no content region simply shows none (the same null-guarded degradation as
|
|
2062
|
+
* the other regions).
|
|
2063
|
+
*/
|
|
2064
|
+
_updateFinishNote() {
|
|
2065
|
+
const show = this._finishReason === "length" && !this._streaming && this._role !== "user";
|
|
2066
|
+
let note = this.querySelector(".aparte-finish-note");
|
|
2067
|
+
if (!show) {
|
|
2068
|
+
note?.remove();
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
if (!note) {
|
|
2072
|
+
const region = this.querySelector(".aparte-message-content");
|
|
2073
|
+
if (!region) return;
|
|
2074
|
+
note = document.createElement("div");
|
|
2075
|
+
note.className = "aparte-finish-note";
|
|
2076
|
+
region.after(note);
|
|
2077
|
+
}
|
|
2078
|
+
note.textContent = this._cfg.getLocale().responseCutOff ?? "Cut off at the model's length limit.";
|
|
2079
|
+
}
|
|
2080
|
+
/** This message was written by an older compaction, as the host said or as its `data-kind` stamp says. */
|
|
2081
|
+
_isLegacyCompaction() {
|
|
2082
|
+
return this._legacyCompaction ?? this.getAttribute("data-kind") === "compaction";
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* What the separator should say, or `null` for none. A compaction in progress or
|
|
2086
|
+
* failed outranks what the message already carries: it is what is happening now.
|
|
2087
|
+
*/
|
|
2088
|
+
_checkpointKind() {
|
|
2089
|
+
const run = this.getAttribute("data-checkpoint");
|
|
2090
|
+
if (run === "pending" || run === "failed") return run;
|
|
2091
|
+
if (this._contextSummary) return "done";
|
|
2092
|
+
return this._isLegacyCompaction() ? "legacy" : null;
|
|
2093
|
+
}
|
|
2094
|
+
/** The text the summary panel shows: the checkpoint's summary, or the older compaction message itself. */
|
|
2095
|
+
_checkpointSummaryText() {
|
|
2096
|
+
return this._contextSummary ? this._contextSummary.summary : this._content;
|
|
1711
2097
|
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Draw the separator above the message, or take it off.
|
|
2100
|
+
*
|
|
2101
|
+
* Written into the light DOM as the FIRST child of the bubble, outside `.aparte-message`:
|
|
2102
|
+
* a custom shell roots at that class and is free to be anything, and the separator is not
|
|
2103
|
+
* its to carry. Drawn from `_render` (so every frame gets it) and from every setter that
|
|
2104
|
+
* changes what it says. A re-sync that changes nothing — the framework host calls this on
|
|
2105
|
+
* every render of every bubble — finds the same signature and the element in place and
|
|
2106
|
+
* leaves both alone, so a keyboard user's focus and a scrolled summary are not lost to a
|
|
2107
|
+
* rebuild; one that does change something rebuilds it and puts the focus back on the
|
|
2108
|
+
* button that had it (or on `focus`, which a click that swaps the buttons names).
|
|
2109
|
+
*
|
|
2110
|
+
* A message an older compaction wrote is marked `data-superseded` on its own row: it
|
|
2111
|
+
* stays in the DOM and does not paint (bubble.css), the separator speaking for it.
|
|
2112
|
+
*/
|
|
2113
|
+
_updateCheckpoint(focus) {
|
|
2114
|
+
const frame = this.querySelector(".aparte-message");
|
|
2115
|
+
if (!frame) return;
|
|
2116
|
+
const kind = this._checkpointKind();
|
|
2117
|
+
frame.toggleAttribute("data-superseded", this._isLegacyCompaction() && !this._contextSummary);
|
|
2118
|
+
const current = this.querySelector(":scope > .aparte-checkpoint");
|
|
2119
|
+
if (!kind) {
|
|
2120
|
+
const holdsFocus = current && document.activeElement && current.contains(document.activeElement);
|
|
2121
|
+
current?.remove();
|
|
2122
|
+
this._checkpointDrawn = "";
|
|
2123
|
+
if (holdsFocus) {
|
|
2124
|
+
if (!frame.hasAttribute("tabindex")) frame.setAttribute("tabindex", "-1");
|
|
2125
|
+
frame.focus({ preventScroll: true });
|
|
2126
|
+
}
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
const reason = this.getAttribute("data-checkpoint-reason") ?? "";
|
|
2130
|
+
const summary = this._checkpointSummaryText();
|
|
2131
|
+
const signature = JSON.stringify([
|
|
2132
|
+
kind,
|
|
2133
|
+
this._contextSummary?.count,
|
|
2134
|
+
this._contextSummary?.createdAt,
|
|
2135
|
+
reason,
|
|
2136
|
+
this._checkpointOpen,
|
|
2137
|
+
this._confirmingRestore,
|
|
2138
|
+
this._checkpointOpen ? summary : ""
|
|
2139
|
+
]);
|
|
2140
|
+
if (current && !focus && signature === this._checkpointDrawn) return;
|
|
2141
|
+
const active = document.activeElement;
|
|
2142
|
+
const hadFocus = current && active && current.contains(active) ? active.getAttribute("data-checkpoint-action") : null;
|
|
2143
|
+
const next = buildCheckpointSeparator({
|
|
2144
|
+
kind,
|
|
2145
|
+
count: this._contextSummary?.count,
|
|
2146
|
+
reason,
|
|
2147
|
+
open: this._checkpointOpen,
|
|
2148
|
+
confirming: this._confirmingRestore,
|
|
2149
|
+
summaryId: this._checkpointId,
|
|
2150
|
+
locale: this._cfg.getLocale()
|
|
2151
|
+
});
|
|
2152
|
+
if (this._checkpointOpen && (kind === "done" || kind === "legacy")) {
|
|
2153
|
+
const panel = next.querySelector(".aparte-checkpoint__summary");
|
|
2154
|
+
if (panel) panel.innerHTML = this._cfg.renderMarkdown(summary);
|
|
2155
|
+
}
|
|
2156
|
+
if (current) current.replaceWith(next);
|
|
2157
|
+
else this.prepend(next);
|
|
2158
|
+
this._checkpointDrawn = signature;
|
|
2159
|
+
const wanted = focus ?? hadFocus;
|
|
2160
|
+
if (wanted) next.querySelector(`[data-checkpoint-action="${cssEscape(wanted)}"]`)?.focus();
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* The separator's buttons. One delegated listener on the bubble, for the reason the branch
|
|
2164
|
+
* picker has one: the separator is rebuilt on every change, and a listener per button would
|
|
2165
|
+
* be lost with it.
|
|
2166
|
+
*/
|
|
2167
|
+
_onCheckpointClick = (event) => {
|
|
2168
|
+
const button = event.target?.closest?.("[data-checkpoint-action]");
|
|
2169
|
+
const separator = button?.closest(".aparte-checkpoint");
|
|
2170
|
+
if (!button || !separator || separator.parentElement !== this) return;
|
|
2171
|
+
switch (button.getAttribute("data-checkpoint-action")) {
|
|
2172
|
+
case "toggle":
|
|
2173
|
+
this._checkpointOpen = !this._checkpointOpen;
|
|
2174
|
+
this._updateCheckpoint("toggle");
|
|
2175
|
+
break;
|
|
2176
|
+
case "restore":
|
|
2177
|
+
this._confirmingRestore = true;
|
|
2178
|
+
this._updateCheckpoint("restore-confirm");
|
|
2179
|
+
break;
|
|
2180
|
+
case "restore-cancel":
|
|
2181
|
+
this._confirmingRestore = false;
|
|
2182
|
+
this._updateCheckpoint("restore");
|
|
2183
|
+
break;
|
|
2184
|
+
case "restore-confirm": {
|
|
2185
|
+
this._confirmingRestore = false;
|
|
2186
|
+
const messageId = this.getAttribute("message-id") ?? void 0;
|
|
2187
|
+
restoreContext(this, { messageId, targetId: this._resolveTargetId() });
|
|
2188
|
+
this.setContextSummary(null);
|
|
2189
|
+
break;
|
|
2190
|
+
}
|
|
2191
|
+
case "retry": {
|
|
2192
|
+
const targetId = this._resolveTargetId();
|
|
2193
|
+
const compactor = this._cfg.getCompactor();
|
|
2194
|
+
if (compactor) {
|
|
2195
|
+
void compactor.compact({ target: chatBoundaryOf(this), ...targetId ? { targetId } : {}, force: true, reason: "retry" });
|
|
2196
|
+
} else {
|
|
2197
|
+
window.dispatchEvent(new CustomEvent("aparte-compact", { detail: { targetId } }));
|
|
2198
|
+
}
|
|
2199
|
+
break;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
/** Escape withdraws Restore's question, the way it closes every other inline prompt here. */
|
|
2204
|
+
_onCheckpointKeydown = (event) => {
|
|
2205
|
+
if (event.key !== "Escape" || !this._confirmingRestore) return;
|
|
2206
|
+
const separator = event.target?.closest?.(".aparte-checkpoint");
|
|
2207
|
+
if (!separator || separator.parentElement !== this) return;
|
|
2208
|
+
event.stopPropagation();
|
|
2209
|
+
this._confirmingRestore = false;
|
|
2210
|
+
this._updateCheckpoint("restore");
|
|
2211
|
+
};
|
|
1712
2212
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1713
2213
|
// Private Methods
|
|
1714
2214
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1723,10 +2223,12 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1723
2223
|
const el2 = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)), segment);
|
|
1724
2224
|
if (el2) {
|
|
1725
2225
|
this._segmentsEl.appendChild(el2);
|
|
1726
|
-
|
|
2226
|
+
this._install(segment, el2, renderer);
|
|
1727
2227
|
}
|
|
1728
2228
|
} else {
|
|
1729
|
-
|
|
2229
|
+
const el2 = unrenderedSegment(segment);
|
|
2230
|
+
this._segmentsEl.appendChild(el2);
|
|
2231
|
+
this._install(segment, el2, void 0);
|
|
1730
2232
|
}
|
|
1731
2233
|
if (this._contentEl) this._contentEl.style.display = "none";
|
|
1732
2234
|
this._reflectError();
|
|
@@ -1737,15 +2239,32 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1737
2239
|
this._renderSegments();
|
|
1738
2240
|
return;
|
|
1739
2241
|
}
|
|
2242
|
+
if (this._releasedWhileDetached) return;
|
|
1740
2243
|
const renderer = resolveSegmentRenderer(segment.type, this._cfg);
|
|
1741
2244
|
if (!renderer) return;
|
|
2245
|
+
const drawnBefore = this._drawn.get(segmentId);
|
|
2246
|
+
if (drawnBefore && drawnBefore.renderer !== renderer) {
|
|
2247
|
+
drawnBefore.segment = segment;
|
|
2248
|
+
this._redrawSegment(segment);
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
1742
2251
|
if (renderer.update) {
|
|
1743
2252
|
runWithConfig(this._cfg, () => renderer.update(el2, segment));
|
|
2253
|
+
const drawn = this._drawn.get(segmentId);
|
|
2254
|
+
if (drawn) {
|
|
2255
|
+
drawn.segment = segment;
|
|
2256
|
+
const now = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
|
|
2257
|
+
if (now && now !== drawn.el) {
|
|
2258
|
+
drawn.el = now;
|
|
2259
|
+
drawn.tool = segment.type === "tool_call" ? toolRendererThatDrew(now) : null;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
1744
2262
|
} else {
|
|
1745
2263
|
const newEl = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)), segment);
|
|
1746
2264
|
if (newEl) {
|
|
2265
|
+
this._release(segmentId);
|
|
1747
2266
|
el2.replaceWith(newEl);
|
|
1748
|
-
|
|
2267
|
+
this._install(segment, newEl, renderer);
|
|
1749
2268
|
}
|
|
1750
2269
|
}
|
|
1751
2270
|
if ("collapsed" in updates) {
|
|
@@ -1869,6 +2388,8 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1869
2388
|
this._renderAvatar();
|
|
1870
2389
|
if (this._streaming) this._updateStreaming(true);
|
|
1871
2390
|
this._updateWaiting();
|
|
2391
|
+
this._updateFinishNote();
|
|
2392
|
+
this._updateCheckpoint();
|
|
1872
2393
|
}
|
|
1873
2394
|
/**
|
|
1874
2395
|
* Show the built-in waiting indicator while this bubble is in flight and has
|
|
@@ -2025,6 +2546,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2025
2546
|
_renderSegments() {
|
|
2026
2547
|
if (!this._segmentsEl) return;
|
|
2027
2548
|
this._drawnWithVersion = renderingVersionOf(this._cfg);
|
|
2549
|
+
this._releaseAll();
|
|
2028
2550
|
this._segmentsEl.innerHTML = "";
|
|
2029
2551
|
for (const segment of this._segments) {
|
|
2030
2552
|
const renderer = resolveSegmentRenderer(segment.type, this._cfg);
|
|
@@ -2032,10 +2554,12 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2032
2554
|
const el2 = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)), segment);
|
|
2033
2555
|
if (el2) {
|
|
2034
2556
|
this._segmentsEl.appendChild(el2);
|
|
2035
|
-
|
|
2557
|
+
this._install(segment, el2, renderer);
|
|
2036
2558
|
}
|
|
2037
2559
|
} else {
|
|
2038
|
-
|
|
2560
|
+
const el2 = unrenderedSegment(segment);
|
|
2561
|
+
this._segmentsEl.appendChild(el2);
|
|
2562
|
+
this._install(segment, el2, void 0);
|
|
2039
2563
|
}
|
|
2040
2564
|
}
|
|
2041
2565
|
if (this._contentEl) {
|
|
@@ -2097,18 +2621,18 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2097
2621
|
const customAttachment = this._cfg.getAttachmentRenderer?.();
|
|
2098
2622
|
if (customAttachment) {
|
|
2099
2623
|
this._attachmentsEl.replaceChildren();
|
|
2100
|
-
for (const
|
|
2101
|
-
const el2 = segmentRenderResultToElement(runWithConfig(this._cfg, () => customAttachment(
|
|
2624
|
+
for (const a3 of this._attachments) {
|
|
2625
|
+
const el2 = segmentRenderResultToElement(runWithConfig(this._cfg, () => customAttachment(a3)));
|
|
2102
2626
|
if (el2) this._attachmentsEl.appendChild(el2);
|
|
2103
2627
|
}
|
|
2104
2628
|
return;
|
|
2105
2629
|
}
|
|
2106
|
-
this._attachmentsEl.innerHTML = this._attachments.map((
|
|
2107
|
-
const name = escapeHtml(
|
|
2108
|
-
if (
|
|
2109
|
-
return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumbnail__image" src="${escapeHtml(
|
|
2630
|
+
this._attachmentsEl.innerHTML = this._attachments.map((a3) => {
|
|
2631
|
+
const name = escapeHtml(a3.name);
|
|
2632
|
+
if (a3.type.startsWith("image/") && this._isShowableUrl(a3.url)) {
|
|
2633
|
+
return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumbnail__image" src="${escapeHtml(a3.url)}" alt="${name}" loading="lazy" /><span class="aparte-thumb__name">${name}</span></div>`;
|
|
2110
2634
|
}
|
|
2111
|
-
return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumbnail__label">${escapeHtml(this._fileExt(
|
|
2635
|
+
return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumbnail__label">${escapeHtml(this._fileExt(a3.name))}</span><span class="aparte-thumb__name">${name}</span></div>`;
|
|
2112
2636
|
}).join("");
|
|
2113
2637
|
if (!this._cfg.getHostHandlers().attachmentPreview) return;
|
|
2114
2638
|
this._attachmentsEl.querySelectorAll(".aparte-thumb--image").forEach((tile) => {
|
|
@@ -2264,7 +2788,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2264
2788
|
/**
|
|
2265
2789
|
* What the copy action copies: the reply, not the reasoning. A `thinking` segment is
|
|
2266
2790
|
* the model's scratchpad, and the client already keeps it out of the history it sends
|
|
2267
|
-
* back (`
|
|
2791
|
+
* back (`segmentsToWireText`) for the same reason — copying it along with the answer
|
|
2268
2792
|
* pasted a paragraph of deliberation above every reply, which no assistant on the
|
|
2269
2793
|
* market does. Segments with no `content` (a tool call) contribute nothing, so a
|
|
2270
2794
|
* turn made only of those copies as `''` — and the action bar reads this to decide
|
|
@@ -2294,14 +2818,14 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2294
2818
|
const buttons = [];
|
|
2295
2819
|
if (this._role === "user") {
|
|
2296
2820
|
if (config.user) {
|
|
2297
|
-
for (const
|
|
2821
|
+
for (const a3 of config.user) buttons.push(this._actionButtonHtml(a3, icons, locale));
|
|
2298
2822
|
} else {
|
|
2299
2823
|
if (config.copy) buttons.push(this._actionButtonHtml("copy", icons, locale));
|
|
2300
2824
|
if (config.edit) buttons.push(this._actionButtonHtml("edit", icons, locale));
|
|
2301
2825
|
}
|
|
2302
2826
|
} else if (this._role === "assistant") {
|
|
2303
2827
|
if (config.assistant) {
|
|
2304
|
-
for (const
|
|
2828
|
+
for (const a3 of config.assistant) buttons.push(this._actionButtonHtml(a3, icons, locale));
|
|
2305
2829
|
} else {
|
|
2306
2830
|
if (config.copy) buttons.push(this._actionButtonHtml("copy", icons, locale));
|
|
2307
2831
|
if (config.retry) buttons.push(this._actionButtonHtml("retry", icons, locale));
|
|
@@ -2397,22 +2921,22 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2397
2921
|
/** Append the registered custom action buttons for this bubble's role. */
|
|
2398
2922
|
_appendCustomActions() {
|
|
2399
2923
|
if (!this._actionBarEl) return;
|
|
2400
|
-
for (const
|
|
2401
|
-
const roles =
|
|
2924
|
+
for (const a3 of this._cfg.getActions("bubble")) {
|
|
2925
|
+
const roles = a3.bubble?.roles ?? ["user", "assistant"];
|
|
2402
2926
|
if (!roles.includes(this._role)) continue;
|
|
2403
2927
|
const btn = document.createElement("button");
|
|
2404
2928
|
btn.type = "button";
|
|
2405
2929
|
btn.className = "aparte-btn aparte-btn--icon aparte-btn--sm aparte-action-btn aparte-action-custom";
|
|
2406
|
-
btn.dataset["action"] = `custom:${
|
|
2407
|
-
btn.setAttribute("aria-label",
|
|
2408
|
-
btn.setAttribute("title",
|
|
2409
|
-
btn.innerHTML =
|
|
2930
|
+
btn.dataset["action"] = `custom:${a3.id}`;
|
|
2931
|
+
btn.setAttribute("aria-label", a3.label);
|
|
2932
|
+
btn.setAttribute("title", a3.label);
|
|
2933
|
+
btn.innerHTML = a3.icon.startsWith("<") ? a3.icon : this._cfg.getIcon(a3.icon) ?? a3.iconFallback ?? "";
|
|
2410
2934
|
this._actionBarEl.appendChild(btn);
|
|
2411
2935
|
}
|
|
2412
2936
|
}
|
|
2413
2937
|
/** Build the `<button>` HTML for a single named action (shared by flag + per-role rendering). */
|
|
2414
|
-
_actionButtonHtml(
|
|
2415
|
-
switch (
|
|
2938
|
+
_actionButtonHtml(action2, icons, locale) {
|
|
2939
|
+
switch (action2) {
|
|
2416
2940
|
case "copy": {
|
|
2417
2941
|
const l2 = locale.copy ?? "Copy";
|
|
2418
2942
|
return `<button type="button" class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-action-btn aparte-action-copy" data-action="copy" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.copy()}</button>`;
|
|
@@ -2456,10 +2980,10 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2456
2980
|
}
|
|
2457
2981
|
_handleActionClick(e) {
|
|
2458
2982
|
const btn = e.currentTarget;
|
|
2459
|
-
const
|
|
2983
|
+
const action2 = btn.dataset["action"];
|
|
2460
2984
|
const messageId = this.getAttribute("message-id");
|
|
2461
|
-
if (
|
|
2462
|
-
const actionId =
|
|
2985
|
+
if (action2?.startsWith("custom:") && messageId) {
|
|
2986
|
+
const actionId = action2.slice("custom:".length);
|
|
2463
2987
|
const detail = {
|
|
2464
2988
|
actionId,
|
|
2465
2989
|
zone: "bubble",
|
|
@@ -2475,7 +2999,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2475
2999
|
this._cfg.getActions("bubble").find((x) => x.id === actionId)?.onClick?.(e);
|
|
2476
3000
|
return;
|
|
2477
3001
|
}
|
|
2478
|
-
switch (
|
|
3002
|
+
switch (action2) {
|
|
2479
3003
|
case "copy": {
|
|
2480
3004
|
const text = this._copyableText();
|
|
2481
3005
|
const icons = this._cfg.getIconProvider();
|
|
@@ -2524,7 +3048,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2524
3048
|
case "feedback-positive":
|
|
2525
3049
|
case "feedback-negative": {
|
|
2526
3050
|
if (!messageId) break;
|
|
2527
|
-
const value =
|
|
3051
|
+
const value = action2 === "feedback-positive" ? "positive" : "negative";
|
|
2528
3052
|
btn.setAttribute("data-submitted", "");
|
|
2529
3053
|
const detail = { messageId, value };
|
|
2530
3054
|
this.dispatchEvent(new CustomEvent("aparte-feedback", {
|
|
@@ -2652,12 +3176,12 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2652
3176
|
}
|
|
2653
3177
|
}
|
|
2654
3178
|
this._updateWaiting();
|
|
3179
|
+
this._updateFinishNote();
|
|
2655
3180
|
if (wasStreaming && !streaming) this._updateContent();
|
|
2656
|
-
if (wasStreaming && !streaming && this._segments.length > 0
|
|
2657
|
-
this._renderSegments();
|
|
2658
|
-
}
|
|
3181
|
+
if (wasStreaming && !streaming && this._segments.length > 0) this._redrawIfStale();
|
|
2659
3182
|
}
|
|
2660
3183
|
}
|
|
3184
|
+
deferHydrationOf(AparteChatBubble);
|
|
2661
3185
|
if (!customElements.get("aparte-chat-bubble")) {
|
|
2662
3186
|
customElements.define("aparte-chat-bubble", AparteChatBubble);
|
|
2663
3187
|
}
|
|
@@ -2831,6 +3355,7 @@ class AparteChatStatus extends HTMLElement {
|
|
|
2831
3355
|
container.setAttribute("aria-label", text || this._word());
|
|
2832
3356
|
}
|
|
2833
3357
|
}
|
|
3358
|
+
deferHydrationOf(AparteChatStatus);
|
|
2834
3359
|
if (!customElements.get("aparte-chat-status")) {
|
|
2835
3360
|
customElements.define("aparte-chat-status", AparteChatStatus);
|
|
2836
3361
|
}
|
|
@@ -2946,6 +3471,13 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2946
3471
|
/** Last `--aparte-transcript-inset` written on the host, in px — same write-on-change rule. */
|
|
2947
3472
|
_transcriptInset = -1;
|
|
2948
3473
|
_boundResetHandler = null;
|
|
3474
|
+
/**
|
|
3475
|
+
* Marks the bubble a compaction is writing its checkpoint onto (`data-checkpoint`). The
|
|
3476
|
+
* vanilla viewport's job: under a wrapper the transcript is the framework's, and the
|
|
3477
|
+
* host answers for it instead (`active` below).
|
|
3478
|
+
*/
|
|
3479
|
+
_checkpointState = null;
|
|
3480
|
+
_stopCheckpointState = null;
|
|
2949
3481
|
/**
|
|
2950
3482
|
* When true, _reRenderActivePath() only dispatches aparte-path-changed without
|
|
2951
3483
|
* touching the DOM. Set via setFrameworkManagedDOM(true) when a framework
|
|
@@ -3015,6 +3547,16 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3015
3547
|
this._boundResetHandler = () => this.clearAll();
|
|
3016
3548
|
window.addEventListener("aparte-reset", this._boundResetHandler);
|
|
3017
3549
|
window.addEventListener("aparte-config-change", this._onConfigChange);
|
|
3550
|
+
this._checkpointState ??= new CheckpointStateWatcher({
|
|
3551
|
+
// A bare `<aparte-chat-viewport id="…">` with no named host around it is its own
|
|
3552
|
+
// chat: the client names its events with that id (`_targetIdOf` falls back to the
|
|
3553
|
+
// element's own), so the watcher answers to it too, or every event is someone else's.
|
|
3554
|
+
ownId: () => chatIdOf(this) ?? (this.id || void 0),
|
|
3555
|
+
bubbleOf: (id) => this.querySelector(`aparte-chat-bubble[message-id="${cssEscape(id)}"]`),
|
|
3556
|
+
owns: (id) => !!this.getMessage(id),
|
|
3557
|
+
active: () => !this._frameworkManagedDOM
|
|
3558
|
+
});
|
|
3559
|
+
this._stopCheckpointState ??= this._checkpointState.start();
|
|
3018
3560
|
}
|
|
3019
3561
|
/**
|
|
3020
3562
|
* A locale switch changes the reading direction, and `dir` follows it, so a chat
|
|
@@ -3071,6 +3613,8 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3071
3613
|
window.removeEventListener("aparte-reset", this._boundResetHandler);
|
|
3072
3614
|
this._boundResetHandler = null;
|
|
3073
3615
|
}
|
|
3616
|
+
this._stopCheckpointState?.();
|
|
3617
|
+
this._stopCheckpointState = null;
|
|
3074
3618
|
this._cleanup();
|
|
3075
3619
|
}
|
|
3076
3620
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
@@ -3243,10 +3787,10 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3243
3787
|
this._notifyBubble(messageId, "addSegment", stamped);
|
|
3244
3788
|
this._autoScroll();
|
|
3245
3789
|
}
|
|
3246
|
-
updateSegment(
|
|
3790
|
+
updateSegment(a3, b2, c2) {
|
|
3247
3791
|
const clientForm = c2 === void 0 && typeof b2 === "object";
|
|
3248
|
-
const messageId = clientForm ? this._activeMessageId() :
|
|
3249
|
-
const segmentId = clientForm ?
|
|
3792
|
+
const messageId = clientForm ? this._activeMessageId() : a3;
|
|
3793
|
+
const segmentId = clientForm ? a3 : b2;
|
|
3250
3794
|
const updates = clientForm ? b2 : c2;
|
|
3251
3795
|
if (!messageId) return;
|
|
3252
3796
|
const message = this._repo.getMessageById(messageId);
|
|
@@ -3259,10 +3803,10 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3259
3803
|
this._notifyBubble(messageId, "updateSegment", { segmentId, updates: stamped });
|
|
3260
3804
|
}
|
|
3261
3805
|
}
|
|
3262
|
-
removeSegment(
|
|
3806
|
+
removeSegment(a3, b2) {
|
|
3263
3807
|
const clientForm = b2 === void 0;
|
|
3264
|
-
const messageId = clientForm ? this._activeMessageId() :
|
|
3265
|
-
const segmentId = clientForm ?
|
|
3808
|
+
const messageId = clientForm ? this._activeMessageId() : a3;
|
|
3809
|
+
const segmentId = clientForm ? a3 : b2;
|
|
3266
3810
|
if (!messageId || !segmentId) return;
|
|
3267
3811
|
const message = this._repo.getMessageById(messageId);
|
|
3268
3812
|
if (message?.segments) {
|
|
@@ -3613,6 +4157,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3613
4157
|
this._setSpacerHeight(0);
|
|
3614
4158
|
this._isAutoScrollEnabled = true;
|
|
3615
4159
|
this._updateScrollButton();
|
|
4160
|
+
this._checkpointState?.clear();
|
|
3616
4161
|
this.dispatchEvent(new CustomEvent("aparte-reset-done", { bubbles: true, composed: true }));
|
|
3617
4162
|
}
|
|
3618
4163
|
/**
|
|
@@ -3688,12 +4233,12 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3688
4233
|
this._repo.addOrUpdateMessage(this._repo.headId, message);
|
|
3689
4234
|
return message;
|
|
3690
4235
|
}
|
|
3691
|
-
_notifyBubble(messageId,
|
|
4236
|
+
_notifyBubble(messageId, action2, payload, segmentId) {
|
|
3692
4237
|
const bubble = this.querySelector(
|
|
3693
4238
|
`aparte-chat-bubble[message-id="${cssEscape(messageId)}"], [data-aparte-bubble][message-id="${cssEscape(messageId)}"]`
|
|
3694
4239
|
);
|
|
3695
4240
|
if (!bubble) return;
|
|
3696
|
-
switch (
|
|
4241
|
+
switch (action2) {
|
|
3697
4242
|
case "appendToken":
|
|
3698
4243
|
bubble.appendToken?.(payload);
|
|
3699
4244
|
break;
|
|
@@ -3716,7 +4261,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3716
4261
|
break;
|
|
3717
4262
|
case "update": {
|
|
3718
4263
|
const updates = payload;
|
|
3719
|
-
const renderable = ["status", "segments", "content", "attachments", "usage"];
|
|
4264
|
+
const renderable = ["status", "segments", "content", "attachments", "usage", "finishReason", "contextSummary", "compaction"];
|
|
3720
4265
|
if (renderable.some((key) => key in updates)) {
|
|
3721
4266
|
bubble.updateMessage?.(payload);
|
|
3722
4267
|
}
|
|
@@ -3767,6 +4312,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3767
4312
|
}
|
|
3768
4313
|
this._dispatchPathChanged(activeMessages, siblingsInfo);
|
|
3769
4314
|
this._syncBusy();
|
|
4315
|
+
this._checkpointState?.reapply();
|
|
3770
4316
|
this._recalculateSpacer();
|
|
3771
4317
|
}
|
|
3772
4318
|
_dispatchPathChanged(messages, siblings) {
|
|
@@ -4399,6 +4945,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
4399
4945
|
this._settleUntil = 0;
|
|
4400
4946
|
}
|
|
4401
4947
|
}
|
|
4948
|
+
deferHydrationOf(AparteChatViewport);
|
|
4402
4949
|
if (!customElements.get("aparte-chat-viewport")) {
|
|
4403
4950
|
customElements.define("aparte-chat-viewport", AparteChatViewport);
|
|
4404
4951
|
}
|
|
@@ -4905,6 +5452,7 @@ class AparteComposer extends HTMLElement {
|
|
|
4905
5452
|
if (this._panelActive) this._evictPanel();
|
|
4906
5453
|
}
|
|
4907
5454
|
}
|
|
5455
|
+
deferHydrationOf(AparteComposer);
|
|
4908
5456
|
if (!customElements.get("aparte-composer")) {
|
|
4909
5457
|
customElements.define("aparte-composer", AparteComposer);
|
|
4910
5458
|
}
|
|
@@ -5152,6 +5700,7 @@ class AparteComposerInput extends HTMLElement {
|
|
|
5152
5700
|
}
|
|
5153
5701
|
/** Escape a value before it lands in a double-quoted HTML attribute. */
|
|
5154
5702
|
}
|
|
5703
|
+
deferHydrationOf(AparteComposerInput);
|
|
5155
5704
|
if (!customElements.get("aparte-composer-input")) {
|
|
5156
5705
|
customElements.define("aparte-composer-input", AparteComposerInput);
|
|
5157
5706
|
}
|
|
@@ -5313,6 +5862,7 @@ class AparteComposerSend extends HTMLElement {
|
|
|
5313
5862
|
return resolveConfig(this).getIcon("stop");
|
|
5314
5863
|
}
|
|
5315
5864
|
}
|
|
5865
|
+
deferHydrationOf(AparteComposerSend);
|
|
5316
5866
|
if (!customElements.get("aparte-composer-send")) {
|
|
5317
5867
|
customElements.define("aparte-composer-send", AparteComposerSend);
|
|
5318
5868
|
}
|
|
@@ -5381,6 +5931,7 @@ class AparteComposerCancel extends HTMLElement {
|
|
|
5381
5931
|
return resolveConfig(this).getIcon("stop");
|
|
5382
5932
|
}
|
|
5383
5933
|
}
|
|
5934
|
+
deferHydrationOf(AparteComposerCancel);
|
|
5384
5935
|
if (!customElements.get("aparte-composer-cancel")) {
|
|
5385
5936
|
customElements.define("aparte-composer-cancel", AparteComposerCancel);
|
|
5386
5937
|
}
|
|
@@ -5466,6 +6017,7 @@ class AparteComposerAttachments extends HTMLElement {
|
|
|
5466
6017
|
return dot > 0 ? filename.slice(dot + 1).toUpperCase().slice(0, 4) : "FILE";
|
|
5467
6018
|
}
|
|
5468
6019
|
}
|
|
6020
|
+
deferHydrationOf(AparteComposerAttachments);
|
|
5469
6021
|
if (!customElements.get("aparte-composer-attachments")) {
|
|
5470
6022
|
customElements.define("aparte-composer-attachments", AparteComposerAttachments);
|
|
5471
6023
|
}
|
|
@@ -5592,6 +6144,7 @@ class AparteComposerAddAttachment extends HTMLElement {
|
|
|
5592
6144
|
};
|
|
5593
6145
|
}
|
|
5594
6146
|
}
|
|
6147
|
+
deferHydrationOf(AparteComposerAddAttachment);
|
|
5595
6148
|
if (!customElements.get("aparte-composer-add-attachment")) {
|
|
5596
6149
|
customElements.define("aparte-composer-add-attachment", AparteComposerAddAttachment);
|
|
5597
6150
|
}
|
|
@@ -5674,6 +6227,7 @@ class AparteComposerAction extends HTMLElement {
|
|
|
5674
6227
|
return resolveConfig(this).getIcon(icon) ?? icon;
|
|
5675
6228
|
}
|
|
5676
6229
|
}
|
|
6230
|
+
deferHydrationOf(AparteComposerAction);
|
|
5677
6231
|
if (!customElements.get("aparte-composer-action")) {
|
|
5678
6232
|
customElements.define("aparte-composer-action", AparteComposerAction);
|
|
5679
6233
|
}
|
|
@@ -5712,6 +6266,7 @@ class AparteComposerToolbar extends HTMLElement {
|
|
|
5712
6266
|
else this.setAttribute("data-empty", "");
|
|
5713
6267
|
}
|
|
5714
6268
|
}
|
|
6269
|
+
deferHydrationOf(AparteComposerToolbar);
|
|
5715
6270
|
if (!customElements.get("aparte-composer-toolbar")) {
|
|
5716
6271
|
customElements.define("aparte-composer-toolbar", AparteComposerToolbar);
|
|
5717
6272
|
}
|
|
@@ -6053,7 +6608,7 @@ class AparteConversationList extends HTMLElement {
|
|
|
6053
6608
|
}
|
|
6054
6609
|
}
|
|
6055
6610
|
const fixed = ["pinned", "today", "yesterday", "week", "month"].map((k2) => buckets.get(k2)).filter((g2) => !!g2);
|
|
6056
|
-
const months = [...buckets.keys()].filter((k2) => k2.startsWith("m-")).sort((
|
|
6611
|
+
const months = [...buckets.keys()].filter((k2) => k2.startsWith("m-")).sort((a3, b2) => b2.localeCompare(a3)).map((k2) => buckets.get(k2));
|
|
6057
6612
|
const undated = buckets.get("undated");
|
|
6058
6613
|
return [...fixed, ...months, ...undated ? [undated] : []];
|
|
6059
6614
|
}
|
|
@@ -6251,7 +6806,7 @@ class AparteConversationList extends HTMLElement {
|
|
|
6251
6806
|
console.warn("[aparte-conversation-list] the conversation manager refused the change:", err);
|
|
6252
6807
|
});
|
|
6253
6808
|
}
|
|
6254
|
-
_onMenuAction(
|
|
6809
|
+
_onMenuAction(action2) {
|
|
6255
6810
|
const open = this._open;
|
|
6256
6811
|
if (!open) return;
|
|
6257
6812
|
const conv = this._conversations.find((c2) => c2.id === open.id);
|
|
@@ -6260,7 +6815,7 @@ class AparteConversationList extends HTMLElement {
|
|
|
6260
6815
|
return;
|
|
6261
6816
|
}
|
|
6262
6817
|
const id = conv.id;
|
|
6263
|
-
switch (
|
|
6818
|
+
switch (action2) {
|
|
6264
6819
|
case "rename":
|
|
6265
6820
|
this._closeMenu();
|
|
6266
6821
|
this._startRename(open.row, conv);
|
|
@@ -6472,6 +7027,7 @@ class AparteConversationList extends HTMLElement {
|
|
|
6472
7027
|
});
|
|
6473
7028
|
}
|
|
6474
7029
|
}
|
|
7030
|
+
deferHydrationOf(AparteConversationList);
|
|
6475
7031
|
if (!customElements.get("aparte-conversation-list")) customElements.define("aparte-conversation-list", AparteConversationList);
|
|
6476
7032
|
const isSuggestion = (value) => typeof value === "string" || typeof value === "object" && value !== null && typeof value.label === "string";
|
|
6477
7033
|
const parseSuggestions = (raw) => {
|
|
@@ -6699,6 +7255,7 @@ class AparteSuggestions extends HTMLElement {
|
|
|
6699
7255
|
}
|
|
6700
7256
|
}
|
|
6701
7257
|
}
|
|
7258
|
+
deferHydrationOf(AparteSuggestions);
|
|
6702
7259
|
if (!customElements.get("aparte-suggestions")) {
|
|
6703
7260
|
customElements.define("aparte-suggestions", AparteSuggestions);
|
|
6704
7261
|
}
|
|
@@ -6706,9 +7263,14 @@ const fraction = (raw, fallback) => {
|
|
|
6706
7263
|
const n2 = Number(raw);
|
|
6707
7264
|
return raw !== null && Number.isFinite(n2) && n2 > 0 && n2 <= 1 ? n2 : fallback;
|
|
6708
7265
|
};
|
|
6709
|
-
const
|
|
7266
|
+
const hostIdOf = (target) => {
|
|
7267
|
+
const node = target instanceof Element ? target.closest("aparte-chat, [data-aparte-chat]") : null;
|
|
7268
|
+
return node?.id || void 0;
|
|
7269
|
+
};
|
|
7270
|
+
const el = (tag, className, text) => {
|
|
6710
7271
|
const node = document.createElement(tag);
|
|
6711
7272
|
node.className = className;
|
|
7273
|
+
if (text !== void 0) node.textContent = text;
|
|
6712
7274
|
return node;
|
|
6713
7275
|
};
|
|
6714
7276
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
@@ -6727,33 +7289,129 @@ const ringSvg = () => {
|
|
|
6727
7289
|
}
|
|
6728
7290
|
return svg;
|
|
6729
7291
|
};
|
|
7292
|
+
const readsMessages = (candidate) => !!candidate && typeof candidate.getMessages === "function";
|
|
7293
|
+
const transcriptOf = (chat) => {
|
|
7294
|
+
if (!chat) return null;
|
|
7295
|
+
if (readsMessages(chat)) return chat;
|
|
7296
|
+
const viewport = chat.viewport ?? chat.querySelector("aparte-chat-viewport");
|
|
7297
|
+
return readsMessages(viewport) ? viewport : null;
|
|
7298
|
+
};
|
|
7299
|
+
let panelSeq = 0;
|
|
7300
|
+
const NO_PARTS = { system: 0, tools: 0, summary: 0, messages: 0 };
|
|
6730
7301
|
class AparteContext extends HTMLElement {
|
|
6731
7302
|
static get observedAttributes() {
|
|
6732
7303
|
return ["window", "warn", "danger", "target", "variant", "used"];
|
|
6733
7304
|
}
|
|
7305
|
+
/**
|
|
7306
|
+
* A reading with no transcript behind it: the `used` attribute's seed, or a turn's usage
|
|
7307
|
+
* when the chat's list cannot be read from here (a gauge placed far from its chat, a test).
|
|
7308
|
+
* When the list can be read, the measure comes from it and this is only the fallback.
|
|
7309
|
+
*/
|
|
6734
7310
|
_used = null;
|
|
6735
7311
|
_level = null;
|
|
6736
7312
|
_compactRequested = false;
|
|
7313
|
+
/** The last turn's usage and the reply it belongs to, so the measure has it before the list has written it back. */
|
|
7314
|
+
_lastDone = null;
|
|
7315
|
+
/** What the model reads changed with nothing on the path to say so (a Restore): the reading is an estimate until the next turn reports. */
|
|
7316
|
+
_viewChanged = false;
|
|
7317
|
+
/** A conversation was replaced and nothing has reported or loaded since: the list on screen is not read yet. */
|
|
7318
|
+
_blank = false;
|
|
7319
|
+
/** The model refused a request as over its window; the level is `danger` until something changes. */
|
|
7320
|
+
_overflow = false;
|
|
7321
|
+
_compact = null;
|
|
7322
|
+
_failure = "";
|
|
7323
|
+
_open = false;
|
|
7324
|
+
/** The reading drawn last, so the `used` getter and the panel read what is on screen. */
|
|
7325
|
+
_measured = null;
|
|
7326
|
+
/** The chat an event last came from, for a gauge that is neither under its chat nor names it with `target`. */
|
|
7327
|
+
_seen = null;
|
|
7328
|
+
/** What the panel was last built from: a render that changes nothing leaves its nodes, and the focus in them, alone. */
|
|
7329
|
+
_panelSignature = "";
|
|
7330
|
+
_remember(target) {
|
|
7331
|
+
const chat = target instanceof Element ? chatBoundaryOf(target) : null;
|
|
7332
|
+
if (chat) this._seen = chat;
|
|
7333
|
+
}
|
|
6737
7334
|
_onDone = (e) => {
|
|
6738
7335
|
const detail = e.detail;
|
|
6739
7336
|
if (!detail?.usage || !this._isMine(detail.targetId)) return;
|
|
6740
|
-
|
|
6741
|
-
this._used =
|
|
7337
|
+
this._remember(e.target);
|
|
7338
|
+
this._used = usageTokens(detail.usage);
|
|
7339
|
+
this._lastDone = { messageId: detail.messageId, usage: detail.usage };
|
|
7340
|
+
this._viewChanged = false;
|
|
7341
|
+
this._blank = false;
|
|
7342
|
+
this._overflow = false;
|
|
7343
|
+
this._render(true);
|
|
7344
|
+
};
|
|
7345
|
+
/**
|
|
7346
|
+
* A conversation is replaced — a new chat, or one loaded in place of the last — and the
|
|
7347
|
+
* previous reading is not this one's. The viewport says so with `aparte-reset-done` (no
|
|
7348
|
+
* detail: the chat is found from where the event came from). A load clears first and
|
|
7349
|
+
* `aparte-conversation-loaded` follows, so the reading is dropped here and read again
|
|
7350
|
+
* there. Until then the chat's list is not trusted: a framework's may not have caught up.
|
|
7351
|
+
*/
|
|
7352
|
+
_onReset = (e) => {
|
|
7353
|
+
if (!this._isMine(hostIdOf(e.target))) return;
|
|
7354
|
+
this._remember(e.target);
|
|
7355
|
+
this._used = null;
|
|
7356
|
+
this._lastDone = null;
|
|
7357
|
+
this._viewChanged = false;
|
|
7358
|
+
this._overflow = false;
|
|
7359
|
+
this._compact = null;
|
|
7360
|
+
this._blank = true;
|
|
6742
7361
|
this._render();
|
|
6743
7362
|
};
|
|
6744
7363
|
/**
|
|
6745
|
-
*
|
|
6746
|
-
*
|
|
6747
|
-
*
|
|
6748
|
-
*
|
|
6749
|
-
|
|
7364
|
+
* The messages of a stored conversation are on screen: the reading is the last reply's
|
|
7365
|
+
* usage — what a turn would have reported had it just ended. A transcript with none
|
|
7366
|
+
* (an empty one, a conversation saved before usage was kept) shows nothing, the same as
|
|
7367
|
+
* a chat that has not spoken yet.
|
|
7368
|
+
*/
|
|
7369
|
+
_onLoaded = (e) => {
|
|
7370
|
+
const detail = e.detail;
|
|
7371
|
+
if (!this._isMine(detail?.targetId)) return;
|
|
7372
|
+
this._remember(e.target);
|
|
7373
|
+
this._blank = false;
|
|
7374
|
+
this._lastDone = null;
|
|
7375
|
+
this._viewChanged = false;
|
|
7376
|
+
this._overflow = false;
|
|
7377
|
+
this._compact = null;
|
|
7378
|
+
const host = e.target;
|
|
7379
|
+
const messages = host?.getMessages?.() ?? [];
|
|
7380
|
+
const last = [...messages].reverse().find((m2) => m2.role === "assistant" && m2.usage);
|
|
7381
|
+
this._used = last?.usage ? usageTokens(last.usage) : null;
|
|
7382
|
+
this._render();
|
|
7383
|
+
};
|
|
7384
|
+
/** A compaction began for this chat: say so on the element and in the panel. */
|
|
7385
|
+
_onCompactStart = (e) => {
|
|
7386
|
+
const detail = e.detail;
|
|
7387
|
+
if (!this._isMine(detail?.targetId)) return;
|
|
7388
|
+
this._compact = "running";
|
|
7389
|
+
this._failure = "";
|
|
7390
|
+
this._render();
|
|
7391
|
+
};
|
|
7392
|
+
/**
|
|
7393
|
+
* After a real compaction — or a Restore — what the model reads is a different size by an
|
|
7394
|
+
* amount nobody has measured yet, and the last reply's reported usage describes the request
|
|
7395
|
+
* from before. The gauge measures the request that would be sent now and says so with `~`,
|
|
7396
|
+
* rather than keeping a number it knows to be wrong or going blank until the next turn. The
|
|
7397
|
+
* event names the chat it compacted, so on a multi-chat page only that chat's gauge
|
|
7398
|
+
* moves; an unnamed one moves every gauge.
|
|
7399
|
+
*
|
|
7400
|
+
* Without a transcript to measure (a gauge that cannot read its chat's list) there is
|
|
7401
|
+
* nothing to estimate from, and the reading is dropped as it always was.
|
|
6750
7402
|
*/
|
|
6751
7403
|
_onCompacted = (e) => {
|
|
6752
7404
|
const detail = e.detail;
|
|
6753
7405
|
if (!this._isMine(detail?.targetId)) return;
|
|
6754
7406
|
this._compactRequested = false;
|
|
6755
|
-
|
|
6756
|
-
|
|
7407
|
+
this._compact = null;
|
|
7408
|
+
if (detail?.skipped) {
|
|
7409
|
+
this._render();
|
|
7410
|
+
return;
|
|
7411
|
+
}
|
|
7412
|
+
this._viewChanged = true;
|
|
7413
|
+
this._overflow = false;
|
|
7414
|
+
if (!this._messages()) this._used = null;
|
|
6757
7415
|
this._render();
|
|
6758
7416
|
};
|
|
6759
7417
|
/** A failed compaction spends the request too; the next `danger` asks again. */
|
|
@@ -6761,11 +7419,23 @@ class AparteContext extends HTMLElement {
|
|
|
6761
7419
|
const detail = e.detail;
|
|
6762
7420
|
if (!this._isMine(detail?.targetId)) return;
|
|
6763
7421
|
this._compactRequested = false;
|
|
7422
|
+
this._compact = detail?.aborted ? null : "failed";
|
|
7423
|
+
this._failure = detail?.aborted ? "" : typeof detail?.error === "string" ? detail.error : "";
|
|
7424
|
+
this._render();
|
|
7425
|
+
};
|
|
7426
|
+
/** The model refused this chat's request as over its window: the number was wrong, and so is "ok". */
|
|
7427
|
+
_onMessageError = (e) => {
|
|
7428
|
+
const detail = e.detail;
|
|
7429
|
+
if (!this._isMine(detail?.targetId)) return;
|
|
7430
|
+
if (detail?.error?.code !== AparteErrorCode.USAGE_CONTEXT_EXCEEDED) return;
|
|
7431
|
+
this._remember(e.target);
|
|
7432
|
+
this._overflow = true;
|
|
7433
|
+
this._render();
|
|
6764
7434
|
};
|
|
6765
7435
|
_onRerender = () => this._render();
|
|
6766
|
-
/** Tokens
|
|
7436
|
+
/** Tokens in use — what is drawn — else the last reported or seeded reading, else `null`. */
|
|
6767
7437
|
get used() {
|
|
6768
|
-
return this._used;
|
|
7438
|
+
return this._measured?.used ?? this._used;
|
|
6769
7439
|
}
|
|
6770
7440
|
/**
|
|
6771
7441
|
* Writes the `used` attribute: a reading for a conversation restored without a turn of
|
|
@@ -6794,18 +7464,33 @@ class AparteContext extends HTMLElement {
|
|
|
6794
7464
|
get level() {
|
|
6795
7465
|
return this._level;
|
|
6796
7466
|
}
|
|
7467
|
+
/** The reading drawn now, parts and all, or `null` while nothing is shown. */
|
|
7468
|
+
get measure() {
|
|
7469
|
+
return this._measured;
|
|
7470
|
+
}
|
|
6797
7471
|
connectedCallback() {
|
|
6798
7472
|
window.addEventListener("aparte-message-done", this._onDone);
|
|
7473
|
+
window.addEventListener("aparte-message-error", this._onMessageError);
|
|
7474
|
+
window.addEventListener("aparte-compact-start", this._onCompactStart);
|
|
6799
7475
|
window.addEventListener("aparte-compact-done", this._onCompacted);
|
|
6800
7476
|
window.addEventListener("aparte-compact-error", this._onCompactError);
|
|
7477
|
+
window.addEventListener("aparte-reset-done", this._onReset);
|
|
7478
|
+
window.addEventListener("aparte-conversation-loaded", this._onLoaded);
|
|
6801
7479
|
window.addEventListener("aparte-config-change", this._onRerender);
|
|
7480
|
+
this.addEventListener("click", this._onClick);
|
|
6802
7481
|
this._render();
|
|
6803
7482
|
}
|
|
6804
7483
|
disconnectedCallback() {
|
|
6805
7484
|
window.removeEventListener("aparte-message-done", this._onDone);
|
|
7485
|
+
window.removeEventListener("aparte-message-error", this._onMessageError);
|
|
7486
|
+
window.removeEventListener("aparte-compact-start", this._onCompactStart);
|
|
6806
7487
|
window.removeEventListener("aparte-compact-done", this._onCompacted);
|
|
6807
7488
|
window.removeEventListener("aparte-compact-error", this._onCompactError);
|
|
7489
|
+
window.removeEventListener("aparte-reset-done", this._onReset);
|
|
7490
|
+
window.removeEventListener("aparte-conversation-loaded", this._onLoaded);
|
|
6808
7491
|
window.removeEventListener("aparte-config-change", this._onRerender);
|
|
7492
|
+
this.removeEventListener("click", this._onClick);
|
|
7493
|
+
this._stopListeningWhileOpen();
|
|
6809
7494
|
}
|
|
6810
7495
|
attributeChangedCallback(name) {
|
|
6811
7496
|
if (name === "used") {
|
|
@@ -6814,10 +7499,21 @@ class AparteContext extends HTMLElement {
|
|
|
6814
7499
|
}
|
|
6815
7500
|
if (this.isConnected) this._render();
|
|
6816
7501
|
}
|
|
7502
|
+
/**
|
|
7503
|
+
* The config the chat this gauge watches answers to, not the one at the gauge's own
|
|
7504
|
+
* position. A gauge in a header, with `target=`, stands outside the chat's boundary, and
|
|
7505
|
+
* a chat that carries its own config (a wrapper's `config` prop) has its own prompt, tools,
|
|
7506
|
+
* window and compactor: the client and the plugin read them from there, so the gauge must
|
|
7507
|
+
* too, or it measures a different request and offers no Compact now. Resolved live, like
|
|
7508
|
+
* every config read: the boundary may be attached after this element connects.
|
|
7509
|
+
*/
|
|
7510
|
+
_cfg() {
|
|
7511
|
+
return resolveConfig(this._chat() ?? this);
|
|
7512
|
+
}
|
|
6817
7513
|
_window() {
|
|
6818
7514
|
const attr = Number(this.getAttribute("window"));
|
|
6819
7515
|
if (this.hasAttribute("window") && Number.isFinite(attr) && attr > 0) return attr;
|
|
6820
|
-
const model =
|
|
7516
|
+
const model = this._cfg().getCurrentModel();
|
|
6821
7517
|
return model?.contextWindow && model.contextWindow > 0 ? model.contextWindow : null;
|
|
6822
7518
|
}
|
|
6823
7519
|
/** The chat this gauge watches: `target`, else the chat host above it — the composer's rule. */
|
|
@@ -6836,43 +7532,113 @@ class AparteContext extends HTMLElement {
|
|
|
6836
7532
|
const own = this._ownTargetId();
|
|
6837
7533
|
return !targetId || !own || targetId === own;
|
|
6838
7534
|
}
|
|
6839
|
-
|
|
7535
|
+
/**
|
|
7536
|
+
* The chat's element: the one `target` names, else the chat above the gauge, else the one
|
|
7537
|
+
* its events last came from. Passed to the compactor as it is — a chat inside a shadow
|
|
7538
|
+
* root is not found by id, so the element is the address that always works.
|
|
7539
|
+
*/
|
|
7540
|
+
_chat() {
|
|
7541
|
+
const attr = this.getAttribute("target");
|
|
7542
|
+
if (attr) {
|
|
7543
|
+
const root = this.getRootNode();
|
|
7544
|
+
const named = root.getElementById?.(attr) ?? document.getElementById(attr);
|
|
7545
|
+
if (named) return named;
|
|
7546
|
+
}
|
|
7547
|
+
const above = chatBoundaryOf(this);
|
|
7548
|
+
if (above) return above;
|
|
7549
|
+
return this._seen?.isConnected ? this._seen : null;
|
|
7550
|
+
}
|
|
7551
|
+
/** The chat's active path, or `null` when it cannot be read from here. */
|
|
7552
|
+
_messages() {
|
|
7553
|
+
const reader = transcriptOf(this._chat());
|
|
7554
|
+
if (!reader) return null;
|
|
7555
|
+
try {
|
|
7556
|
+
return reader.getMessages();
|
|
7557
|
+
} catch {
|
|
7558
|
+
return null;
|
|
7559
|
+
}
|
|
7560
|
+
}
|
|
7561
|
+
/**
|
|
7562
|
+
* What to draw: the measure of the chat's own transcript when it holds a reply with usage
|
|
7563
|
+
* (or the model has just refused a request, and an estimate is better than nothing), else
|
|
7564
|
+
* the seeded or reported number with nothing to break down.
|
|
7565
|
+
*
|
|
7566
|
+
* The last turn's usage is laid over its reply when the list has not written it back yet.
|
|
7567
|
+
* If the list does not hold that reply at all it is not used: the list is the path on
|
|
7568
|
+
* screen, and a reply that is not on it (a branch was switched) is not its reading.
|
|
7569
|
+
*/
|
|
7570
|
+
_measure(capacity) {
|
|
7571
|
+
const stored = this._messages();
|
|
7572
|
+
const done = this._lastDone;
|
|
7573
|
+
const messages = stored && done?.messageId ? stored.map((m2) => m2.id === done.messageId && !m2.usage ? { ...m2, usage: done.usage } : m2) : stored;
|
|
7574
|
+
const hasUsage = !!messages?.some((m2) => m2.role === "assistant" && m2.usage);
|
|
7575
|
+
if (messages && (hasUsage && !this._blank || this._overflow && messages.length > 0)) {
|
|
7576
|
+
return measureContext({ messages, config: this._cfg(), windowOverride: capacity, viewChanged: this._viewChanged });
|
|
7577
|
+
}
|
|
7578
|
+
if (this._used === null) return null;
|
|
7579
|
+
return { window: capacity, used: this._used, estimated: false, parts: { ...NO_PARTS }, other: this._used };
|
|
7580
|
+
}
|
|
7581
|
+
_render(askToCompact = false) {
|
|
6840
7582
|
const capacity = this._window();
|
|
6841
|
-
|
|
7583
|
+
const measure = capacity === null ? null : this._measure(capacity);
|
|
7584
|
+
if (this._compact) this.setAttribute("data-compact", this._compact);
|
|
7585
|
+
else this.removeAttribute("data-compact");
|
|
7586
|
+
if (capacity === null || measure === null) {
|
|
7587
|
+
this._measured = null;
|
|
6842
7588
|
this.setAttribute("data-empty", "");
|
|
6843
7589
|
this.removeAttribute("data-level");
|
|
7590
|
+
this._open = false;
|
|
7591
|
+
this._stopListeningWhileOpen();
|
|
7592
|
+
this._panelSignature = "";
|
|
6844
7593
|
this.replaceChildren();
|
|
6845
7594
|
this._level = null;
|
|
6846
7595
|
return;
|
|
6847
7596
|
}
|
|
7597
|
+
this._measured = measure;
|
|
6848
7598
|
this.removeAttribute("data-empty");
|
|
6849
|
-
const used =
|
|
7599
|
+
const used = measure.used;
|
|
6850
7600
|
const ratio = Math.min(1, used / capacity);
|
|
6851
7601
|
const warn = fraction(this.getAttribute("warn"), 0.75);
|
|
6852
7602
|
const danger = fraction(this.getAttribute("danger"), 0.9);
|
|
6853
|
-
const level = ratio >= danger ? "danger" : ratio >= warn ? "warn" : "ok";
|
|
7603
|
+
const level = this._overflow || ratio >= danger ? "danger" : ratio >= warn ? "warn" : "ok";
|
|
6854
7604
|
this.setAttribute("data-level", level);
|
|
6855
7605
|
const ring = this.getAttribute("variant") === "ring";
|
|
6856
7606
|
let root = this.querySelector(".aparte-context");
|
|
6857
7607
|
if (root && root.classList.contains("aparte-context--ring") !== ring) {
|
|
6858
7608
|
this.replaceChildren();
|
|
6859
7609
|
root = null;
|
|
7610
|
+
this._panelSignature = "";
|
|
6860
7611
|
}
|
|
6861
7612
|
let meter = this.querySelector('[role="meter"]');
|
|
6862
7613
|
let text = this.querySelector(".aparte-context__text");
|
|
6863
|
-
|
|
7614
|
+
let toggle = this.querySelector(".aparte-context__toggle");
|
|
7615
|
+
let name = this.querySelector(".aparte-context__name");
|
|
7616
|
+
let panel = this.querySelector(".aparte-context__panel");
|
|
7617
|
+
if (!root || !meter || !text || !toggle || !name || !panel) {
|
|
6864
7618
|
root = el("div", ring ? "aparte-context aparte-context--ring" : "aparte-context");
|
|
6865
7619
|
meter = ring ? ringSvg() : el("div", "aparte-progress");
|
|
6866
7620
|
meter.setAttribute("role", "meter");
|
|
6867
7621
|
if (!ring) meter.appendChild(el("div", "aparte-progress__bar"));
|
|
7622
|
+
toggle = el("button", "aparte-btn aparte-btn--ghost aparte-btn--sm aparte-context__toggle");
|
|
7623
|
+
toggle.type = "button";
|
|
7624
|
+
name = el("span", "aparte-sr-only");
|
|
7625
|
+
name.classList.add("aparte-context__name");
|
|
6868
7626
|
text = el("span", "aparte-context__text");
|
|
6869
|
-
|
|
7627
|
+
toggle.append(name, text);
|
|
7628
|
+
panel = el("div", "aparte-context__panel aparte-popover");
|
|
7629
|
+
panel.id = `aparte-context-panel-${++panelSeq}`;
|
|
7630
|
+
panel.setAttribute("role", "group");
|
|
7631
|
+
panel.hidden = true;
|
|
7632
|
+
toggle.setAttribute("aria-controls", panel.id);
|
|
7633
|
+
root.append(meter, toggle, panel);
|
|
6870
7634
|
this.replaceChildren(root);
|
|
7635
|
+
this._panelSignature = "";
|
|
6871
7636
|
}
|
|
6872
|
-
const cfg =
|
|
7637
|
+
const cfg = this._cfg();
|
|
6873
7638
|
const tag = cfg.getLocale().tag || void 0;
|
|
6874
7639
|
const format = new Intl.NumberFormat(tag, { notation: "compact", maximumFractionDigits: 1 });
|
|
6875
|
-
const
|
|
7640
|
+
const approx = measure.estimated ? "~" : "";
|
|
7641
|
+
const reading = `${approx}${format.format(used)} / ${format.format(capacity)}`;
|
|
6876
7642
|
const percent = Math.round(ratio * 100);
|
|
6877
7643
|
const label = cfg.t("contextLabel") || "Context window";
|
|
6878
7644
|
if (ring) {
|
|
@@ -6887,7 +7653,13 @@ class AparteContext extends HTMLElement {
|
|
|
6887
7653
|
meter.setAttribute("aria-valuemax", String(capacity));
|
|
6888
7654
|
meter.setAttribute("aria-valuenow", String(used));
|
|
6889
7655
|
meter.setAttribute("aria-label", `${label}: ${reading}`);
|
|
6890
|
-
|
|
7656
|
+
name.textContent = `${label}: `;
|
|
7657
|
+
toggle.setAttribute("aria-expanded", String(this._open));
|
|
7658
|
+
text.textContent = ring ? `${approx}${new Intl.NumberFormat(tag, { style: "percent", maximumFractionDigits: 0 }).format(percent / 100)}` : reading;
|
|
7659
|
+
panel.setAttribute("aria-label", label);
|
|
7660
|
+
panel.hidden = !this._open;
|
|
7661
|
+
if (this._open) this._renderPanel(panel, measure, capacity, format);
|
|
7662
|
+
else this._panelSignature = "";
|
|
6891
7663
|
if (level !== this._level) {
|
|
6892
7664
|
this._level = level;
|
|
6893
7665
|
const targetId = this._ownTargetId();
|
|
@@ -6895,12 +7667,191 @@ class AparteContext extends HTMLElement {
|
|
|
6895
7667
|
this.dispatchEvent(new CustomEvent("aparte-context-threshold", { detail, bubbles: true, composed: true }));
|
|
6896
7668
|
if (level !== "danger") this._compactRequested = false;
|
|
6897
7669
|
}
|
|
6898
|
-
if (level === "danger" && this.hasAttribute("auto-compact") && !this._compactRequested) {
|
|
7670
|
+
if (askToCompact && level === "danger" && this.hasAttribute("auto-compact") && !this._compactRequested) {
|
|
6899
7671
|
this._compactRequested = true;
|
|
6900
|
-
window.dispatchEvent(new CustomEvent("aparte-compact", {
|
|
7672
|
+
window.dispatchEvent(new CustomEvent("aparte-compact", {
|
|
7673
|
+
detail: { targetId: this._ownTargetId(), auto: true, window: capacity }
|
|
7674
|
+
}));
|
|
7675
|
+
}
|
|
7676
|
+
}
|
|
7677
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
7678
|
+
// The panel
|
|
7679
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
7680
|
+
/**
|
|
7681
|
+
* What the window holds, row by row, and what can be done about it.
|
|
7682
|
+
*
|
|
7683
|
+
* The parts are raw estimates and `used` is the number the gauge draws — measured or
|
|
7684
|
+
* calibrated — so they need not agree. The panel never shows a sum above the total: when
|
|
7685
|
+
* the estimates add up to more than `used`, each is scaled down to fit and `other` is
|
|
7686
|
+
* zero; otherwise the difference is `other`, the part the heuristic cannot see.
|
|
7687
|
+
*/
|
|
7688
|
+
_renderPanel(panel, measure, capacity, format) {
|
|
7689
|
+
const cfg = this._cfg();
|
|
7690
|
+
const compactor = cfg.getCompactor();
|
|
7691
|
+
const chat = this._chat();
|
|
7692
|
+
const targetId = this._ownTargetId();
|
|
7693
|
+
let preview = null;
|
|
7694
|
+
try {
|
|
7695
|
+
preview = compactor?.preview?.({ target: chat, ...targetId ? { targetId } : {} }) ?? null;
|
|
7696
|
+
} catch {
|
|
7697
|
+
preview = null;
|
|
7698
|
+
}
|
|
7699
|
+
const { parts, used } = measure;
|
|
7700
|
+
const sum = parts.system + parts.tools + parts.summary + parts.messages;
|
|
7701
|
+
const scale = sum > used && sum > 0 ? used / sum : 1;
|
|
7702
|
+
const approxOf = measure.estimated ? "~" : "";
|
|
7703
|
+
const rows = [
|
|
7704
|
+
{ key: "system", label: cfg.t("contextSystem") || "System prompt", tokens: parts.system * scale, approx: "~" },
|
|
7705
|
+
{ key: "tools", label: cfg.t("contextTools") || "Tools", tokens: parts.tools * scale, approx: "~" },
|
|
7706
|
+
{ key: "summary", label: cfg.t("contextSummary") || "Summary", tokens: parts.summary * scale, approx: "~" },
|
|
7707
|
+
{ key: "messages", label: cfg.t("contextMessages") || "Messages", tokens: parts.messages * scale, approx: "~" },
|
|
7708
|
+
{ key: "other", label: cfg.t("contextOther") || "Other", tokens: scale < 1 ? 0 : measure.other, approx: "~" },
|
|
7709
|
+
{ key: "free", label: cfg.t("contextFree") || "Free", tokens: Math.max(0, capacity - used), approx: approxOf }
|
|
7710
|
+
];
|
|
7711
|
+
const total = `${approxOf}${format.format(used)} / ${format.format(capacity)}`;
|
|
7712
|
+
const reclaim = preview && preview.tokens > 0 ? (cfg.t("contextReclaim") || "Compacting now would free ~{n} tokens").replace("{n}", () => format.format(Math.round(preview.tokens))) : "";
|
|
7713
|
+
const canCompact = !!compactor && !!preview && this._compact === null;
|
|
7714
|
+
const list = el("ul", "aparte-context__rows");
|
|
7715
|
+
for (const row of rows) {
|
|
7716
|
+
const item = el("li", "aparte-context__row");
|
|
7717
|
+
item.setAttribute("data-part", row.key);
|
|
7718
|
+
item.append(el("span", "aparte-context__row-label", row.label));
|
|
7719
|
+
item.append(el("span", "aparte-context__row-value", `${row.approx}${format.format(Math.round(row.tokens))}`));
|
|
7720
|
+
const bar = el("div", "aparte-progress aparte-context__row-bar");
|
|
7721
|
+
bar.setAttribute("aria-hidden", "true");
|
|
7722
|
+
bar.style.setProperty("--aparte-progress-value", String(Math.round(Math.min(1, row.tokens / capacity) * 100)));
|
|
7723
|
+
bar.appendChild(el("div", "aparte-progress__bar"));
|
|
7724
|
+
item.append(bar);
|
|
7725
|
+
list.append(item);
|
|
7726
|
+
}
|
|
7727
|
+
const totalRow = el("li", "aparte-context__row aparte-context__row--total");
|
|
7728
|
+
totalRow.setAttribute("data-part", "total");
|
|
7729
|
+
totalRow.append(el("span", "aparte-context__row-label", cfg.t("contextTotal") || "Total"), el("span", "aparte-context__row-value", total));
|
|
7730
|
+
list.append(totalRow);
|
|
7731
|
+
const nodes = [list];
|
|
7732
|
+
if (this._compact === "running") {
|
|
7733
|
+
const status = el("p", "aparte-context__status aparte-shimmer", cfg.t("checkpointPending") || "Summarising the context…");
|
|
7734
|
+
status.setAttribute("role", "status");
|
|
7735
|
+
nodes.push(status);
|
|
7736
|
+
} else if (this._compact === "failed") {
|
|
7737
|
+
const status = el("p", "aparte-context__status aparte-context__status--failed", cfg.t("checkpointFailed") || "Couldn't summarise the context");
|
|
7738
|
+
status.setAttribute("role", "status");
|
|
7739
|
+
if (this._failure) {
|
|
7740
|
+
status.title = this._failure;
|
|
7741
|
+
status.appendChild(el("span", "aparte-sr-only", `: ${this._failure}`));
|
|
7742
|
+
}
|
|
7743
|
+
nodes.push(status, this._action("retry", cfg.t("retry") || "Retry"));
|
|
7744
|
+
}
|
|
7745
|
+
if (reclaim) nodes.push(el("p", "aparte-context__reclaim", reclaim));
|
|
7746
|
+
if (canCompact) nodes.push(this._action("compact", cfg.t("contextCompactNow") || "Compact now"));
|
|
7747
|
+
const next = el("div", "aparte-context__content");
|
|
7748
|
+
next.append(...nodes);
|
|
7749
|
+
if (next.innerHTML === this._panelSignature) return;
|
|
7750
|
+
this._panelSignature = next.innerHTML;
|
|
7751
|
+
const active = document.activeElement;
|
|
7752
|
+
const hadFocus = active && panel.contains(active) ? active.getAttribute("data-context-action") ?? "" : null;
|
|
7753
|
+
panel.replaceChildren(...Array.from(next.childNodes));
|
|
7754
|
+
if (hadFocus !== null) {
|
|
7755
|
+
const same = hadFocus ? panel.querySelector(`[data-context-action="${cssEscape(hadFocus)}"]`) : null;
|
|
7756
|
+
(same ?? this.querySelector(".aparte-context__toggle"))?.focus();
|
|
7757
|
+
}
|
|
7758
|
+
}
|
|
7759
|
+
_action(name, label) {
|
|
7760
|
+
const button = el("button", "aparte-btn aparte-btn--sm aparte-btn--surface aparte-context__action", label);
|
|
7761
|
+
button.type = "button";
|
|
7762
|
+
button.setAttribute("data-context-action", name);
|
|
7763
|
+
return button;
|
|
7764
|
+
}
|
|
7765
|
+
/**
|
|
7766
|
+
* Ask the registered compactor to compact this chat now, as a person asked. With none
|
|
7767
|
+
* registered (Retry only — Compact now is not offered without one) the command event
|
|
7768
|
+
* is the way to ask, the one a host's own button uses.
|
|
7769
|
+
*/
|
|
7770
|
+
async _compactNow(reason) {
|
|
7771
|
+
const compactor = this._cfg().getCompactor();
|
|
7772
|
+
const targetId = this._ownTargetId();
|
|
7773
|
+
if (!compactor) {
|
|
7774
|
+
window.dispatchEvent(new CustomEvent("aparte-compact", { detail: { targetId } }));
|
|
7775
|
+
return;
|
|
7776
|
+
}
|
|
7777
|
+
this._compact = "running";
|
|
7778
|
+
this._failure = "";
|
|
7779
|
+
this._render();
|
|
7780
|
+
let failed = null;
|
|
7781
|
+
try {
|
|
7782
|
+
const outcome = await compactor.compact({ target: this._chat(), ...targetId ? { targetId } : {}, force: true, reason });
|
|
7783
|
+
if (!outcome.ok) failed = outcome.error;
|
|
7784
|
+
else if (!outcome.skipped) this._viewChanged = true;
|
|
7785
|
+
} catch (error) {
|
|
7786
|
+
failed = error instanceof Error ? error.message : String(error);
|
|
6901
7787
|
}
|
|
7788
|
+
if (this._compact === "running") {
|
|
7789
|
+
this._compact = failed === null ? null : "failed";
|
|
7790
|
+
this._failure = failed ?? "";
|
|
7791
|
+
}
|
|
7792
|
+
if (this.isConnected) this._render();
|
|
7793
|
+
}
|
|
7794
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
7795
|
+
// The disclosure
|
|
7796
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
7797
|
+
_onClick = (event) => {
|
|
7798
|
+
const target = event.target;
|
|
7799
|
+
const action2 = target?.closest?.("[data-context-action]");
|
|
7800
|
+
if (action2) {
|
|
7801
|
+
void this._compactNow(action2.getAttribute("data-context-action") === "retry" ? "retry" : "manual");
|
|
7802
|
+
return;
|
|
7803
|
+
}
|
|
7804
|
+
if (target?.closest?.(".aparte-context__panel")) return;
|
|
7805
|
+
if (!target?.closest?.(".aparte-context")) return;
|
|
7806
|
+
this._setOpen(!this._open);
|
|
7807
|
+
};
|
|
7808
|
+
_setOpen(open, returnFocus = false) {
|
|
7809
|
+
if (open === this._open) return;
|
|
7810
|
+
this._open = open;
|
|
7811
|
+
if (open) this._listenWhileOpen();
|
|
7812
|
+
else this._stopListeningWhileOpen();
|
|
7813
|
+
this._render();
|
|
7814
|
+
if (open) this._place();
|
|
7815
|
+
else if (returnFocus) this.querySelector(".aparte-context__toggle")?.focus();
|
|
7816
|
+
}
|
|
7817
|
+
/**
|
|
7818
|
+
* Escape and a click anywhere else close the panel. Listened for on the document only
|
|
7819
|
+
* while it is open: a page full of closed gauges holds no listeners but its own.
|
|
7820
|
+
*/
|
|
7821
|
+
_onDocumentClick = (event) => {
|
|
7822
|
+
if (!event.composedPath().includes(this)) this._setOpen(false);
|
|
7823
|
+
};
|
|
7824
|
+
_onDocumentKeydown = (event) => {
|
|
7825
|
+
if (event.key !== "Escape") return;
|
|
7826
|
+
const inside = event.composedPath().includes(this);
|
|
7827
|
+
if (inside) event.stopPropagation();
|
|
7828
|
+
this._setOpen(false, inside || this.contains(document.activeElement));
|
|
7829
|
+
};
|
|
7830
|
+
_listenWhileOpen() {
|
|
7831
|
+
document.addEventListener("click", this._onDocumentClick);
|
|
7832
|
+
document.addEventListener("keydown", this._onDocumentKeydown);
|
|
7833
|
+
}
|
|
7834
|
+
_stopListeningWhileOpen() {
|
|
7835
|
+
document.removeEventListener("click", this._onDocumentClick);
|
|
7836
|
+
document.removeEventListener("keydown", this._onDocumentKeydown);
|
|
7837
|
+
}
|
|
7838
|
+
/**
|
|
7839
|
+
* Where the panel opens: above the reading by default, since a gauge usually sits in the
|
|
7840
|
+
* composer at the foot of the page; below when there is no room above, and flush with the
|
|
7841
|
+
* other edge when it would run past the window's.
|
|
7842
|
+
*/
|
|
7843
|
+
_place() {
|
|
7844
|
+
const panel = this.querySelector(".aparte-context__panel");
|
|
7845
|
+
if (!panel) return;
|
|
7846
|
+
panel.removeAttribute("data-placement");
|
|
7847
|
+
panel.removeAttribute("data-align");
|
|
7848
|
+
const anchor = this.getBoundingClientRect();
|
|
7849
|
+
const box = panel.getBoundingClientRect();
|
|
7850
|
+
if (box.height > 0 && anchor.top < box.height) panel.setAttribute("data-placement", "below");
|
|
7851
|
+
if (box.width > 0 && box.right > document.documentElement.clientWidth) panel.setAttribute("data-align", "end");
|
|
6902
7852
|
}
|
|
6903
7853
|
}
|
|
7854
|
+
deferHydrationOf(AparteContext);
|
|
6904
7855
|
if (!customElements.get("aparte-context")) {
|
|
6905
7856
|
customElements.define("aparte-context", AparteContext);
|
|
6906
7857
|
}
|
|
@@ -7363,11 +8314,12 @@ class AparteScrollRail extends HTMLElement {
|
|
|
7363
8314
|
this._held = null;
|
|
7364
8315
|
}
|
|
7365
8316
|
}
|
|
7366
|
-
function sameElements(
|
|
7367
|
-
if (
|
|
7368
|
-
for (let i2 = 0; i2 <
|
|
8317
|
+
function sameElements(a3, b2) {
|
|
8318
|
+
if (a3.length !== b2.length) return false;
|
|
8319
|
+
for (let i2 = 0; i2 < a3.length; i2++) if (a3[i2] !== b2[i2]) return false;
|
|
7369
8320
|
return true;
|
|
7370
8321
|
}
|
|
8322
|
+
deferHydrationOf(AparteScrollRail);
|
|
7371
8323
|
if (!customElements.get("aparte-scroll-rail")) {
|
|
7372
8324
|
customElements.define("aparte-scroll-rail", AparteScrollRail);
|
|
7373
8325
|
}
|
|
@@ -7718,6 +8670,7 @@ function visibleFocusable(root) {
|
|
|
7718
8670
|
function nearestSidebar(control) {
|
|
7719
8671
|
return control.closest("aparte-sidebar") ?? rootOf(control).querySelector("aparte-sidebar") ?? document.querySelector("aparte-sidebar");
|
|
7720
8672
|
}
|
|
8673
|
+
deferHydrationOf(AparteSidebar);
|
|
7721
8674
|
if (!customElements.get("aparte-sidebar")) {
|
|
7722
8675
|
customElements.define("aparte-sidebar", AparteSidebar);
|
|
7723
8676
|
}
|
|
@@ -8496,6 +9449,7 @@ class AparteSplit extends HTMLElement {
|
|
|
8496
9449
|
handle.dataset["ownLabel"] = "locale";
|
|
8497
9450
|
}
|
|
8498
9451
|
}
|
|
9452
|
+
deferHydrationOf(AparteSplit);
|
|
8499
9453
|
if (!customElements.get("aparte-split")) {
|
|
8500
9454
|
customElements.define("aparte-split", AparteSplit);
|
|
8501
9455
|
}
|
|
@@ -8571,7 +9525,7 @@ export {
|
|
|
8571
9525
|
AparteElicitation,
|
|
8572
9526
|
AparteElicitationAbortError,
|
|
8573
9527
|
p as AparteError,
|
|
8574
|
-
|
|
9528
|
+
AparteErrorCode,
|
|
8575
9529
|
AparteIcon,
|
|
8576
9530
|
AparteMessageRepository,
|
|
8577
9531
|
AparteOptgroup,
|
|
@@ -8583,7 +9537,7 @@ export {
|
|
|
8583
9537
|
AparteSplit,
|
|
8584
9538
|
s as AparteStreamParser,
|
|
8585
9539
|
AparteSuggestions,
|
|
8586
|
-
|
|
9540
|
+
aparteGlobalConfig,
|
|
8587
9541
|
u as applyElementProps,
|
|
8588
9542
|
v as attachConfig,
|
|
8589
9543
|
buildApprovalPanel,
|
|
@@ -8599,34 +9553,44 @@ export {
|
|
|
8599
9553
|
cssEscape,
|
|
8600
9554
|
I as declineDefaultRenderers,
|
|
8601
9555
|
J as defaultSanitizer,
|
|
8602
|
-
|
|
9556
|
+
deferHydrationOf,
|
|
9557
|
+
L as defineMountedSegmentRenderer,
|
|
9558
|
+
M as defineMountedToolRenderer,
|
|
9559
|
+
N as detachConfig,
|
|
8603
9560
|
escapeAttr,
|
|
8604
9561
|
escapeHtml,
|
|
8605
|
-
|
|
8606
|
-
|
|
9562
|
+
Q as estimateTokens,
|
|
9563
|
+
R as estimateWireTokens,
|
|
9564
|
+
S as filesToAttachments,
|
|
9565
|
+
T as getAllRenderers,
|
|
8607
9566
|
getSegmentRenderer,
|
|
9567
|
+
V as historyFromCheckpoint,
|
|
8608
9568
|
installDefaultRenderersOnce,
|
|
8609
9569
|
installDialogTriggersOnce,
|
|
8610
9570
|
isAwaitingReply,
|
|
8611
|
-
|
|
9571
|
+
Z as isFormatAdapter,
|
|
8612
9572
|
isSafeUrl,
|
|
8613
9573
|
isSegmentSettled,
|
|
8614
|
-
|
|
9574
|
+
measureContext,
|
|
9575
|
+
a1 as overrideSegmentRenderer,
|
|
9576
|
+
a2 as parseAparteEventStream,
|
|
8615
9577
|
parseMarkdownToSegments,
|
|
8616
9578
|
populateBubbleFromMessage,
|
|
8617
|
-
|
|
9579
|
+
a5 as readableToAsyncIterable,
|
|
8618
9580
|
registerAllComponents,
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
9581
|
+
a6 as registerDefaultRenderers,
|
|
9582
|
+
a7 as registerSegmentRenderer,
|
|
9583
|
+
a8 as releaseDeferredHydration,
|
|
9584
|
+
a9 as requestUserInput,
|
|
8622
9585
|
resolveConfig,
|
|
8623
9586
|
revokeAttachmentUrls,
|
|
8624
9587
|
runWithConfig,
|
|
8625
|
-
|
|
8626
|
-
|
|
9588
|
+
ad as segmentDuration,
|
|
9589
|
+
ae as segmentTiming,
|
|
8627
9590
|
subscribeConfigChange,
|
|
8628
|
-
|
|
8629
|
-
|
|
9591
|
+
ag as toToolCallView,
|
|
9592
|
+
ah as toolRefusal,
|
|
9593
|
+
ai as unregisterSegmentRenderer,
|
|
8630
9594
|
uuid
|
|
8631
9595
|
};
|
|
8632
9596
|
//# sourceMappingURL=index.js.map
|