@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.
Files changed (69) hide show
  1. package/dist/client/agent-runner.d.ts.map +1 -1
  2. package/dist/client/aparte-client.d.ts +35 -76
  3. package/dist/client/aparte-client.d.ts.map +1 -1
  4. package/dist/client/stream-adapter.d.ts.map +1 -1
  5. package/dist/client/wire-history.d.ts +78 -0
  6. package/dist/client/wire-history.d.ts.map +1 -1
  7. package/dist/components/bubble/aparte-chat-bubble.d.ts +88 -2
  8. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  9. package/dist/components/bubble/bubble-sync.d.ts +5 -1
  10. package/dist/components/bubble/bubble-sync.d.ts.map +1 -1
  11. package/dist/components/bubble/checkpoint-separator.d.ts +40 -0
  12. package/dist/components/bubble/checkpoint-separator.d.ts.map +1 -0
  13. package/dist/components/context/aparte-context.d.ts +157 -25
  14. package/dist/components/context/aparte-context.d.ts.map +1 -1
  15. package/dist/components/viewport/aparte-chat-viewport.d.ts +7 -0
  16. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  17. package/dist/config/aparte-config.d.ts +17 -0
  18. package/dist/config/aparte-config.d.ts.map +1 -1
  19. package/dist/config/locale.d.ts +62 -4
  20. package/dist/config/locale.d.ts.map +1 -1
  21. package/dist/context/checkpoint-state.d.ts +70 -0
  22. package/dist/context/checkpoint-state.d.ts.map +1 -0
  23. package/dist/context/checkpoint.d.ts +28 -0
  24. package/dist/context/checkpoint.d.ts.map +1 -0
  25. package/dist/context/measure.d.ts +148 -0
  26. package/dist/context/measure.d.ts.map +1 -0
  27. package/dist/context/restore.d.ts +24 -0
  28. package/dist/context/restore.d.ts.map +1 -0
  29. package/dist/conversations/conversation-controller.d.ts +13 -4
  30. package/dist/conversations/conversation-controller.d.ts.map +1 -1
  31. package/dist/conversations/conversation-manager.d.ts +6 -0
  32. package/dist/conversations/conversation-manager.d.ts.map +1 -1
  33. package/dist/custom-elements.json +13734 -12139
  34. package/dist/{defer-hydration-BwMqeRHq.js → defer-hydration-8vI8xNNx.js} +683 -250
  35. package/dist/defer-hydration-8vI8xNNx.js.map +1 -0
  36. package/dist/generated/element-attributes.d.ts +8 -2
  37. package/dist/generated/element-attributes.d.ts.map +1 -1
  38. package/dist/host/aparte-chat-host.d.ts +19 -2
  39. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  40. package/dist/index.css +1 -1
  41. package/dist/index.d.ts +5 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +819 -81
  44. package/dist/index.js.map +1 -1
  45. package/dist/index.node.d.ts +4 -1
  46. package/dist/index.node.d.ts.map +1 -1
  47. package/dist/index.node.js +33 -29
  48. package/dist/index.node.js.map +1 -1
  49. package/dist/transport/agent-handler.d.ts.map +1 -1
  50. package/dist/transport/backend-handler.d.ts.map +1 -1
  51. package/dist/transport/types.d.ts +24 -4
  52. package/dist/transport/types.d.ts.map +1 -1
  53. package/dist/types/chat.d.ts +16 -1
  54. package/dist/types/chat.d.ts.map +1 -1
  55. package/dist/types/compactor.d.ts +104 -0
  56. package/dist/types/compactor.d.ts.map +1 -0
  57. package/dist/types/errors.d.ts +2 -0
  58. package/dist/types/errors.d.ts.map +1 -1
  59. package/dist/types/events.d.ts +50 -3
  60. package/dist/types/events.d.ts.map +1 -1
  61. package/dist/types/index.d.ts +3 -2
  62. package/dist/types/index.d.ts.map +1 -1
  63. package/dist/types/model-provider.d.ts +3 -0
  64. package/dist/types/model-provider.d.ts.map +1 -1
  65. package/dist/types/models.d.ts +43 -3
  66. package/dist/types/models.d.ts.map +1 -1
  67. package/dist/types/segments.d.ts +1 -1
  68. package/package.json +2 -2
  69. package/dist/defer-hydration-BwMqeRHq.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { K as deferHydrationOf, a6 as resolveConfig, ag as eventOrigin, ah as activeElementIn, ab as subscribeConfigChange, a8 as runWithConfig, o as AparteElicitationAbortError, w as buildApprovalPanel, x as buildElicitationPanel, ai as gateGlide, O as escapeAttr, aj as toolCallRenderer, ak as toolRendererThatDrew, t as aparteGlobalConfig, al as isMountedRenderer, am as renderingVersionOf, H as cssEscape, an as onRenderersChange, ao as mergeSegmentUpdate, P as escapeHtml, Y as isSegmentSettled, ap as writeStreamedMarkdown, X as isSafeUrl, C as copyText, S as getSegmentRenderer, T as installDefaultRenderersOnce, r as AparteMessageRepository, aq as stampSegmentActivity, ar as stampSegmentOnInsert, as as stampSegmentOnUpdate, at as renumberSegments, au as openSegmentIds, av as isTerminalStatus, aw as adoptMessageSegments, V as isAwaitingReply, a0 as populateBubbleFromMessage, af as uuid, a7 as revokeAttachmentUrls, $ as parseMarkdownToSegments, ax as APARTE_DERIVED_SEGMENTS, ay as rememberConversationStatus, az as tellConversationStatus, aA as conversationStatusOf, e as APARTE_DEFAULT_LOCALE, aB as chatBoundaryOf, aC as rootOf, U as installDialogTriggersOnce } from "./defer-hydration-BwMqeRHq.js";
