@aparte/core 0.25.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 +35 -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 +88 -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/context/aparte-context.d.ts +157 -25
- package/dist/components/context/aparte-context.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 +17 -0
- 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 +13734 -12139
- package/dist/{defer-hydration-BwMqeRHq.js → defer-hydration-8vI8xNNx.js} +683 -250
- 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/host/aparte-chat-host.d.ts +19 -2
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +819 -81
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +4 -1
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +33 -29
- package/dist/index.node.js.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 +3 -2
- 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 +1 -1
- package/package.json +2 -2
- package/dist/defer-hydration-BwMqeRHq.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { K as deferHydrationOf,
|
|
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
|
}
|
|
@@ -1307,6 +1307,96 @@ deferHydrationOf(AparteChat);
|
|
|
1307
1307
|
if (!customElements.get("aparte-chat")) {
|
|
1308
1308
|
customElements.define("aparte-chat", AparteChat);
|
|
1309
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
|
+
}
|
|
1310
1400
|
let _warnedNoRenderer = false;
|
|
1311
1401
|
function warnMissingRenderer(type) {
|
|
1312
1402
|
if (_warnedNoRenderer) return;
|
|
@@ -1349,6 +1439,7 @@ function flushAvatarMarks() {
|
|
|
1349
1439
|
});
|
|
1350
1440
|
batch.forEach(([avatar], i2) => avatar.toggleAttribute("data-image", shown[i2]));
|
|
1351
1441
|
}
|
|
1442
|
+
let checkpointIds = 0;
|
|
1352
1443
|
class AparteChatBubble extends HTMLElement {
|
|
1353
1444
|
_contentEl = null;
|
|
1354
1445
|
_segmentsEl = null;
|
|
@@ -1362,6 +1453,23 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1362
1453
|
_role = "assistant";
|
|
1363
1454
|
_attachments = [];
|
|
1364
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}`;
|
|
1365
1473
|
/** Cleanup returned by the avatar provider — called on disconnect/re-render. */
|
|
1366
1474
|
_avatarCleanup = null;
|
|
1367
1475
|
/** Whether the avatar slot holds what a provider drew, which goes when the provider does. */
|
|
@@ -1391,7 +1499,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1391
1499
|
*/
|
|
1392
1500
|
_editReturnAction = null;
|
|
1393
1501
|
static get observedAttributes() {
|
|
1394
|
-
return ["data-role", "content", "timestamp", "message-id", "streaming", "name"];
|
|
1502
|
+
return ["data-role", "content", "timestamp", "message-id", "streaming", "name", "data-checkpoint", "data-checkpoint-reason"];
|
|
1395
1503
|
}
|
|
1396
1504
|
constructor() {
|
|
1397
1505
|
super();
|
|
@@ -1406,6 +1514,9 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1406
1514
|
this._updateName();
|
|
1407
1515
|
this._updateLocalizedLabels();
|
|
1408
1516
|
this._updateWaiting();
|
|
1517
|
+
this._updateFinishNote();
|
|
1518
|
+
this._checkpointDrawn = "";
|
|
1519
|
+
this._updateCheckpoint();
|
|
1409
1520
|
this._renderAvatar();
|
|
1410
1521
|
this._redrawSegmentsWhoseRendererChanged();
|
|
1411
1522
|
this._relabelSegments();
|
|
@@ -1633,6 +1744,8 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1633
1744
|
this._offRenderers ??= onRenderersChange(this._onRenderersChange);
|
|
1634
1745
|
this.addEventListener("click", this._onBranchPickerClick);
|
|
1635
1746
|
this.addEventListener("click", this._onLinkClick);
|
|
1747
|
+
this.addEventListener("click", this._onCheckpointClick);
|
|
1748
|
+
this.addEventListener("keydown", this._onCheckpointKeydown);
|
|
1636
1749
|
}
|
|
1637
1750
|
/**
|
|
1638
1751
|
* The boundary above this bubble changed — under every wrapper the chat's own config is
|
|
@@ -1671,6 +1784,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1671
1784
|
this._updateAttachments();
|
|
1672
1785
|
this._updateTimestamp(this.getAttribute("timestamp"));
|
|
1673
1786
|
this._updateBranchPicker();
|
|
1787
|
+
this._updateCheckpoint();
|
|
1674
1788
|
}
|
|
1675
1789
|
disconnectedCallback() {
|
|
1676
1790
|
window.removeEventListener("aparte-config-change", this._onConfigChange);
|
|
@@ -1678,6 +1792,8 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1678
1792
|
this._offRenderers = null;
|
|
1679
1793
|
this.removeEventListener("click", this._onBranchPickerClick);
|
|
1680
1794
|
this.removeEventListener("click", this._onLinkClick);
|
|
1795
|
+
this.removeEventListener("click", this._onCheckpointClick);
|
|
1796
|
+
this.removeEventListener("keydown", this._onCheckpointKeydown);
|
|
1681
1797
|
if (this._avatarCleanup) {
|
|
1682
1798
|
try {
|
|
1683
1799
|
this._avatarCleanup();
|
|
@@ -1704,6 +1820,12 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1704
1820
|
case "timestamp":
|
|
1705
1821
|
this._updateTimestamp(newValue);
|
|
1706
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;
|
|
1707
1829
|
case "streaming":
|
|
1708
1830
|
this._updateStreaming(newValue !== null && newValue !== "false");
|
|
1709
1831
|
break;
|
|
@@ -1853,6 +1975,38 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1853
1975
|
this._usage = usage ?? null;
|
|
1854
1976
|
this._updateActionBar();
|
|
1855
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
|
+
}
|
|
1856
2010
|
/**
|
|
1857
2011
|
* Update the branch picker UI for tree-based navigation.
|
|
1858
2012
|
* The viewport calls this after a branch switch or re-render.
|
|
@@ -1891,7 +2045,170 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1891
2045
|
this._updateAttachments();
|
|
1892
2046
|
this._updateWaiting();
|
|
1893
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;
|
|
1894
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;
|
|
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
|
+
};
|
|
1895
2212
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1896
2213
|
// Private Methods
|
|
1897
2214
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -2071,6 +2388,8 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2071
2388
|
this._renderAvatar();
|
|
2072
2389
|
if (this._streaming) this._updateStreaming(true);
|
|
2073
2390
|
this._updateWaiting();
|
|
2391
|
+
this._updateFinishNote();
|
|
2392
|
+
this._updateCheckpoint();
|
|
2074
2393
|
}
|
|
2075
2394
|
/**
|
|
2076
2395
|
* Show the built-in waiting indicator while this bubble is in flight and has
|
|
@@ -2302,18 +2621,18 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2302
2621
|
const customAttachment = this._cfg.getAttachmentRenderer?.();
|
|
2303
2622
|
if (customAttachment) {
|
|
2304
2623
|
this._attachmentsEl.replaceChildren();
|
|
2305
|
-
for (const
|
|
2306
|
-
const el2 = segmentRenderResultToElement(runWithConfig(this._cfg, () => customAttachment(
|
|
2624
|
+
for (const a3 of this._attachments) {
|
|
2625
|
+
const el2 = segmentRenderResultToElement(runWithConfig(this._cfg, () => customAttachment(a3)));
|
|
2307
2626
|
if (el2) this._attachmentsEl.appendChild(el2);
|
|
2308
2627
|
}
|
|
2309
2628
|
return;
|
|
2310
2629
|
}
|
|
2311
|
-
this._attachmentsEl.innerHTML = this._attachments.map((
|
|
2312
|
-
const name = escapeHtml(
|
|
2313
|
-
if (
|
|
2314
|
-
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>`;
|
|
2315
2634
|
}
|
|
2316
|
-
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>`;
|
|
2317
2636
|
}).join("");
|
|
2318
2637
|
if (!this._cfg.getHostHandlers().attachmentPreview) return;
|
|
2319
2638
|
this._attachmentsEl.querySelectorAll(".aparte-thumb--image").forEach((tile) => {
|
|
@@ -2469,7 +2788,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2469
2788
|
/**
|
|
2470
2789
|
* What the copy action copies: the reply, not the reasoning. A `thinking` segment is
|
|
2471
2790
|
* the model's scratchpad, and the client already keeps it out of the history it sends
|
|
2472
|
-
* back (`
|
|
2791
|
+
* back (`segmentsToWireText`) for the same reason — copying it along with the answer
|
|
2473
2792
|
* pasted a paragraph of deliberation above every reply, which no assistant on the
|
|
2474
2793
|
* market does. Segments with no `content` (a tool call) contribute nothing, so a
|
|
2475
2794
|
* turn made only of those copies as `''` — and the action bar reads this to decide
|
|
@@ -2499,14 +2818,14 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2499
2818
|
const buttons = [];
|
|
2500
2819
|
if (this._role === "user") {
|
|
2501
2820
|
if (config.user) {
|
|
2502
|
-
for (const
|
|
2821
|
+
for (const a3 of config.user) buttons.push(this._actionButtonHtml(a3, icons, locale));
|
|
2503
2822
|
} else {
|
|
2504
2823
|
if (config.copy) buttons.push(this._actionButtonHtml("copy", icons, locale));
|
|
2505
2824
|
if (config.edit) buttons.push(this._actionButtonHtml("edit", icons, locale));
|
|
2506
2825
|
}
|
|
2507
2826
|
} else if (this._role === "assistant") {
|
|
2508
2827
|
if (config.assistant) {
|
|
2509
|
-
for (const
|
|
2828
|
+
for (const a3 of config.assistant) buttons.push(this._actionButtonHtml(a3, icons, locale));
|
|
2510
2829
|
} else {
|
|
2511
2830
|
if (config.copy) buttons.push(this._actionButtonHtml("copy", icons, locale));
|
|
2512
2831
|
if (config.retry) buttons.push(this._actionButtonHtml("retry", icons, locale));
|
|
@@ -2602,22 +2921,22 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2602
2921
|
/** Append the registered custom action buttons for this bubble's role. */
|
|
2603
2922
|
_appendCustomActions() {
|
|
2604
2923
|
if (!this._actionBarEl) return;
|
|
2605
|
-
for (const
|
|
2606
|
-
const roles =
|
|
2924
|
+
for (const a3 of this._cfg.getActions("bubble")) {
|
|
2925
|
+
const roles = a3.bubble?.roles ?? ["user", "assistant"];
|
|
2607
2926
|
if (!roles.includes(this._role)) continue;
|
|
2608
2927
|
const btn = document.createElement("button");
|
|
2609
2928
|
btn.type = "button";
|
|
2610
2929
|
btn.className = "aparte-btn aparte-btn--icon aparte-btn--sm aparte-action-btn aparte-action-custom";
|
|
2611
|
-
btn.dataset["action"] = `custom:${
|
|
2612
|
-
btn.setAttribute("aria-label",
|
|
2613
|
-
btn.setAttribute("title",
|
|
2614
|
-
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 ?? "";
|
|
2615
2934
|
this._actionBarEl.appendChild(btn);
|
|
2616
2935
|
}
|
|
2617
2936
|
}
|
|
2618
2937
|
/** Build the `<button>` HTML for a single named action (shared by flag + per-role rendering). */
|
|
2619
|
-
_actionButtonHtml(
|
|
2620
|
-
switch (
|
|
2938
|
+
_actionButtonHtml(action2, icons, locale) {
|
|
2939
|
+
switch (action2) {
|
|
2621
2940
|
case "copy": {
|
|
2622
2941
|
const l2 = locale.copy ?? "Copy";
|
|
2623
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>`;
|
|
@@ -2661,10 +2980,10 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2661
2980
|
}
|
|
2662
2981
|
_handleActionClick(e) {
|
|
2663
2982
|
const btn = e.currentTarget;
|
|
2664
|
-
const
|
|
2983
|
+
const action2 = btn.dataset["action"];
|
|
2665
2984
|
const messageId = this.getAttribute("message-id");
|
|
2666
|
-
if (
|
|
2667
|
-
const actionId =
|
|
2985
|
+
if (action2?.startsWith("custom:") && messageId) {
|
|
2986
|
+
const actionId = action2.slice("custom:".length);
|
|
2668
2987
|
const detail = {
|
|
2669
2988
|
actionId,
|
|
2670
2989
|
zone: "bubble",
|
|
@@ -2680,7 +2999,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2680
2999
|
this._cfg.getActions("bubble").find((x) => x.id === actionId)?.onClick?.(e);
|
|
2681
3000
|
return;
|
|
2682
3001
|
}
|
|
2683
|
-
switch (
|
|
3002
|
+
switch (action2) {
|
|
2684
3003
|
case "copy": {
|
|
2685
3004
|
const text = this._copyableText();
|
|
2686
3005
|
const icons = this._cfg.getIconProvider();
|
|
@@ -2729,7 +3048,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2729
3048
|
case "feedback-positive":
|
|
2730
3049
|
case "feedback-negative": {
|
|
2731
3050
|
if (!messageId) break;
|
|
2732
|
-
const value =
|
|
3051
|
+
const value = action2 === "feedback-positive" ? "positive" : "negative";
|
|
2733
3052
|
btn.setAttribute("data-submitted", "");
|
|
2734
3053
|
const detail = { messageId, value };
|
|
2735
3054
|
this.dispatchEvent(new CustomEvent("aparte-feedback", {
|
|
@@ -2857,6 +3176,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2857
3176
|
}
|
|
2858
3177
|
}
|
|
2859
3178
|
this._updateWaiting();
|
|
3179
|
+
this._updateFinishNote();
|
|
2860
3180
|
if (wasStreaming && !streaming) this._updateContent();
|
|
2861
3181
|
if (wasStreaming && !streaming && this._segments.length > 0) this._redrawIfStale();
|
|
2862
3182
|
}
|
|
@@ -3151,6 +3471,13 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3151
3471
|
/** Last `--aparte-transcript-inset` written on the host, in px — same write-on-change rule. */
|
|
3152
3472
|
_transcriptInset = -1;
|
|
3153
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;
|
|
3154
3481
|
/**
|
|
3155
3482
|
* When true, _reRenderActivePath() only dispatches aparte-path-changed without
|
|
3156
3483
|
* touching the DOM. Set via setFrameworkManagedDOM(true) when a framework
|
|
@@ -3220,6 +3547,16 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3220
3547
|
this._boundResetHandler = () => this.clearAll();
|
|
3221
3548
|
window.addEventListener("aparte-reset", this._boundResetHandler);
|
|
3222
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();
|
|
3223
3560
|
}
|
|
3224
3561
|
/**
|
|
3225
3562
|
* A locale switch changes the reading direction, and `dir` follows it, so a chat
|
|
@@ -3276,6 +3613,8 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3276
3613
|
window.removeEventListener("aparte-reset", this._boundResetHandler);
|
|
3277
3614
|
this._boundResetHandler = null;
|
|
3278
3615
|
}
|
|
3616
|
+
this._stopCheckpointState?.();
|
|
3617
|
+
this._stopCheckpointState = null;
|
|
3279
3618
|
this._cleanup();
|
|
3280
3619
|
}
|
|
3281
3620
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
@@ -3448,10 +3787,10 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3448
3787
|
this._notifyBubble(messageId, "addSegment", stamped);
|
|
3449
3788
|
this._autoScroll();
|
|
3450
3789
|
}
|
|
3451
|
-
updateSegment(
|
|
3790
|
+
updateSegment(a3, b2, c2) {
|
|
3452
3791
|
const clientForm = c2 === void 0 && typeof b2 === "object";
|
|
3453
|
-
const messageId = clientForm ? this._activeMessageId() :
|
|
3454
|
-
const segmentId = clientForm ?
|
|
3792
|
+
const messageId = clientForm ? this._activeMessageId() : a3;
|
|
3793
|
+
const segmentId = clientForm ? a3 : b2;
|
|
3455
3794
|
const updates = clientForm ? b2 : c2;
|
|
3456
3795
|
if (!messageId) return;
|
|
3457
3796
|
const message = this._repo.getMessageById(messageId);
|
|
@@ -3464,10 +3803,10 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3464
3803
|
this._notifyBubble(messageId, "updateSegment", { segmentId, updates: stamped });
|
|
3465
3804
|
}
|
|
3466
3805
|
}
|
|
3467
|
-
removeSegment(
|
|
3806
|
+
removeSegment(a3, b2) {
|
|
3468
3807
|
const clientForm = b2 === void 0;
|
|
3469
|
-
const messageId = clientForm ? this._activeMessageId() :
|
|
3470
|
-
const segmentId = clientForm ?
|
|
3808
|
+
const messageId = clientForm ? this._activeMessageId() : a3;
|
|
3809
|
+
const segmentId = clientForm ? a3 : b2;
|
|
3471
3810
|
if (!messageId || !segmentId) return;
|
|
3472
3811
|
const message = this._repo.getMessageById(messageId);
|
|
3473
3812
|
if (message?.segments) {
|
|
@@ -3818,6 +4157,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3818
4157
|
this._setSpacerHeight(0);
|
|
3819
4158
|
this._isAutoScrollEnabled = true;
|
|
3820
4159
|
this._updateScrollButton();
|
|
4160
|
+
this._checkpointState?.clear();
|
|
3821
4161
|
this.dispatchEvent(new CustomEvent("aparte-reset-done", { bubbles: true, composed: true }));
|
|
3822
4162
|
}
|
|
3823
4163
|
/**
|
|
@@ -3893,12 +4233,12 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3893
4233
|
this._repo.addOrUpdateMessage(this._repo.headId, message);
|
|
3894
4234
|
return message;
|
|
3895
4235
|
}
|
|
3896
|
-
_notifyBubble(messageId,
|
|
4236
|
+
_notifyBubble(messageId, action2, payload, segmentId) {
|
|
3897
4237
|
const bubble = this.querySelector(
|
|
3898
4238
|
`aparte-chat-bubble[message-id="${cssEscape(messageId)}"], [data-aparte-bubble][message-id="${cssEscape(messageId)}"]`
|
|
3899
4239
|
);
|
|
3900
4240
|
if (!bubble) return;
|
|
3901
|
-
switch (
|
|
4241
|
+
switch (action2) {
|
|
3902
4242
|
case "appendToken":
|
|
3903
4243
|
bubble.appendToken?.(payload);
|
|
3904
4244
|
break;
|
|
@@ -3921,7 +4261,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3921
4261
|
break;
|
|
3922
4262
|
case "update": {
|
|
3923
4263
|
const updates = payload;
|
|
3924
|
-
const renderable = ["status", "segments", "content", "attachments", "usage"];
|
|
4264
|
+
const renderable = ["status", "segments", "content", "attachments", "usage", "finishReason", "contextSummary", "compaction"];
|
|
3925
4265
|
if (renderable.some((key) => key in updates)) {
|
|
3926
4266
|
bubble.updateMessage?.(payload);
|
|
3927
4267
|
}
|
|
@@ -3972,6 +4312,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3972
4312
|
}
|
|
3973
4313
|
this._dispatchPathChanged(activeMessages, siblingsInfo);
|
|
3974
4314
|
this._syncBusy();
|
|
4315
|
+
this._checkpointState?.reapply();
|
|
3975
4316
|
this._recalculateSpacer();
|
|
3976
4317
|
}
|
|
3977
4318
|
_dispatchPathChanged(messages, siblings) {
|
|
@@ -6267,7 +6608,7 @@ class AparteConversationList extends HTMLElement {
|
|
|
6267
6608
|
}
|
|
6268
6609
|
}
|
|
6269
6610
|
const fixed = ["pinned", "today", "yesterday", "week", "month"].map((k2) => buckets.get(k2)).filter((g2) => !!g2);
|
|
6270
|
-
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));
|
|
6271
6612
|
const undated = buckets.get("undated");
|
|
6272
6613
|
return [...fixed, ...months, ...undated ? [undated] : []];
|
|
6273
6614
|
}
|
|
@@ -6465,7 +6806,7 @@ class AparteConversationList extends HTMLElement {
|
|
|
6465
6806
|
console.warn("[aparte-conversation-list] the conversation manager refused the change:", err);
|
|
6466
6807
|
});
|
|
6467
6808
|
}
|
|
6468
|
-
_onMenuAction(
|
|
6809
|
+
_onMenuAction(action2) {
|
|
6469
6810
|
const open = this._open;
|
|
6470
6811
|
if (!open) return;
|
|
6471
6812
|
const conv = this._conversations.find((c2) => c2.id === open.id);
|
|
@@ -6474,7 +6815,7 @@ class AparteConversationList extends HTMLElement {
|
|
|
6474
6815
|
return;
|
|
6475
6816
|
}
|
|
6476
6817
|
const id = conv.id;
|
|
6477
|
-
switch (
|
|
6818
|
+
switch (action2) {
|
|
6478
6819
|
case "rename":
|
|
6479
6820
|
this._closeMenu();
|
|
6480
6821
|
this._startRename(open.row, conv);
|
|
@@ -6922,9 +7263,14 @@ const fraction = (raw, fallback) => {
|
|
|
6922
7263
|
const n2 = Number(raw);
|
|
6923
7264
|
return raw !== null && Number.isFinite(n2) && n2 > 0 && n2 <= 1 ? n2 : fallback;
|
|
6924
7265
|
};
|
|
6925
|
-
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) => {
|
|
6926
7271
|
const node = document.createElement(tag);
|
|
6927
7272
|
node.className = className;
|
|
7273
|
+
if (text !== void 0) node.textContent = text;
|
|
6928
7274
|
return node;
|
|
6929
7275
|
};
|
|
6930
7276
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
@@ -6943,33 +7289,129 @@ const ringSvg = () => {
|
|
|
6943
7289
|
}
|
|
6944
7290
|
return svg;
|
|
6945
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 };
|
|
6946
7301
|
class AparteContext extends HTMLElement {
|
|
6947
7302
|
static get observedAttributes() {
|
|
6948
7303
|
return ["window", "warn", "danger", "target", "variant", "used"];
|
|
6949
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
|
+
*/
|
|
6950
7310
|
_used = null;
|
|
6951
7311
|
_level = null;
|
|
6952
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
|
+
}
|
|
6953
7334
|
_onDone = (e) => {
|
|
6954
7335
|
const detail = e.detail;
|
|
6955
7336
|
if (!detail?.usage || !this._isMine(detail.targetId)) return;
|
|
6956
|
-
|
|
6957
|
-
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;
|
|
7361
|
+
this._render();
|
|
7362
|
+
};
|
|
7363
|
+
/**
|
|
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 = "";
|
|
6958
7390
|
this._render();
|
|
6959
7391
|
};
|
|
6960
7392
|
/**
|
|
6961
|
-
* After a real compaction the
|
|
6962
|
-
* yet
|
|
6963
|
-
*
|
|
6964
|
-
*
|
|
6965
|
-
*
|
|
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.
|
|
6966
7402
|
*/
|
|
6967
7403
|
_onCompacted = (e) => {
|
|
6968
7404
|
const detail = e.detail;
|
|
6969
7405
|
if (!this._isMine(detail?.targetId)) return;
|
|
6970
7406
|
this._compactRequested = false;
|
|
6971
|
-
|
|
6972
|
-
|
|
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;
|
|
6973
7415
|
this._render();
|
|
6974
7416
|
};
|
|
6975
7417
|
/** A failed compaction spends the request too; the next `danger` asks again. */
|
|
@@ -6977,11 +7419,23 @@ class AparteContext extends HTMLElement {
|
|
|
6977
7419
|
const detail = e.detail;
|
|
6978
7420
|
if (!this._isMine(detail?.targetId)) return;
|
|
6979
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();
|
|
6980
7434
|
};
|
|
6981
7435
|
_onRerender = () => this._render();
|
|
6982
|
-
/** Tokens
|
|
7436
|
+
/** Tokens in use — what is drawn — else the last reported or seeded reading, else `null`. */
|
|
6983
7437
|
get used() {
|
|
6984
|
-
return this._used;
|
|
7438
|
+
return this._measured?.used ?? this._used;
|
|
6985
7439
|
}
|
|
6986
7440
|
/**
|
|
6987
7441
|
* Writes the `used` attribute: a reading for a conversation restored without a turn of
|
|
@@ -7010,18 +7464,33 @@ class AparteContext extends HTMLElement {
|
|
|
7010
7464
|
get level() {
|
|
7011
7465
|
return this._level;
|
|
7012
7466
|
}
|
|
7467
|
+
/** The reading drawn now, parts and all, or `null` while nothing is shown. */
|
|
7468
|
+
get measure() {
|
|
7469
|
+
return this._measured;
|
|
7470
|
+
}
|
|
7013
7471
|
connectedCallback() {
|
|
7014
7472
|
window.addEventListener("aparte-message-done", this._onDone);
|
|
7473
|
+
window.addEventListener("aparte-message-error", this._onMessageError);
|
|
7474
|
+
window.addEventListener("aparte-compact-start", this._onCompactStart);
|
|
7015
7475
|
window.addEventListener("aparte-compact-done", this._onCompacted);
|
|
7016
7476
|
window.addEventListener("aparte-compact-error", this._onCompactError);
|
|
7477
|
+
window.addEventListener("aparte-reset-done", this._onReset);
|
|
7478
|
+
window.addEventListener("aparte-conversation-loaded", this._onLoaded);
|
|
7017
7479
|
window.addEventListener("aparte-config-change", this._onRerender);
|
|
7480
|
+
this.addEventListener("click", this._onClick);
|
|
7018
7481
|
this._render();
|
|
7019
7482
|
}
|
|
7020
7483
|
disconnectedCallback() {
|
|
7021
7484
|
window.removeEventListener("aparte-message-done", this._onDone);
|
|
7485
|
+
window.removeEventListener("aparte-message-error", this._onMessageError);
|
|
7486
|
+
window.removeEventListener("aparte-compact-start", this._onCompactStart);
|
|
7022
7487
|
window.removeEventListener("aparte-compact-done", this._onCompacted);
|
|
7023
7488
|
window.removeEventListener("aparte-compact-error", this._onCompactError);
|
|
7489
|
+
window.removeEventListener("aparte-reset-done", this._onReset);
|
|
7490
|
+
window.removeEventListener("aparte-conversation-loaded", this._onLoaded);
|
|
7024
7491
|
window.removeEventListener("aparte-config-change", this._onRerender);
|
|
7492
|
+
this.removeEventListener("click", this._onClick);
|
|
7493
|
+
this._stopListeningWhileOpen();
|
|
7025
7494
|
}
|
|
7026
7495
|
attributeChangedCallback(name) {
|
|
7027
7496
|
if (name === "used") {
|
|
@@ -7030,10 +7499,21 @@ class AparteContext extends HTMLElement {
|
|
|
7030
7499
|
}
|
|
7031
7500
|
if (this.isConnected) this._render();
|
|
7032
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
|
+
}
|
|
7033
7513
|
_window() {
|
|
7034
7514
|
const attr = Number(this.getAttribute("window"));
|
|
7035
7515
|
if (this.hasAttribute("window") && Number.isFinite(attr) && attr > 0) return attr;
|
|
7036
|
-
const model =
|
|
7516
|
+
const model = this._cfg().getCurrentModel();
|
|
7037
7517
|
return model?.contextWindow && model.contextWindow > 0 ? model.contextWindow : null;
|
|
7038
7518
|
}
|
|
7039
7519
|
/** The chat this gauge watches: `target`, else the chat host above it — the composer's rule. */
|
|
@@ -7052,43 +7532,113 @@ class AparteContext extends HTMLElement {
|
|
|
7052
7532
|
const own = this._ownTargetId();
|
|
7053
7533
|
return !targetId || !own || targetId === own;
|
|
7054
7534
|
}
|
|
7055
|
-
|
|
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) {
|
|
7056
7582
|
const capacity = this._window();
|
|
7057
|
-
|
|
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;
|
|
7058
7588
|
this.setAttribute("data-empty", "");
|
|
7059
7589
|
this.removeAttribute("data-level");
|
|
7590
|
+
this._open = false;
|
|
7591
|
+
this._stopListeningWhileOpen();
|
|
7592
|
+
this._panelSignature = "";
|
|
7060
7593
|
this.replaceChildren();
|
|
7061
7594
|
this._level = null;
|
|
7062
7595
|
return;
|
|
7063
7596
|
}
|
|
7597
|
+
this._measured = measure;
|
|
7064
7598
|
this.removeAttribute("data-empty");
|
|
7065
|
-
const used =
|
|
7599
|
+
const used = measure.used;
|
|
7066
7600
|
const ratio = Math.min(1, used / capacity);
|
|
7067
7601
|
const warn = fraction(this.getAttribute("warn"), 0.75);
|
|
7068
7602
|
const danger = fraction(this.getAttribute("danger"), 0.9);
|
|
7069
|
-
const level = ratio >= danger ? "danger" : ratio >= warn ? "warn" : "ok";
|
|
7603
|
+
const level = this._overflow || ratio >= danger ? "danger" : ratio >= warn ? "warn" : "ok";
|
|
7070
7604
|
this.setAttribute("data-level", level);
|
|
7071
7605
|
const ring = this.getAttribute("variant") === "ring";
|
|
7072
7606
|
let root = this.querySelector(".aparte-context");
|
|
7073
7607
|
if (root && root.classList.contains("aparte-context--ring") !== ring) {
|
|
7074
7608
|
this.replaceChildren();
|
|
7075
7609
|
root = null;
|
|
7610
|
+
this._panelSignature = "";
|
|
7076
7611
|
}
|
|
7077
7612
|
let meter = this.querySelector('[role="meter"]');
|
|
7078
7613
|
let text = this.querySelector(".aparte-context__text");
|
|
7079
|
-
|
|
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) {
|
|
7080
7618
|
root = el("div", ring ? "aparte-context aparte-context--ring" : "aparte-context");
|
|
7081
7619
|
meter = ring ? ringSvg() : el("div", "aparte-progress");
|
|
7082
7620
|
meter.setAttribute("role", "meter");
|
|
7083
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");
|
|
7084
7626
|
text = el("span", "aparte-context__text");
|
|
7085
|
-
|
|
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);
|
|
7086
7634
|
this.replaceChildren(root);
|
|
7635
|
+
this._panelSignature = "";
|
|
7087
7636
|
}
|
|
7088
|
-
const cfg =
|
|
7637
|
+
const cfg = this._cfg();
|
|
7089
7638
|
const tag = cfg.getLocale().tag || void 0;
|
|
7090
7639
|
const format = new Intl.NumberFormat(tag, { notation: "compact", maximumFractionDigits: 1 });
|
|
7091
|
-
const
|
|
7640
|
+
const approx = measure.estimated ? "~" : "";
|
|
7641
|
+
const reading = `${approx}${format.format(used)} / ${format.format(capacity)}`;
|
|
7092
7642
|
const percent = Math.round(ratio * 100);
|
|
7093
7643
|
const label = cfg.t("contextLabel") || "Context window";
|
|
7094
7644
|
if (ring) {
|
|
@@ -7103,7 +7653,13 @@ class AparteContext extends HTMLElement {
|
|
|
7103
7653
|
meter.setAttribute("aria-valuemax", String(capacity));
|
|
7104
7654
|
meter.setAttribute("aria-valuenow", String(used));
|
|
7105
7655
|
meter.setAttribute("aria-label", `${label}: ${reading}`);
|
|
7106
|
-
|
|
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 = "";
|
|
7107
7663
|
if (level !== this._level) {
|
|
7108
7664
|
this._level = level;
|
|
7109
7665
|
const targetId = this._ownTargetId();
|
|
@@ -7111,11 +7667,189 @@ class AparteContext extends HTMLElement {
|
|
|
7111
7667
|
this.dispatchEvent(new CustomEvent("aparte-context-threshold", { detail, bubbles: true, composed: true }));
|
|
7112
7668
|
if (level !== "danger") this._compactRequested = false;
|
|
7113
7669
|
}
|
|
7114
|
-
if (level === "danger" && this.hasAttribute("auto-compact") && !this._compactRequested) {
|
|
7670
|
+
if (askToCompact && level === "danger" && this.hasAttribute("auto-compact") && !this._compactRequested) {
|
|
7115
7671
|
this._compactRequested = true;
|
|
7116
|
-
window.dispatchEvent(new CustomEvent("aparte-compact", {
|
|
7672
|
+
window.dispatchEvent(new CustomEvent("aparte-compact", {
|
|
7673
|
+
detail: { targetId: this._ownTargetId(), auto: true, window: capacity }
|
|
7674
|
+
}));
|
|
7117
7675
|
}
|
|
7118
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);
|
|
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");
|
|
7852
|
+
}
|
|
7119
7853
|
}
|
|
7120
7854
|
deferHydrationOf(AparteContext);
|
|
7121
7855
|
if (!customElements.get("aparte-context")) {
|
|
@@ -7580,9 +8314,9 @@ class AparteScrollRail extends HTMLElement {
|
|
|
7580
8314
|
this._held = null;
|
|
7581
8315
|
}
|
|
7582
8316
|
}
|
|
7583
|
-
function sameElements(
|
|
7584
|
-
if (
|
|
7585
|
-
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;
|
|
7586
8320
|
return true;
|
|
7587
8321
|
}
|
|
7588
8322
|
deferHydrationOf(AparteScrollRail);
|
|
@@ -8791,7 +9525,7 @@ export {
|
|
|
8791
9525
|
AparteElicitation,
|
|
8792
9526
|
AparteElicitationAbortError,
|
|
8793
9527
|
p as AparteError,
|
|
8794
|
-
|
|
9528
|
+
AparteErrorCode,
|
|
8795
9529
|
AparteIcon,
|
|
8796
9530
|
AparteMessageRepository,
|
|
8797
9531
|
AparteOptgroup,
|
|
@@ -8825,34 +9559,38 @@ export {
|
|
|
8825
9559
|
N as detachConfig,
|
|
8826
9560
|
escapeAttr,
|
|
8827
9561
|
escapeHtml,
|
|
8828
|
-
Q as
|
|
8829
|
-
R as
|
|
9562
|
+
Q as estimateTokens,
|
|
9563
|
+
R as estimateWireTokens,
|
|
9564
|
+
S as filesToAttachments,
|
|
9565
|
+
T as getAllRenderers,
|
|
8830
9566
|
getSegmentRenderer,
|
|
9567
|
+
V as historyFromCheckpoint,
|
|
8831
9568
|
installDefaultRenderersOnce,
|
|
8832
9569
|
installDialogTriggersOnce,
|
|
8833
9570
|
isAwaitingReply,
|
|
8834
|
-
|
|
9571
|
+
Z as isFormatAdapter,
|
|
8835
9572
|
isSafeUrl,
|
|
8836
9573
|
isSegmentSettled,
|
|
8837
|
-
|
|
8838
|
-
|
|
9574
|
+
measureContext,
|
|
9575
|
+
a1 as overrideSegmentRenderer,
|
|
9576
|
+
a2 as parseAparteEventStream,
|
|
8839
9577
|
parseMarkdownToSegments,
|
|
8840
9578
|
populateBubbleFromMessage,
|
|
8841
|
-
|
|
9579
|
+
a5 as readableToAsyncIterable,
|
|
8842
9580
|
registerAllComponents,
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
9581
|
+
a6 as registerDefaultRenderers,
|
|
9582
|
+
a7 as registerSegmentRenderer,
|
|
9583
|
+
a8 as releaseDeferredHydration,
|
|
9584
|
+
a9 as requestUserInput,
|
|
8847
9585
|
resolveConfig,
|
|
8848
9586
|
revokeAttachmentUrls,
|
|
8849
9587
|
runWithConfig,
|
|
8850
|
-
|
|
8851
|
-
|
|
9588
|
+
ad as segmentDuration,
|
|
9589
|
+
ae as segmentTiming,
|
|
8852
9590
|
subscribeConfigChange,
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
9591
|
+
ag as toToolCallView,
|
|
9592
|
+
ah as toolRefusal,
|
|
9593
|
+
ai as unregisterSegmentRenderer,
|
|
8856
9594
|
uuid
|
|
8857
9595
|
};
|
|
8858
9596
|
//# sourceMappingURL=index.js.map
|