2
- import { A, a, b, c, d, f, g, h, i, j, k, l, m, n, p, q, s, u, v, y, z, B, D, E, F, G, I, J, L, M, N, Q, R, W, Z, _, a1, a2, a3, a4, a5, a9, aa, ac, ad, ae } from "./defer-hydration-BwMqeRHq.js";
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 a6 of this._attachments) {
2306
- const el2 = segmentRenderResultToElement(runWithConfig(this._cfg, () => customAttachment(a6)));
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((a6) => {
2312
- const name = escapeHtml(a6.name);
2313
- if (a6.type.startsWith("image/") && this._isShowableUrl(a6.url)) {
2314
- return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumbnail__image" src="${escapeHtml(a6.url)}" alt="${name}" loading="lazy" /><span class="aparte-thumb__name">${name}</span></div>`;
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(a6.name))}</span><span class="aparte-thumb__name">${name}</span></div>`;
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 (`_segmentsToText`) for the same reason — copying it along with the answer
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 a6 of config.user) buttons.push(this._actionButtonHtml(a6, icons, locale));
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 a6 of config.assistant) buttons.push(this._actionButtonHtml(a6, icons, locale));
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 a6 of this._cfg.getActions("bubble")) {
2606
- const roles = a6.bubble?.roles ?? ["user", "assistant"];
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:${a6.id}`;
2612
- btn.setAttribute("aria-label", a6.label);
2613
- btn.setAttribute("title", a6.label);
2614
- btn.innerHTML = a6.icon.startsWith("<") ? a6.icon : this._cfg.getIcon(a6.icon) ?? a6.iconFallback ?? "";
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(action, icons, locale) {
2620
- switch (action) {
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 action = btn.dataset["action"];
2983
+ const action2 = btn.dataset["action"];
2665
2984
  const messageId = this.getAttribute("message-id");
2666
- if (action?.startsWith("custom:") && messageId) {
2667
- const actionId = action.slice("custom:".length);
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 (action) {
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 = action === "feedback-positive" ? "positive" : "negative";
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(a6, b2, c2) {
3790
+ updateSegment(a3, b2, c2) {
3452
3791
  const clientForm = c2 === void 0 && typeof b2 === "object";
3453
- const messageId = clientForm ? this._activeMessageId() : a6;
3454
- const segmentId = clientForm ? a6 : b2;
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(a6, b2) {
3806
+ removeSegment(a3, b2) {
3468
3807
  const clientForm = b2 === void 0;
3469
- const messageId = clientForm ? this._activeMessageId() : a6;
3470
- const segmentId = clientForm ? a6 : b2;
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, action, payload, segmentId) {
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 (action) {
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((a6, b2) => b2.localeCompare(a6)).map((k2) => buckets.get(k2));
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(action) {
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 (action) {
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 el = (tag, className) => {
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
- const { usage } = detail;
6957
- this._used = usage.totalTokens ?? usage.inputTokens + usage.outputTokens;
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 context is smaller by an amount nobody has measured
6962
- * yet — the next turn's usage will say. Until then the gauge shows nothing rather
6963
- * than a number it knows to be wrong. The event names the chat it compacted, so on a
6964
- * multi-chat page only that chat's gauge resets; an unnamed one resets every gauge,
6965
- * and each is right again one turn later.
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
- if (detail?.skipped) return;
6972
- this._used = null;
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 the last turn reported — or `used` set, until a turn reports — else `null`. */
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 = resolveConfig(this).getCurrentModel();
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
- _render() {
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
- if (capacity === null || this._used === null) {
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 = this._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
- if (!root || !meter || !text) {
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
- root.append(meter, text);
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 = resolveConfig(this);
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 reading = `${format.format(used)} / ${format.format(capacity)}`;
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
- text.textContent = ring ? new Intl.NumberFormat(tag, { style: "percent", maximumFractionDigits: 0 }).format(percent / 100) : reading;
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", { detail: { targetId: this._ownTargetId() } }));
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(a6, b2) {
7584
- if (a6.length !== b2.length) return false;
7585
- for (let i2 = 0; i2 < a6.length; i2++) if (a6[i2] !== b2[i2]) return false;
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
- q as AparteErrorCode,
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 filesToAttachments,
8829
- R as getAllRenderers,
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
- W as isFormatAdapter,
9571
+ Z as isFormatAdapter,
8835
9572
  isSafeUrl,
8836
9573
  isSegmentSettled,
8837
- Z as overrideSegmentRenderer,
8838
- _ as parseAparteEventStream,
9574
+ measureContext,
9575
+ a1 as overrideSegmentRenderer,
9576
+ a2 as parseAparteEventStream,
8839
9577
  parseMarkdownToSegments,
8840
9578
  populateBubbleFromMessage,
8841
- a1 as readableToAsyncIterable,
9579
+ a5 as readableToAsyncIterable,
8842
9580
  registerAllComponents,
8843
- a2 as registerDefaultRenderers,
8844
- a3 as registerSegmentRenderer,
8845
- a4 as releaseDeferredHydration,
8846
- a5 as requestUserInput,
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
- a9 as segmentDuration,
8851
- aa as segmentTiming,
9588
+ ad as segmentDuration,
9589
+ ae as segmentTiming,
8852
9590
  subscribeConfigChange,
8853
- ac as toToolCallView,
8854
- ad as toolRefusal,
8855
- ae as unregisterSegmentRenderer,
9591
+ ag as toToolCallView,
9592
+ ah as toolRefusal,
9593
+ ai as unregisterSegmentRenderer,
8856
9594
  uuid
8857
9595
  };
8858
9596
  //# sourceMappingURL=index.js.map