@aparte/core 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/client/agent-runner.d.ts.map +1 -1
  2. package/dist/client/aparte-client.d.ts +38 -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 +152 -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/chat/aparte-chat.d.ts.map +1 -1
  14. package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
  15. package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
  16. package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
  17. package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
  18. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  19. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  20. package/dist/components/composer/aparte-composer-toolbar.d.ts.map +1 -1
  21. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  22. package/dist/components/context/aparte-context.d.ts +157 -25
  23. package/dist/components/context/aparte-context.d.ts.map +1 -1
  24. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  25. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  26. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -1
  27. package/dist/components/sidebar/aparte-sidebar.d.ts.map +1 -1
  28. package/dist/components/split/aparte-split.d.ts.map +1 -1
  29. package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
  30. package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -1
  31. package/dist/components/viewport/aparte-chat-viewport.d.ts +7 -0
  32. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  33. package/dist/config/aparte-config.d.ts +47 -1
  34. package/dist/config/aparte-config.d.ts.map +1 -1
  35. package/dist/config/locale.d.ts +62 -4
  36. package/dist/config/locale.d.ts.map +1 -1
  37. package/dist/context/checkpoint-state.d.ts +70 -0
  38. package/dist/context/checkpoint-state.d.ts.map +1 -0
  39. package/dist/context/checkpoint.d.ts +28 -0
  40. package/dist/context/checkpoint.d.ts.map +1 -0
  41. package/dist/context/measure.d.ts +148 -0
  42. package/dist/context/measure.d.ts.map +1 -0
  43. package/dist/context/restore.d.ts +24 -0
  44. package/dist/context/restore.d.ts.map +1 -0
  45. package/dist/conversations/conversation-controller.d.ts +13 -4
  46. package/dist/conversations/conversation-controller.d.ts.map +1 -1
  47. package/dist/conversations/conversation-manager.d.ts +6 -0
  48. package/dist/conversations/conversation-manager.d.ts.map +1 -1
  49. package/dist/custom-elements.json +15347 -12878
  50. package/dist/{dialog-triggers-BqXRdHWZ.js → defer-hydration-8vI8xNNx.js} +1179 -374
  51. package/dist/defer-hydration-8vI8xNNx.js.map +1 -0
  52. package/dist/generated/element-attributes.d.ts +8 -2
  53. package/dist/generated/element-attributes.d.ts.map +1 -1
  54. package/dist/{glyphs-C8z2oJUg.js → glyphs-2bym5jl5.js} +1 -1
  55. package/dist/{glyphs-C8z2oJUg.js.map → glyphs-2bym5jl5.js.map} +1 -1
  56. package/dist/host/aparte-chat-host.d.ts +41 -2
  57. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  58. package/dist/icons.js +2 -2
  59. package/dist/index.css +1 -1
  60. package/dist/index.d.ts +7 -2
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +1059 -95
  63. package/dist/index.js.map +1 -1
  64. package/dist/index.node.d.ts +6 -2
  65. package/dist/index.node.d.ts.map +1 -1
  66. package/dist/index.node.js +39 -29
  67. package/dist/index.node.js.map +1 -1
  68. package/dist/interop/defer-hydration.d.ts +34 -0
  69. package/dist/interop/defer-hydration.d.ts.map +1 -0
  70. package/dist/primitives/icon/aparte-icon.d.ts.map +1 -1
  71. package/dist/primitives/progress-spinner/aparte-progress-spinner.d.ts.map +1 -1
  72. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  73. package/dist/primitives/select/aparte-option.d.ts.map +1 -1
  74. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  75. package/dist/renderers/index.d.ts +2 -1
  76. package/dist/renderers/index.d.ts.map +1 -1
  77. package/dist/renderers/mounted-renderer.d.ts +34 -0
  78. package/dist/renderers/mounted-renderer.d.ts.map +1 -0
  79. package/dist/renderers/segment-renderers.d.ts +14 -1
  80. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  81. package/dist/renderers/segments/tool-call.d.ts +2 -1
  82. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  83. package/dist/transport/agent-handler.d.ts.map +1 -1
  84. package/dist/transport/backend-handler.d.ts.map +1 -1
  85. package/dist/transport/types.d.ts +24 -4
  86. package/dist/transport/types.d.ts.map +1 -1
  87. package/dist/types/chat.d.ts +16 -1
  88. package/dist/types/chat.d.ts.map +1 -1
  89. package/dist/types/compactor.d.ts +104 -0
  90. package/dist/types/compactor.d.ts.map +1 -0
  91. package/dist/types/errors.d.ts +2 -0
  92. package/dist/types/errors.d.ts.map +1 -1
  93. package/dist/types/events.d.ts +50 -3
  94. package/dist/types/events.d.ts.map +1 -1
  95. package/dist/types/index.d.ts +5 -4
  96. package/dist/types/index.d.ts.map +1 -1
  97. package/dist/types/model-provider.d.ts +3 -0
  98. package/dist/types/model-provider.d.ts.map +1 -1
  99. package/dist/types/models.d.ts +43 -3
  100. package/dist/types/models.d.ts.map +1 -1
  101. package/dist/types/segments.d.ts +22 -4
  102. package/dist/types/segments.d.ts.map +1 -1
  103. package/dist/types/tools.d.ts +56 -0
  104. package/dist/types/tools.d.ts.map +1 -1
  105. package/package.json +2 -2
  106. package/dist/dialog-triggers-BqXRdHWZ.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { toolRefusal as toolRefusal$1, unansweredTail, runStreamAgent } from "@aparte/engine";
2
- import { A as APARTE_ICON_GLYPHS } from "./glyphs-C8z2oJUg.js";
2
+ import { A as APARTE_ICON_GLYPHS } from "./glyphs-2bym5jl5.js";
3
3
  function contentToText(content) {
4
4
  if (typeof content === "string") return content;
5
5
  return content.filter((p) => p.type === "text").map((p) => p.text).join("");
@@ -58,9 +58,14 @@ class AparteError extends Error {
58
58
  if (error instanceof AparteError) return error;
59
59
  const message = error instanceof Error ? error.message : String(error);
60
60
  const status = error?.status || error?.statusCode || defaultStatus;
61
- const code = defaultCode !== "UNKNOWN_ERROR" ? defaultCode : AparteError.codeForStatus(status) ?? AparteError.codeForFailure(error) ?? defaultCode;
61
+ const carried = error?.code;
62
+ const code = defaultCode !== "UNKNOWN_ERROR" ? defaultCode : AparteError.carriedCode(carried) ?? AparteError.codeForStatus(status) ?? AparteError.codeForFailure(error) ?? defaultCode;
62
63
  return new AparteError(message, code, void 0, error, status);
63
64
  }
65
+ /** The value as an {@link AparteErrorCode}, when it is one — a Node `ECONNRESET` or a vendor's own string is not. */
66
+ static carriedCode(value) {
67
+ return typeof value === "string" && Object.values(AparteErrorCode).includes(value) ? value : void 0;
68
+ }
64
69
  /**
65
70
  * What a thrown, statusless error says about itself. `fetch` rejects a network
66
71
  * failure with a TypeError whose message names the fetch ("Failed to fetch",
@@ -104,7 +109,26 @@ const APARTE_DEFAULT_LOCALE = {
104
109
  scrollToBottom: "Scroll to bottom",
105
110
  suggestionsLabel: "Suggested prompts",
106
111
  contextLabel: "Context window",
112
+ contextSystem: "System prompt",
113
+ contextTools: "Tools",
114
+ contextSummary: "Summary",
115
+ contextMessages: "Messages",
116
+ contextOther: "Other",
117
+ contextFree: "Free",
118
+ contextTotal: "Total",
119
+ contextReclaim: "Compacting now would free ~{n} tokens",
120
+ contextCompactNow: "Compact now",
107
121
  compactionSummaryTitle: "Conversation summary",
122
+ checkpointCount: "{n} messages summarised for the model",
123
+ checkpointCountOne: "1 message summarised for the model",
124
+ checkpointLegacy: "Earlier messages were summarised",
125
+ checkpointGroup: "Context summary",
126
+ checkpointShow: "Show summary",
127
+ checkpointHide: "Hide summary",
128
+ checkpointRestore: "Restore",
129
+ checkpointRestoreConfirm: "Send the original messages to the model again?",
130
+ checkpointPending: "Summarising the context…",
131
+ checkpointFailed: "Couldn't summarise the context",
108
132
  toolRunning: "Running",
109
133
  toolCompleted: "Done",
110
134
  toolRejected: "Rejected",
@@ -143,6 +167,7 @@ const APARTE_DEFAULT_LOCALE = {
143
167
  generating: "Generating…",
144
168
  rebuildingPreview: "Rebuilding preview…",
145
169
  typing: "Typing",
170
+ responseCutOff: "Cut off at the model's length limit.",
146
171
  error: "Error",
147
172
  running: "Running...",
148
173
  run: "Run",
@@ -199,15 +224,19 @@ function readAuth(auth) {
199
224
  if (auth && typeof auth === "object") return { key: auth["apiKey"], endpoint: auth["endpoint"] };
200
225
  return {};
201
226
  }
202
- async function vendorErrorMessage(response, label = "HTTP") {
203
- const body = await response.json().catch(() => ({}));
227
+ function messageOf(body, status, label) {
204
228
  const raw = body?.error;
205
229
  const msg = typeof raw === "object" ? raw?.message : raw;
206
- return typeof msg === "string" && msg ? msg : `${label} ${response.status}`;
230
+ return typeof msg === "string" && msg ? msg : `${label} ${status}`;
231
+ }
232
+ function isAparteErrorCode(value) {
233
+ return typeof value === "string" && Object.values(AparteErrorCode).includes(value);
207
234
  }
208
- async function vendorError(response, label = "HTTP") {
209
- const message = await vendorErrorMessage(response, label);
210
- const code = AparteError.codeForStatus(response.status) ?? AparteErrorCode.UNKNOWN_ERROR;
235
+ async function vendorError(response, label = "HTTP", adapter) {
236
+ const body = await response.json().catch(() => void 0);
237
+ const message = messageOf(body, response.status, label);
238
+ const forwarded = body?.error?.code;
239
+ const code = adapter?.classifyError?.(response.status, body) ?? (isAparteErrorCode(forwarded) ? forwarded : void 0) ?? AparteError.codeForStatus(response.status) ?? AparteErrorCode.UNKNOWN_ERROR;
211
240
  return new AparteError(message, code, void 0, void 0, response.status);
212
241
  }
213
242
  async function parseNonStreamText(provider, response, providerId) {
@@ -250,7 +279,7 @@ class AparteDirectTransport {
250
279
  signal: ctx.signal
251
280
  });
252
281
  if (!response.ok) {
253
- throw await vendorError(response);
282
+ throw await vendorError(response, "HTTP", provider);
254
283
  }
255
284
  if (request.stream === false) {
256
285
  return parseNonStreamText(provider, response, ctx.providerId);
@@ -878,12 +907,18 @@ function createAparteChatHandler(options) {
878
907
  const raw = await vendor.text().catch(() => "");
879
908
  let code;
880
909
  let type;
910
+ let body;
881
911
  try {
882
- const parsed = JSON.parse(raw);
912
+ body = JSON.parse(raw);
913
+ const parsed = body;
883
914
  code = parsed?.error?.code;
884
915
  type = parsed?.error?.type;
885
916
  } catch {
886
917
  }
918
+ try {
919
+ code = adapter.classifyError?.(vendor.status, body) ?? code;
920
+ } catch {
921
+ }
887
922
  return new Response(
888
923
  JSON.stringify({
889
924
  error: {
@@ -897,7 +932,12 @@ function createAparteChatHandler(options) {
897
932
  }
898
933
  if (request.stream === false) {
899
934
  const json = await vendor.json().catch(() => ({}));
900
- const text = typeof adapter.parseText === "function" ? adapter.parseText(json) : "";
935
+ let text;
936
+ try {
937
+ text = typeof adapter.parseText === "function" ? adapter.parseText(json) : "";
938
+ } catch {
939
+ return jsonError$1(502, "The vendor answered with an error.");
940
+ }
901
941
  return new Response(JSON.stringify({ text }), {
902
942
  headers: { "Content-Type": "application/json" }
903
943
  });
@@ -915,7 +955,7 @@ function createAparteChatHandler(options) {
915
955
  controller.close();
916
956
  return;
917
957
  }
918
- controller.enqueue(encoder2.encode(JSON.stringify(value) + "\n"));
958
+ controller.enqueue(encoder2.encode(JSON.stringify(relayable(value)) + "\n"));
919
959
  } catch (err) {
920
960
  controller.error(err);
921
961
  }
@@ -927,6 +967,13 @@ function createAparteChatHandler(options) {
927
967
  return new Response(ndjson, { headers: { "Content-Type": "application/x-ndjson" } });
928
968
  };
929
969
  }
970
+ function relayable(event) {
971
+ const e = event;
972
+ if (e?.type !== "error") return event;
973
+ console.error("[aparte] the vendor streamed an error", event);
974
+ const code = typeof e.code === "string" && Object.values(AparteErrorCode).includes(e.code) ? e.code : void 0;
975
+ return { type: "error", message: "The vendor reported an error.", ...code ? { code } : {} };
976
+ }
930
977
  function jsonError$1(status, message) {
931
978
  return new Response(JSON.stringify({ error: { message } }), {
932
979
  status,
@@ -950,13 +997,13 @@ function stampSegmentOnInsert(segments, segment, messageId, defaults) {
950
997
  ...timingPatch(base, { startedAt: segmentTiming(segment)?.startedAt ?? Date.now() })
951
998
  };
952
999
  }
953
- function adoptSegment(segments, segment, messageId, live = false) {
1000
+ function adoptSegment(segments, segment, messageId, live2 = false) {
954
1001
  return {
955
1002
  ...segment,
956
1003
  messageId,
957
1004
  index: segments.length,
958
1005
  isStreaming: false,
959
- ...segment.type === "tool_call" && (segment.status === "awaiting-approval" || segment.status === "pending" && !live) && !segment.deferred?.ticket ? { status: "aborted" } : {}
1006
+ ...segment.type === "tool_call" && (segment.status === "awaiting-approval" || segment.status === "pending" && !live2) && !segment.deferred?.ticket ? { status: "aborted" } : {}
960
1007
  };
961
1008
  }
962
1009
  const RESERVED = /* @__PURE__ */ new Set(["id", "type", "messageId", "index"]);
@@ -1030,11 +1077,11 @@ function openSegmentIds(segments) {
1030
1077
  }
1031
1078
  function adoptMessageSegments(message) {
1032
1079
  if (!message.segments?.length) return { ...message };
1033
- const live = message.status === "streaming" || message.status === "pending";
1080
+ const live2 = message.status === "streaming" || message.status === "pending";
1034
1081
  return {
1035
1082
  ...message,
1036
1083
  segments: message.segments.reduce((acc, segment) => {
1037
- acc.push(adoptSegment(acc, segment, message.id, live));
1084
+ acc.push(adoptSegment(acc, segment, message.id, live2));
1038
1085
  return acc;
1039
1086
  }, [])
1040
1087
  };
@@ -1070,8 +1117,8 @@ function detachConfig(el2) {
1070
1117
  if (previous) notifyConfigChanged(el2, resolveConfig(el2.parentElement), previous);
1071
1118
  }
1072
1119
  function resolveConfig(el2) {
1073
- const host = el2?.closest?.(`[${APARTE_HOST_ATTR}]`);
1074
- return host?.[CONFIG_SLOT] ?? aparteGlobalConfig;
1120
+ const host2 = el2?.closest?.(`[${APARTE_HOST_ATTR}]`);
1121
+ return host2?.[CONFIG_SLOT] ?? aparteGlobalConfig;
1075
1122
  }
1076
1123
  let _renderConfig = null;
1077
1124
  function runWithConfig(config, fn) {
@@ -1248,7 +1295,10 @@ function createStreamAdapter(opts) {
1248
1295
  break;
1249
1296
  }
1250
1297
  case "run-done":
1251
- target.updateMessage?.(messageId, { status: "completed" });
1298
+ target.updateMessage?.(messageId, {
1299
+ status: "completed",
1300
+ ...e.finishReason ? { finishReason: e.finishReason } : {}
1301
+ });
1252
1302
  if (e.usage) {
1253
1303
  try {
1254
1304
  target.setUsage?.(messageId, e.usage);
@@ -1519,10 +1569,11 @@ function createAparteAgentHandler(options) {
1519
1569
  console.error("[aparte] agent run failed", error);
1520
1570
  await pendingWrite?.catch(() => {
1521
1571
  });
1572
+ const named = AparteError.from(error).code;
1522
1573
  write({
1523
1574
  type: "run-error",
1524
1575
  message: "The agent run failed.",
1525
- code: error instanceof AparteError ? error.code : "AGENT_RUN_FAILED"
1576
+ code: named === AparteErrorCode.UNKNOWN_ERROR ? "AGENT_RUN_FAILED" : named
1526
1577
  });
1527
1578
  close();
1528
1579
  });
@@ -1928,6 +1979,33 @@ function renderingVersionOf(cfg) {
1928
1979
  function bumpRenderingVersion(cfg) {
1929
1980
  renderingVersions.set(cfg, renderingVersionOf(cfg) + 1);
1930
1981
  }
1982
+ const rendererListeners = /* @__PURE__ */ new Set();
1983
+ const pendingRendererChanges = /* @__PURE__ */ new Set();
1984
+ function onRenderersChange(listener) {
1985
+ rendererListeners.add(listener);
1986
+ return () => {
1987
+ rendererListeners.delete(listener);
1988
+ };
1989
+ }
1990
+ function notifyRenderersChanged(cfg) {
1991
+ if (pendingRendererChanges.size === 0) {
1992
+ queueMicrotask(() => {
1993
+ const changed = [...pendingRendererChanges];
1994
+ pendingRendererChanges.clear();
1995
+ for (const config of changed) {
1996
+ for (const listener of [...rendererListeners]) {
1997
+ try {
1998
+ listener(config);
1999
+ } catch (err) {
2000
+ console.error("[aparte] a renderer-change listener threw", err);
2001
+ }
2002
+ }
2003
+ }
2004
+ });
2005
+ }
2006
+ pendingRendererChanges.add(cfg);
2007
+ }
2008
+ const matcherKey = (match) => match instanceof RegExp ? `re:${String(match)}` : `name:${match}`;
1931
2009
  const ELEMENT_SHAPING = ["hidden", "open", "details", "quiet"];
1932
2010
  function drawingOf(provider, name) {
1933
2011
  if (!provider || !(name in provider) || name in Object.prototype) return void 0;
@@ -1996,6 +2074,8 @@ class AparteConfig {
1996
2074
  _modelPreferenceProvider;
1997
2075
  // Per-call approval policy (a mode: plan / ask / auto). `null` = the tools' own flags.
1998
2076
  _approvalPolicy = null;
2077
+ // What makes the context smaller — `@aparte/plugin-compaction` registers itself here. `null` = nothing does.
2078
+ _compactor = null;
1999
2079
  // Conversation persistence (optional, agnostic)
2000
2080
  _conversationManager;
2001
2081
  /*
@@ -2039,6 +2119,11 @@ class AparteConfig {
2039
2119
  _tools = /* @__PURE__ */ new Map();
2040
2120
  // By exact name, pattern or '*' — see `tool-table.ts` for the one resolution rule.
2041
2121
  _toolRenderers = new ToolTable();
2122
+ /** {@link overrideToolRenderer}'s stacks: what each matcher held below every override. */
2123
+ /** Per matcher: what it held below every override, then one entry per override — an object of its own, so two overrides of the same renderer are told apart. */
2124
+ _toolRendererOverrides = /* @__PURE__ */ new Map();
2125
+ /** Bumped by `reset()`: a restore function from before it must not undo what came after. */
2126
+ _overrideGeneration = 0;
2042
2127
  _toolDisplays = new ToolTable();
2043
2128
  /** Tagged blocks the stream parser recognises in the prose — see {@link registerStreamBlock}. */
2044
2129
  _streamBlocks = /* @__PURE__ */ new Map();
@@ -2481,7 +2566,11 @@ class AparteConfig {
2481
2566
  try {
2482
2567
  const auth = await this.getKey(providerId);
2483
2568
  const models = await provider.fetchModels(auth);
2569
+ const held = this._fetchedModels.get(providerId);
2570
+ if (models.length === 0 && held?.length) return held;
2571
+ const windowBefore = this.getCurrentModel()?.contextWindow;
2484
2572
  this._fetchedModels.set(providerId, models);
2573
+ if (this.getCurrentModel()?.contextWindow !== windowBefore) this._notify();
2485
2574
  return models;
2486
2575
  } catch (error) {
2487
2576
  console.warn(`[AparteConfig] Failed to refresh models for ${providerId}`, error);
@@ -2605,6 +2694,26 @@ class AparteConfig {
2605
2694
  getApprovalPolicy() {
2606
2695
  return this._approvalPolicy;
2607
2696
  }
2697
+ /**
2698
+ * Register what makes the context smaller: the gauge's Compact now, a separator's
2699
+ * Retry and the client's pre-flight and overflow retry call it. `null` removes it.
2700
+ *
2701
+ * `@aparte/plugin-compaction`'s `setupCompaction()` registers itself here; core never
2702
+ * compacts on its own, so a page without a compactor has a gauge that measures and a
2703
+ * separator whose Retry falls back to the `aparte-compact` event. A chat reads the one
2704
+ * on ITS config first (`resolveConfig(chat).getCompactor()`), then the client's.
2705
+ *
2706
+ * Notifies like every other registration: a mounted gauge has to hear that "Compact
2707
+ * now" became possible after it was drawn, or went away.
2708
+ */
2709
+ setCompactor(compactor) {
2710
+ this._compactor = compactor;
2711
+ this._notify();
2712
+ }
2713
+ /** The registered compactor, or `null`. */
2714
+ getCompactor() {
2715
+ return this._compactor;
2716
+ }
2608
2717
  /**
2609
2718
  * What the policy says about one call, with the tool's own `needsApproval` as the
2610
2719
  * answer when it has no opinion. The one place the two are combined, so the client's
@@ -2880,12 +2989,64 @@ class AparteConfig {
2880
2989
  * // Custom pill for a web-search tool
2881
2990
  * aparteGlobalConfig.registerToolRenderer('web_search', { render: (seg) => `<div class="aparte-tool-label">Searching...</div>` });
2882
2991
  *
2992
+ * Calls already on screen follow: those this registration now draws are drawn again
2993
+ * by it (the renderer that drew them before gets its `cleanup`), and nothing else on
2994
+ * the page is touched.
2995
+ *
2883
2996
  * @example
2884
2997
  * // One look for every tool an MCP server brings
2885
2998
  * aparteGlobalConfig.registerToolRenderer(/^mcp__/, { render: (seg) => myMcpCard(seg) });
2886
2999
  */
2887
3000
  registerToolRenderer(match, renderer) {
3001
+ if (this._toolRenderers.get(match) === renderer) return;
2888
3002
  this._toolRenderers.set(match, renderer);
3003
+ notifyRenderersChanged(this);
3004
+ }
3005
+ /**
3006
+ * Put `renderer` over whatever this matcher holds, and get back the function that takes
3007
+ * it off again — the registration a component makes for as long as it is mounted.
3008
+ *
3009
+ * Overrides stack per matcher. The function restores what was there before THIS
3010
+ * override, and taking off an override that is no longer on top changes nothing on
3011
+ * screen: two components declaring the same tool, the newer wins, and unmounting the
3012
+ * older does not take the newer's away. When the last one goes, the matcher holds what
3013
+ * it held before the first — another registration, or nothing. Calls on screen follow
3014
+ * each change, as with {@link registerToolRenderer}. The returned function is safe to
3015
+ * call twice.
3016
+ *
3017
+ * @example
3018
+ * const restore = aparteGlobalConfig.overrideToolRenderer('get_weather', weatherCard);
3019
+ * // …later, when the component that declared it unmounts:
3020
+ * restore();
3021
+ */
3022
+ overrideToolRenderer(match, renderer) {
3023
+ const key = matcherKey(match);
3024
+ const current = this._toolRenderers.get(match);
3025
+ let stack = this._toolRendererOverrides.get(key);
3026
+ if (!stack) {
3027
+ stack = [{ renderer: current }];
3028
+ this._toolRendererOverrides.set(key, stack);
3029
+ } else if (stack[stack.length - 1].renderer !== current) {
3030
+ stack.splice(0, stack.length, { renderer: current });
3031
+ }
3032
+ const entry = { renderer };
3033
+ stack.push(entry);
3034
+ this.registerToolRenderer(match, renderer);
3035
+ const generation = this._overrideGeneration;
3036
+ let restored = false;
3037
+ return () => {
3038
+ if (restored || generation !== this._overrideGeneration) return;
3039
+ restored = true;
3040
+ const i = stack.indexOf(entry);
3041
+ if (i <= 0) return;
3042
+ const wasTop = i === stack.length - 1;
3043
+ stack.splice(i, 1);
3044
+ if (stack.length === 1 && this._toolRendererOverrides.get(key) === stack) this._toolRendererOverrides.delete(key);
3045
+ if (!wasTop || this._toolRenderers.get(match) !== renderer) return;
3046
+ const top = stack[stack.length - 1].renderer;
3047
+ if (top) this.registerToolRenderer(match, top);
3048
+ else this.unregisterToolRenderer(match);
3049
+ };
2889
3050
  }
2890
3051
  /**
2891
3052
  * Field defaults for a segment TYPE, filled in when a segment enters a message.
@@ -2925,9 +3086,11 @@ class AparteConfig {
2925
3086
  clearSegmentDefaults(type) {
2926
3087
  this._segmentDefaults.delete(type);
2927
3088
  }
2928
- /** Unregister the renderer registered under this matcher. */
3089
+ /** Unregister the renderer registered under this matcher. Calls on screen it drew go back to core's row. */
2929
3090
  unregisterToolRenderer(match) {
3091
+ if (this._toolRenderers.get(match) === void 0) return;
2930
3092
  this._toolRenderers.delete(match);
3093
+ notifyRenderersChanged(this);
2931
3094
  }
2932
3095
  /** The renderer that draws a tool named `toolName` — exact, pattern or `'*'` — or undefined. */
2933
3096
  getToolRenderer(toolName) {
@@ -3278,6 +3441,9 @@ class AparteConfig {
3278
3441
  this._fetchedModels.clear();
3279
3442
  this._tools.clear();
3280
3443
  this._toolRenderers.clear();
3444
+ this._toolRendererOverrides.clear();
3445
+ this._overrideGeneration++;
3446
+ notifyRenderersChanged(this);
3281
3447
  this._toolDisplays.clear();
3282
3448
  this._streamBlocks.clear();
3283
3449
  this._modelConfig = {};
@@ -3288,6 +3454,7 @@ class AparteConfig {
3288
3454
  this._elicitationFieldRenderer = void 0;
3289
3455
  this._modelPreferenceProvider = void 0;
3290
3456
  this._approvalPolicy = null;
3457
+ this._compactor = null;
3291
3458
  this._bubbleActionsConfig = { ...APARTE_DEFAULT_BUBBLE_ACTIONS };
3292
3459
  this._hostHandlers = { ...APARTE_DEFAULT_HOST_HANDLERS };
3293
3460
  this._notify();
@@ -3329,19 +3496,19 @@ const ONE_SHOT_PROBE = "<i>a</i>";
3329
3496
  function hasOneShotMarkdownProvider() {
3330
3497
  return contextConfig().renderMarkdown(ONE_SHOT_PROBE) !== escapeHtml(ONE_SHOT_PROBE);
3331
3498
  }
3332
- function writeStreamedMarkdown(host, contentEl, content, streaming) {
3499
+ function writeStreamedMarkdown(host2, contentEl, content, streaming) {
3333
3500
  if (streaming) {
3334
- if (host._aparteSmd === void 0) {
3501
+ if (host2._aparteSmd === void 0) {
3335
3502
  settledFromParser.delete(contentEl);
3336
3503
  const renderer = contextConfig().createStreamingMarkdownRenderer(contentEl);
3337
3504
  if (renderer) {
3338
3505
  contentEl.textContent = "";
3339
- host._aparteSmd = { renderer, written: 0 };
3506
+ host2._aparteSmd = { renderer, written: 0 };
3340
3507
  } else {
3341
- host._aparteSmd = null;
3508
+ host2._aparteSmd = null;
3342
3509
  }
3343
3510
  }
3344
- const smd = host._aparteSmd;
3511
+ const smd = host2._aparteSmd;
3345
3512
  if (smd) {
3346
3513
  const delta = content.slice(smd.written);
3347
3514
  if (delta) {
@@ -3350,15 +3517,15 @@ function writeStreamedMarkdown(host, contentEl, content, streaming) {
3350
3517
  }
3351
3518
  return;
3352
3519
  }
3353
- } else if (host._aparteSmd) {
3354
- const smd = host._aparteSmd;
3520
+ } else if (host2._aparteSmd) {
3521
+ const smd = host2._aparteSmd;
3355
3522
  const tail = content.slice(smd.written);
3356
3523
  if (tail) {
3357
3524
  smd.renderer.write(tail);
3358
3525
  smd.written = content.length;
3359
3526
  }
3360
3527
  smd.renderer.end();
3361
- host._aparteSmd = void 0;
3528
+ host2._aparteSmd = void 0;
3362
3529
  if (!hasOneShotMarkdownProvider()) {
3363
3530
  contentEl.innerHTML = contextConfig().sanitizeHtml(contentEl.innerHTML);
3364
3531
  settledFromParser.set(contentEl, content);
@@ -3587,16 +3754,16 @@ function streamHighlight(element, paneSelector, content, lang, segId) {
3587
3754
  if (now - (_lastHighlightAt.get(segId) ?? 0) < HIGHLIGHT_DEBOUNCE_MS) return;
3588
3755
  markThrottle(_lastHighlightAt, segId, now);
3589
3756
  void contextConfig(element).highlightCode(content.slice(0, cut), lang).then((html) => {
3590
- const live = element.querySelector(paneSelector);
3591
- if (!live) return;
3592
- if (cut <= highlightedLen(live)) return;
3593
- live.innerHTML = html;
3594
- const codeEl = live.querySelector("code") ?? live;
3757
+ const live2 = element.querySelector(paneSelector);
3758
+ if (!live2) return;
3759
+ if (cut <= highlightedLen(live2)) return;
3760
+ live2.innerHTML = html;
3761
+ const codeEl = live2.querySelector("code") ?? live2;
3595
3762
  const span = document.createElement("span");
3596
3763
  span.dataset.aparteTail = "";
3597
3764
  span.textContent = content.slice(cut);
3598
3765
  codeEl.appendChild(span);
3599
- live.dataset.aparteHlLen = String(cut);
3766
+ live2.dataset.aparteHlLen = String(cut);
3600
3767
  }).catch(() => {
3601
3768
  });
3602
3769
  }
@@ -3820,9 +3987,9 @@ function sameShape(a, b) {
3820
3987
  }
3821
3988
  return true;
3822
3989
  }
3823
- function patchChanges(live, prev, next) {
3824
- for (let i = 0; i < live.childNodes.length; i++) {
3825
- const l = live.childNodes[i];
3990
+ function patchChanges(live2, prev, next) {
3991
+ for (let i = 0; i < live2.childNodes.length; i++) {
3992
+ const l = live2.childNodes[i];
3826
3993
  const p = prev.childNodes[i];
3827
3994
  const n = next.childNodes[i];
3828
3995
  if (l instanceof Element && p instanceof Element && n instanceof Element) {
@@ -3989,16 +4156,26 @@ function placeholderMarkup(segment) {
3989
4156
  return `<div class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(segment.status ?? "pending")}" data-tool-call-id="${escapeAttr(segment.toolCall?.id ?? "")}" data-tool-name="${escapeAttr(segment.toolCall?.name ?? "tool")}" hidden></div>`;
3990
4157
  }
3991
4158
  const isPlaceholder = (element) => element.hidden && element.classList.contains("aparte-segment-tool-call") && element.childElementCount === 0;
3992
- function drawDefault(segment, behind) {
4159
+ const drawnBy = /* @__PURE__ */ new WeakMap();
4160
+ const behind = /* @__PURE__ */ new WeakMap();
4161
+ function toolRendererThatDrew(element) {
4162
+ return behind.get(element) ?? null;
4163
+ }
4164
+ function drawerOf(element, current) {
4165
+ const recorded = drawnBy.get(element);
4166
+ if (recorded) return recorded;
4167
+ return element.classList.contains("aparte-segment-tool-call") ? null : current;
4168
+ }
4169
+ function drawDefault(segment, behind2) {
3993
4170
  const name = segment.toolCall?.name ?? "tool";
3994
4171
  const status = segment.status ?? "pending";
3995
4172
  const toolCallId = segment.toolCall?.id ?? "";
3996
4173
  const pill = pillMarkup(segment);
3997
4174
  const quiet = isQuiet(segment) ? " data-quiet" : "";
3998
4175
  const toggle = contextConfig().getIcon("expand");
3999
- const input = behind.kind === "parts" ? describeInput(segment) : "";
4000
- const outcome = behind.kind === "parts" ? outcomeOf(segment) : null;
4001
- if (behind.kind === "none" || behind.kind === "parts" && !input && !outcome) {
4176
+ const input = behind2.kind === "parts" ? describeInput(segment) : "";
4177
+ const outcome = behind2.kind === "parts" ? outcomeOf(segment) : null;
4178
+ if (behind2.kind === "none" || behind2.kind === "parts" && !input && !outcome) {
4002
4179
  return `
4003
4180
  <div class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}" data-tool-name="${escapeAttr(name)}"${quiet}><div class="aparte-tool-line">${pill}</div>
4004
4181
  </div>
@@ -4007,7 +4184,7 @@ function drawDefault(segment, behind) {
4007
4184
  const cfg = contextConfig();
4008
4185
  const copy = copyButtonMarkup("aparte-tool-copy");
4009
4186
  const open = segment.collapsed !== void 0 ? !segment.collapsed : cfg.getToolDisplay(name)?.open === true;
4010
- if (behind.kind === "body") {
4187
+ if (behind2.kind === "body") {
4011
4188
  const tpl = document.createElement("template");
4012
4189
  tpl.innerHTML = `
4013
4190
  <details class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}" data-tool-name="${escapeAttr(name)}"${quiet}${open ? " open" : ""}>
@@ -4015,7 +4192,7 @@ function drawDefault(segment, behind) {
4015
4192
  <div class="aparte-tool-detail" data-details="app"></div>
4016
4193
  </details>`.trim();
4017
4194
  const row = tpl.content.firstElementChild;
4018
- setBody(row, behind.body);
4195
+ setBody(row, behind2.body);
4019
4196
  return row;
4020
4197
  }
4021
4198
  return `
@@ -4044,7 +4221,12 @@ const toolCallRenderer = {
4044
4221
  const customRenderer = contextConfig().getToolRenderer(name);
4045
4222
  if (customRenderer) {
4046
4223
  injectToolRendererStyles(name, customRenderer);
4047
- return customRenderer.render(segment) || placeholderMarkup(segment);
4224
+ const out = customRenderer.render(segment);
4225
+ if (out instanceof HTMLElement) {
4226
+ drawnBy.set(out, customRenderer);
4227
+ behind.set(out, customRenderer);
4228
+ }
4229
+ return out || placeholderMarkup(segment);
4048
4230
  }
4049
4231
  return drawDefault(segment, behindOf(segment));
4050
4232
  },
@@ -4061,7 +4243,7 @@ const toolCallRenderer = {
4061
4243
  relabel: (element, segment) => {
4062
4244
  if (isPlaceholder(element)) return;
4063
4245
  const cfg = contextConfig();
4064
- const custom = cfg.getToolRenderer(segment.toolCall?.name);
4246
+ const custom = drawerOf(element, cfg.getToolRenderer(segment.toolCall?.name) ?? null);
4065
4247
  if (custom) {
4066
4248
  custom.relabel?.(element, segment);
4067
4249
  return;
@@ -4081,8 +4263,8 @@ const toolCallRenderer = {
4081
4263
  for (const button of element.querySelectorAll(".aparte-tool-copy")) relabelCopyButton(button);
4082
4264
  const appDetail = appDetailOf(element);
4083
4265
  if (appDetail) {
4084
- const behind = behindOf(segment);
4085
- if (behind.kind === "body") relabelBody(appDetail, behind.body);
4266
+ const behind2 = behindOf(segment);
4267
+ if (behind2.kind === "body") relabelBody(appDetail, behind2.body);
4086
4268
  }
4087
4269
  const statusEl = element.querySelector(".aparte-tool-state");
4088
4270
  if (statusEl) statusEl.innerHTML = stateBadge(segment);
@@ -4108,9 +4290,10 @@ const toolCallRenderer = {
4108
4290
  element.setAttribute("data-status", status);
4109
4291
  return;
4110
4292
  }
4111
- const custom = contextConfig().getToolRenderer(segment.toolCall?.name);
4112
- if (custom?.update && !hidden && !isPlaceholder(element)) {
4113
- custom.update(element, segment);
4293
+ const custom = hidden ? null : contextConfig().getToolRenderer(segment.toolCall?.name) ?? null;
4294
+ const drawn = isPlaceholder(element) ? null : drawerOf(element, custom);
4295
+ if (drawn && drawn === custom && drawn.update && !isPlaceholder(element)) {
4296
+ drawn.update(element, segment);
4114
4297
  return;
4115
4298
  }
4116
4299
  const rebuild = (out) => {
@@ -4119,21 +4302,22 @@ const toolCallRenderer = {
4119
4302
  const rebuilt = typeof out === "string" ? tpl.content.firstElementChild : out;
4120
4303
  if (rebuilt) {
4121
4304
  if (!rebuilt.hasAttribute("data-segment-id")) rebuilt.setAttribute("data-segment-id", segment.id);
4305
+ toolCallRenderer.cleanup?.(element, segment);
4122
4306
  element.replaceWith(rebuilt);
4123
4307
  toolCallRenderer.setup?.(rebuilt, segment);
4124
4308
  }
4125
4309
  };
4126
- if (custom || hidden || isPlaceholder(element)) {
4310
+ if (custom || drawn || hidden || isPlaceholder(element)) {
4127
4311
  rebuild(toolCallRenderer.render(segment));
4128
4312
  return;
4129
4313
  }
4130
- const behind = behindOf(segment);
4131
- const input = behind.kind === "parts" ? describeInput(segment) : "";
4132
- const outcome = behind.kind === "parts" ? outcomeOf(segment) : null;
4133
- const wantsDetail = behind.kind === "body" || !!input || !!outcome;
4314
+ const behind2 = behindOf(segment);
4315
+ const input = behind2.kind === "parts" ? describeInput(segment) : "";
4316
+ const outcome = behind2.kind === "parts" ? outcomeOf(segment) : null;
4317
+ const wantsDetail = behind2.kind === "body" || !!input || !!outcome;
4134
4318
  const holdsAppDetail = !!appDetailOf(element);
4135
- if (wantsDetail !== (element.tagName === "DETAILS") || wantsDetail && behind.kind === "body" !== holdsAppDetail) {
4136
- rebuild(drawDefault(segment, behind));
4319
+ if (wantsDetail !== (element.tagName === "DETAILS") || wantsDetail && behind2.kind === "body" !== holdsAppDetail) {
4320
+ rebuild(drawDefault(segment, behind2));
4137
4321
  return;
4138
4322
  }
4139
4323
  element.setAttribute("data-status", status);
@@ -4143,8 +4327,8 @@ const toolCallRenderer = {
4143
4327
  setName(element, segment);
4144
4328
  setTarget(element, segment);
4145
4329
  setNote(element, segment);
4146
- if (behind.kind === "body") {
4147
- setBody(element, behind.body);
4330
+ if (behind2.kind === "body") {
4331
+ setBody(element, behind2.body);
4148
4332
  return;
4149
4333
  }
4150
4334
  setPart(element, "input", input, "toolInput");
@@ -4152,12 +4336,31 @@ const toolCallRenderer = {
4152
4336
  highlightParts(element);
4153
4337
  },
4154
4338
  setup: (element, segment) => {
4339
+ const hidden = contextConfig().isToolHidden(segment.toolCall?.name ?? "tool");
4340
+ const customRenderer = hidden ? void 0 : contextConfig().getToolRenderer(segment.toolCall?.name);
4341
+ if (customRenderer) behind.set(element, customRenderer);
4155
4342
  if (isPlaceholder(element)) return;
4156
4343
  wireParts(element);
4157
4344
  highlightParts(element);
4158
- const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
4345
+ if (customRenderer) drawnBy.set(element, customRenderer);
4159
4346
  customRenderer?.setup?.(element, segment);
4160
4347
  },
4348
+ /**
4349
+ * The row leaves for good: the registered renderer that drew it, if any, cleans up. A
4350
+ * cleanup that throws is reported and goes no further: the row is replaced or removed
4351
+ * all the same, and the stream that caused it is not interrupted.
4352
+ */
4353
+ cleanup: (element, segment) => {
4354
+ behind.delete(element);
4355
+ const custom = drawnBy.get(element);
4356
+ if (!custom) return;
4357
+ drawnBy.delete(element);
4358
+ try {
4359
+ custom.cleanup?.(element, segment);
4360
+ } catch (err) {
4361
+ console.error("[aparte] a tool renderer's cleanup threw", err);
4362
+ }
4363
+ },
4161
4364
  // CSS lives in styles/segment/ — see that folder for why a
4162
4365
  // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
4163
4366
  // has no other way onto the page.
@@ -4181,8 +4384,51 @@ function effectiveRenderers(config) {
4181
4384
  }
4182
4385
  let styleElement = null;
4183
4386
  function registerSegmentRenderer(renderer, config = contextConfig()) {
4184
- registryFor(config).renderers.set(renderer.type, renderer);
4387
+ const reg = registryFor(config);
4388
+ if (reg.renderers.get(renderer.type) === renderer) return;
4389
+ reg.renderers.set(renderer.type, renderer);
4185
4390
  injectRendererStyles(config);
4391
+ notifyRenderersChanged(config);
4392
+ }
4393
+ const overrideStacks = /* @__PURE__ */ new WeakMap();
4394
+ function overrideSegmentRenderer(renderer, config = contextConfig()) {
4395
+ const type = renderer.type;
4396
+ const reg = registryFor(config);
4397
+ let byType = overrideStacks.get(config);
4398
+ if (!byType) {
4399
+ byType = /* @__PURE__ */ new Map();
4400
+ overrideStacks.set(config, byType);
4401
+ }
4402
+ const current = reg.renderers.get(type);
4403
+ let stack = byType.get(type);
4404
+ if (!stack) {
4405
+ stack = [{ renderer: current }];
4406
+ byType.set(type, stack);
4407
+ } else if (stack[stack.length - 1].renderer !== current) stack.splice(0, stack.length, { renderer: current });
4408
+ const entry = { renderer };
4409
+ stack.push(entry);
4410
+ registerSegmentRenderer(renderer, config);
4411
+ let restored = false;
4412
+ return () => {
4413
+ if (restored) return;
4414
+ restored = true;
4415
+ const i = stack.indexOf(entry);
4416
+ if (i <= 0) return;
4417
+ const wasTop = i === stack.length - 1;
4418
+ stack.splice(i, 1);
4419
+ if (stack.length === 1 && byType.get(type) === stack) byType.delete(type);
4420
+ if (!wasTop || reg.renderers.get(type) !== renderer) return;
4421
+ const top = stack[stack.length - 1].renderer;
4422
+ if (top) {
4423
+ registerSegmentRenderer(top, config);
4424
+ return;
4425
+ }
4426
+ unregisterSegmentRenderer(type, config);
4427
+ if (!effectiveRenderers(config).has(type) && reg.defaultsInstalled && !reg.defaultsDeclined) {
4428
+ const builtIn = DEFAULT_RENDERERS.find((r) => r.type === type);
4429
+ if (builtIn) registerSegmentRenderer(builtIn, config);
4430
+ }
4431
+ };
4186
4432
  }
4187
4433
  function declineDefaultRenderers(config = contextConfig()) {
4188
4434
  registryFor(config).defaultsDeclined = true;
@@ -4197,7 +4443,10 @@ function installDefaultRenderersOnce(config = contextConfig()) {
4197
4443
  injectRendererStyles(config);
4198
4444
  }
4199
4445
  function unregisterSegmentRenderer(type, config = contextConfig()) {
4200
- registryFor(config).renderers.delete(type);
4446
+ const reg = registryFor(config);
4447
+ if (!reg.renderers.has(type)) return;
4448
+ reg.renderers.delete(type);
4449
+ notifyRenderersChanged(config);
4201
4450
  }
4202
4451
  function getSegmentRenderer(type, config = contextConfig()) {
4203
4452
  return effectiveRenderers(config).get(type);
@@ -4257,6 +4506,71 @@ const DEFAULT_RENDERERS = [
4257
4506
  errorRenderer,
4258
4507
  toolCallRenderer
4259
4508
  ];
4509
+ const mountedRenderers = /* @__PURE__ */ new WeakSet();
4510
+ function isMountedRenderer(renderer) {
4511
+ return !!renderer && mountedRenderers.has(renderer);
4512
+ }
4513
+ function toToolCallView(segment) {
4514
+ return {
4515
+ id: segment.toolCall?.id ?? segment.id,
4516
+ name: segment.toolCall?.name ?? "tool",
4517
+ input: segment.toolCall?.input,
4518
+ status: segment.status ?? "pending",
4519
+ result: segment.result,
4520
+ structuredResult: segment.structuredResult,
4521
+ deferred: segment.deferred,
4522
+ refusal: segment.refusal,
4523
+ abortCause: segment.abortCause,
4524
+ segment
4525
+ };
4526
+ }
4527
+ function host(className, segmentId) {
4528
+ const el2 = document.createElement("div");
4529
+ el2.className = className;
4530
+ el2.setAttribute("data-segment-id", segmentId);
4531
+ return el2;
4532
+ }
4533
+ function defineMountedToolRenderer(mount) {
4534
+ const mounted = /* @__PURE__ */ new WeakMap();
4535
+ const renderer = {
4536
+ render: (segment) => host("aparte-tool-ui", segment.id),
4537
+ setup: (el2, segment) => {
4538
+ if (!mounted.has(el2)) mounted.set(el2, mount(el2, toToolCallView(segment)));
4539
+ },
4540
+ update: (el2, segment) => {
4541
+ mounted.get(el2)?.update(toToolCallView(segment));
4542
+ },
4543
+ cleanup: (el2) => {
4544
+ const view = mounted.get(el2);
4545
+ if (!view) return;
4546
+ mounted.delete(el2);
4547
+ view.destroy();
4548
+ }
4549
+ };
4550
+ mountedRenderers.add(renderer);
4551
+ return renderer;
4552
+ }
4553
+ function defineMountedSegmentRenderer(type, mount) {
4554
+ const mounted = /* @__PURE__ */ new WeakMap();
4555
+ const renderer = {
4556
+ type,
4557
+ render: (segment) => host("aparte-segment-ui", segment.id),
4558
+ setup: (el2, segment) => {
4559
+ if (!mounted.has(el2)) mounted.set(el2, mount(el2, segment));
4560
+ },
4561
+ update: (el2, segment) => {
4562
+ mounted.get(el2)?.update(segment);
4563
+ },
4564
+ cleanup: (el2) => {
4565
+ const view = mounted.get(el2);
4566
+ if (!view) return;
4567
+ mounted.delete(el2);
4568
+ view.destroy();
4569
+ }
4570
+ };
4571
+ mountedRenderers.add(renderer);
4572
+ return renderer;
4573
+ }
4260
4574
  function populateBubbleFromMessage(bubble, message, siblingInfo) {
4261
4575
  if (message.segments && message.segments.length > 0) {
4262
4576
  const existing = bubble.getSegments?.() ?? [];
@@ -4284,13 +4598,25 @@ function populateBubbleFromMessage(bubble, message, siblingInfo) {
4284
4598
  if (message.usage !== void 0) {
4285
4599
  bubble.setUsage?.(message.usage);
4286
4600
  }
4601
+ bubble.setFinishReason?.(message.finishReason);
4602
+ bubble.setContextSummary?.(message.contextSummary, { legacy: message.compaction === true });
4287
4603
  }
4288
4604
  const APARTE_CONVERSATION_SCHEMA_VERSION = 2;
4289
4605
  const APARTE_DERIVED_SEGMENTS = Symbol.for("aparte.derivedSegments");
4290
4606
  function applyRetention(messages, tree, maxMessages) {
4291
4607
  if (maxMessages <= 0 || messages.length <= maxMessages) return { messages, tree };
4292
- const trimmedFlat = messages.slice(messages.length - maxMessages);
4293
- const cutoffId = trimmedFlat[0]?.id;
4608
+ const cutIndex = messages.length - maxMessages;
4609
+ const trimmedFlat = messages.slice(cutIndex);
4610
+ const first = trimmedFlat[0];
4611
+ const cutoffId = first?.id;
4612
+ let inherited;
4613
+ for (let i = messages.length - 1; i >= 0; i--) {
4614
+ const own = messages[i].contextSummary;
4615
+ if (!own) continue;
4616
+ if (i < cutIndex) inherited = { ...own, count: own.count + (cutIndex - i) };
4617
+ break;
4618
+ }
4619
+ if (first && inherited) trimmedFlat[0] = { ...first, contextSummary: inherited };
4294
4620
  if (!cutoffId || !tree) return { messages: trimmedFlat, tree };
4295
4621
  const treeIds = new Set(tree.messages.map((m) => m.message.id));
4296
4622
  if (!treeIds.has(cutoffId)) return { messages: trimmedFlat, tree };
@@ -4312,7 +4638,7 @@ function applyRetention(messages, tree, maxMessages) {
4312
4638
  }
4313
4639
  const trimmedTree = {
4314
4640
  headId: tree.headId,
4315
- messages: tree.messages.filter((m) => keep.has(m.message.id)).map((m) => m.message.id === cutoffId ? { message: m.message, parentId: null } : m)
4641
+ messages: tree.messages.filter((m) => keep.has(m.message.id)).map((m) => m.message.id === cutoffId ? { message: inherited ? { ...m.message, contextSummary: inherited } : m.message, parentId: null } : m)
4316
4642
  };
4317
4643
  return { messages: trimmedFlat, tree: trimmedTree };
4318
4644
  }
@@ -4343,6 +4669,7 @@ function treeContentOf(tree) {
4343
4669
  const byId = [...tree.messages].sort((a, b) => a.message.id < b.message.id ? -1 : a.message.id > b.message.id ? 1 : 0);
4344
4670
  return stableStringify(byId.map((entry) => ({ parentId: entry.parentId, message: asContent(entry.message) })));
4345
4671
  }
4672
+ const isTitleSource = (m) => m.role === "user" && !m.compaction;
4346
4673
  class AparteConversationManager {
4347
4674
  _adapter;
4348
4675
  _conversations = [];
@@ -4551,7 +4878,7 @@ class AparteConversationManager {
4551
4878
  async _append(convId, msg) {
4552
4879
  const conv = await this._full(convId);
4553
4880
  if (!conv) return;
4554
- const isFirstUserMsg = msg.role === "user" && conv.messages.every((m) => m.role !== "user");
4881
+ const isFirstUserMsg = msg.role === "user" && !msg.compaction && conv.messages.every((m) => m.role !== "user" || m.compaction);
4555
4882
  const title = isFirstUserMsg ? await this._title(msg) : null;
4556
4883
  const fresh = this._find(convId);
4557
4884
  if (!fresh) return;
@@ -4579,7 +4906,7 @@ class AparteConversationManager {
4579
4906
  console.warn(`[AparteConversationManager] updateMessages('${convId}') before its messages were loaded: ignored.`);
4580
4907
  return;
4581
4908
  }
4582
- const firstUser = messages.find((m) => m.role === "user");
4909
+ const firstUser = messages.find(isTitleSource);
4583
4910
  if (this._retention) {
4584
4911
  ({ messages, tree } = applyRetention(messages, tree, this._retention.maxMessages));
4585
4912
  }
@@ -4600,7 +4927,7 @@ class AparteConversationManager {
4600
4927
  }
4601
4928
  let title;
4602
4929
  if (conv.autoTitle) {
4603
- const before = conv.messages.find((m) => m.role === "user");
4930
+ const before = conv.messages.find(isTitleSource);
4604
4931
  if (firstUser && (firstUser.content ?? "") !== (before?.content ?? "")) {
4605
4932
  title = await this._title(firstUser);
4606
4933
  }
@@ -4902,7 +5229,7 @@ class AparteConversationController {
4902
5229
  * Attach event listeners to the binding host. Returns an unbind function.
4903
5230
  */
4904
5231
  bind() {
4905
- const host = this._binding.host;
5232
+ const host2 = this._binding.host;
4906
5233
  this._onSendCapture = (e) => {
4907
5234
  const evt = e;
4908
5235
  const targetId = evt.detail?.targetId;
@@ -4921,13 +5248,13 @@ class AparteConversationController {
4921
5248
  evt.detail.echoed = true;
4922
5249
  void this._ensureConversationAndPersist(userMsg);
4923
5250
  };
4924
- host.addEventListener("aparte-send", this._onSendCapture, { capture: true });
5251
+ host2.addEventListener("aparte-send", this._onSendCapture, { capture: true });
4925
5252
  this._onPathChanged = () => {
4926
5253
  if (!this._isLoadingConversation) {
4927
5254
  this._persistActive();
4928
5255
  }
4929
5256
  };
4930
- host.addEventListener("aparte-path-changed", this._onPathChanged);
5257
+ host2.addEventListener("aparte-path-changed", this._onPathChanged);
4931
5258
  this._onMessageStart = (e) => {
4932
5259
  this._isStreaming = true;
4933
5260
  this._turnMessage = e.detail?.messageId ?? null;
@@ -4958,12 +5285,12 @@ class AparteConversationController {
4958
5285
  this._onMessageResumed = (e) => {
4959
5286
  if (this._ofThisTurn(e)) this._setTurn("running");
4960
5287
  };
4961
- host.addEventListener("aparte-message-start", this._onMessageStart);
4962
- host.addEventListener("aparte-message-done", this._onMessageDone);
4963
- host.addEventListener("aparte-message-error", this._onMessageError);
4964
- host.addEventListener("aparte-message-aborted", this._onMessageAborted);
4965
- host.addEventListener("aparte-message-paused", this._onMessagePaused);
4966
- host.addEventListener("aparte-message-resumed", this._onMessageResumed);
5288
+ host2.addEventListener("aparte-message-start", this._onMessageStart);
5289
+ host2.addEventListener("aparte-message-done", this._onMessageDone);
5290
+ host2.addEventListener("aparte-message-error", this._onMessageError);
5291
+ host2.addEventListener("aparte-message-aborted", this._onMessageAborted);
5292
+ host2.addEventListener("aparte-message-paused", this._onMessagePaused);
5293
+ host2.addEventListener("aparte-message-resumed", this._onMessageResumed);
4967
5294
  this._onSelectConversation = (e) => {
4968
5295
  const evt = e;
4969
5296
  const targetId = evt.detail?.targetId;
@@ -4973,7 +5300,7 @@ class AparteConversationController {
4973
5300
  this._onCompactDone = (e) => {
4974
5301
  const detail = e.detail;
4975
5302
  if (detail?.targetId && detail.targetId !== this._binding.hostId) return;
4976
- queueMicrotask(() => this._persistOnceSummarised(detail?.summary, 0));
5303
+ queueMicrotask(() => this._persistOnceSummarised(detail ?? {}, 0));
4977
5304
  };
4978
5305
  if (typeof window !== "undefined") {
4979
5306
  window.addEventListener("aparte-conversation-select", this._onSelectConversation);
@@ -4995,37 +5322,37 @@ class AparteConversationController {
4995
5322
  this._turnMessage = null;
4996
5323
  this._stopHydrationRetry?.();
4997
5324
  this._stopHydrationRetry = null;
4998
- const host = this._binding.host;
5325
+ const host2 = this._binding.host;
4999
5326
  if (this._onSendCapture) {
5000
- host.removeEventListener("aparte-send", this._onSendCapture, { capture: true });
5327
+ host2.removeEventListener("aparte-send", this._onSendCapture, { capture: true });
5001
5328
  this._onSendCapture = null;
5002
5329
  }
5003
5330
  if (this._onPathChanged) {
5004
- host.removeEventListener("aparte-path-changed", this._onPathChanged);
5331
+ host2.removeEventListener("aparte-path-changed", this._onPathChanged);
5005
5332
  this._onPathChanged = null;
5006
5333
  }
5007
5334
  if (this._onMessageStart) {
5008
- host.removeEventListener("aparte-message-start", this._onMessageStart);
5335
+ host2.removeEventListener("aparte-message-start", this._onMessageStart);
5009
5336
  this._onMessageStart = null;
5010
5337
  }
5011
5338
  if (this._onMessageDone) {
5012
- host.removeEventListener("aparte-message-done", this._onMessageDone);
5339
+ host2.removeEventListener("aparte-message-done", this._onMessageDone);
5013
5340
  this._onMessageDone = null;
5014
5341
  }
5015
5342
  if (this._onMessageError) {
5016
- host.removeEventListener("aparte-message-error", this._onMessageError);
5343
+ host2.removeEventListener("aparte-message-error", this._onMessageError);
5017
5344
  this._onMessageError = null;
5018
5345
  }
5019
5346
  if (this._onMessageAborted) {
5020
- host.removeEventListener("aparte-message-aborted", this._onMessageAborted);
5347
+ host2.removeEventListener("aparte-message-aborted", this._onMessageAborted);
5021
5348
  this._onMessageAborted = null;
5022
5349
  }
5023
5350
  if (this._onMessagePaused) {
5024
- host.removeEventListener("aparte-message-paused", this._onMessagePaused);
5351
+ host2.removeEventListener("aparte-message-paused", this._onMessagePaused);
5025
5352
  this._onMessagePaused = null;
5026
5353
  }
5027
5354
  if (this._onMessageResumed) {
5028
- host.removeEventListener("aparte-message-resumed", this._onMessageResumed);
5355
+ host2.removeEventListener("aparte-message-resumed", this._onMessageResumed);
5029
5356
  this._onMessageResumed = null;
5030
5357
  }
5031
5358
  if (this._onCompactDone && typeof window !== "undefined") {
@@ -5178,20 +5505,40 @@ class AparteConversationController {
5178
5505
  * that was partially written before a switch or component destruction.
5179
5506
  */
5180
5507
  /**
5181
- * Save once the binding holds the summary a compaction announced. A transcript kept in
5182
- * a framework store reaches the binding only when that framework renders, so a save on
5183
- * the microtask wrote the conversation as it stood before the compaction. Bounded: a
5184
- * run with no summary (skipped), or a binding that never shows it, is saved as it is.
5185
- */
5186
- _persistOnceSummarised(summary, waits) {
5187
- const holds = !summary || this._binding.getMessages().some(
5188
- (m) => m.compaction === true && typeof m.content === "string" && m.content.endsWith(summary)
5189
- );
5508
+ * Save once the binding holds what a compaction (or a Restore) announced. A transcript
5509
+ * kept in a framework store reaches the binding only when that framework renders, so a
5510
+ * save on the microtask wrote the conversation as it stood before the change. Bounded:
5511
+ * a run that changed nothing (skipped), or a binding that never shows the change, is
5512
+ * saved as it is.
5513
+ *
5514
+ * What "holds" means follows what the run said it did:
5515
+ * - a checkpoint — `messageId` names the message carrying it, and it holds once that
5516
+ * message carries a `contextSummary` with its `createdAt` (a summary without the
5517
+ * stamp is a half-written one);
5518
+ * - a Restore — it holds once no message of the path carries a checkpoint any more;
5519
+ * - an older host's summary MESSAGE (`compaction: true`, ending with the summary).
5520
+ * A run that names none of these has nothing to wait for.
5521
+ */
5522
+ _persistOnceSummarised(detail, waits) {
5523
+ const messages = this._binding.getMessages();
5524
+ let holds = true;
5525
+ if (detail.restored) {
5526
+ holds = !messages.some((m) => m.contextSummary !== void 0);
5527
+ } else if (detail.messageId || detail.summary) {
5528
+ const { messageId, summary } = detail;
5529
+ const checkpointed = !!messageId && messages.some(
5530
+ (m) => m.id === messageId && typeof m.contextSummary?.createdAt === "number"
5531
+ );
5532
+ const summaryMessage = !!summary && messages.some(
5533
+ (m) => m.compaction === true && typeof m.content === "string" && m.content.endsWith(summary)
5534
+ );
5535
+ holds = checkpointed || summaryMessage;
5536
+ }
5190
5537
  if (holds || !this._binding.afterRender || waits >= MAX_COMPACTION_SAVE_WAITS) {
5191
5538
  this._persistActive();
5192
5539
  return;
5193
5540
  }
5194
- this._binding.afterRender(() => this._persistOnceSummarised(summary, waits + 1));
5541
+ this._binding.afterRender(() => this._persistOnceSummarised(detail, waits + 1));
5195
5542
  }
5196
5543
  _persistActive() {
5197
5544
  if (!this._activeId) return;
@@ -5269,16 +5616,126 @@ function gateGlide(el2) {
5269
5616
  el2.addEventListener("aparte-send", lift);
5270
5617
  return off;
5271
5618
  }
5619
+ function chatIdOf(el2) {
5620
+ let node = el2.parentElement;
5621
+ while (node) {
5622
+ const isHost = node.tagName.toLowerCase() === "aparte-chat" || node.hasAttribute("data-aparte-chat");
5623
+ if (isHost && node.id) return node.id;
5624
+ node = node.parentElement;
5625
+ }
5626
+ return void 0;
5627
+ }
5628
+ class CheckpointStateWatcher {
5629
+ constructor(_options) {
5630
+ this._options = _options;
5631
+ }
5632
+ _messageId = null;
5633
+ _state = null;
5634
+ _reason = "";
5635
+ /** The bubble the attributes were last written to, so they can be taken off it even after it left the lookup. */
5636
+ _marked = null;
5637
+ /** Listen on `window` for the three events; returns the function that stops. */
5638
+ start() {
5639
+ const mine = (targetId) => {
5640
+ if (this._options.active && !this._options.active()) return false;
5641
+ return !targetId || targetId === this._options.ownId();
5642
+ };
5643
+ const mineMessage = (messageId) => !!messageId && this._options.owns(messageId);
5644
+ const onStart = (e) => {
5645
+ const detail = e.detail;
5646
+ if (!mine(detail?.targetId) || !mineMessage(detail?.messageId)) return;
5647
+ this.clear();
5648
+ this._set(detail.messageId, "pending", "");
5649
+ };
5650
+ const onDone = (e) => {
5651
+ const detail = e.detail;
5652
+ if (!mine(detail?.targetId) || detail?.skipped) return;
5653
+ if (detail?.messageId && detail.messageId === this._messageId) this.clear();
5654
+ };
5655
+ const onError = (e) => {
5656
+ const detail = e.detail;
5657
+ if (!mine(detail?.targetId) || !mineMessage(detail?.messageId)) return;
5658
+ if (detail.aborted) {
5659
+ if (detail.messageId === this._messageId) this.clear();
5660
+ return;
5661
+ }
5662
+ if (detail.messageId !== this._messageId) this.clear();
5663
+ this._set(detail.messageId, "failed", detail.error ?? "");
5664
+ };
5665
+ window.addEventListener("aparte-compact-start", onStart);
5666
+ window.addEventListener("aparte-compact-done", onDone);
5667
+ window.addEventListener("aparte-compact-error", onError);
5668
+ return () => {
5669
+ window.removeEventListener("aparte-compact-start", onStart);
5670
+ window.removeEventListener("aparte-compact-done", onDone);
5671
+ window.removeEventListener("aparte-compact-error", onError);
5672
+ };
5673
+ }
5674
+ /** Take the state off the bubble and forget it. */
5675
+ clear() {
5676
+ const bubble = this._marked ?? (this._messageId ? this._options.bubbleOf(this._messageId) : null);
5677
+ this._messageId = null;
5678
+ this._state = null;
5679
+ this._reason = "";
5680
+ this._marked = null;
5681
+ bubble?.removeAttribute("data-checkpoint");
5682
+ bubble?.removeAttribute("data-checkpoint-reason");
5683
+ }
5684
+ /** Write the remembered state onto the bubble, which may have been created since. */
5685
+ reapply() {
5686
+ if (!this._messageId || !this._state) return;
5687
+ this._write(this._messageId, this._state, this._reason);
5688
+ }
5689
+ _set(messageId, state, reason) {
5690
+ this._messageId = messageId;
5691
+ this._state = state;
5692
+ this._reason = reason;
5693
+ this._write(messageId, state, reason);
5694
+ }
5695
+ _write(messageId, state, reason) {
5696
+ const bubble = this._options.bubbleOf(messageId);
5697
+ if (!bubble) return;
5698
+ if (this._marked && this._marked !== bubble) {
5699
+ this._marked.removeAttribute("data-checkpoint");
5700
+ this._marked.removeAttribute("data-checkpoint-reason");
5701
+ }
5702
+ this._marked = bubble;
5703
+ if (reason) bubble.setAttribute("data-checkpoint-reason", reason);
5704
+ else bubble.removeAttribute("data-checkpoint-reason");
5705
+ if (bubble.getAttribute("data-checkpoint") !== state) bubble.setAttribute("data-checkpoint", state);
5706
+ }
5707
+ }
5272
5708
  const MAX_SIBLING_RETRIES = 6;
5273
5709
  const TREE_OWNED_FIELDS = /* @__PURE__ */ new Set(["id", "branches", "activeBranchIndex", "isStreaming"]);
5274
5710
  const ALWAYS_PRESENT_FIELDS = /* @__PURE__ */ new Set(["content", "segments", "status"]);
5711
+ const RENDERED_FIELDS = ["content", "segments", "attachments", "usage", "finishReason", "contextSummary", "compaction"];
5275
5712
  class AparteChatHost {
5276
5713
  binding;
5277
5714
  options;
5278
5715
  /** Id of the assistant message currently streaming (null when idle). */
5279
5716
  _streamingId = null;
5280
- /** Message ids already reconciled onto a bubble (skip-rerender cache). */
5281
- _renderedIds = /* @__PURE__ */ new Set();
5717
+ /**
5718
+ * The message object each bubble was last drawn from, by id (skip-rerender cache).
5719
+ *
5720
+ * An id alone could not say whether the list had changed under a bubble: a framework
5721
+ * that owns the transcript (a controlled `messages` prop, a v-model, a store behind a
5722
+ * compaction target) never goes through {@link updateMessage}, it hands over a list
5723
+ * with a NEW object for the message it changed, and a checkpoint written that way to a
5724
+ * message that was not the last never reached its bubble. Immutable updates always
5725
+ * produce a new object, so the reference is the signal; the same object again means
5726
+ * nothing changed and the bubble is left alone.
5727
+ */
5728
+ _renderedIds = /* @__PURE__ */ new Map();
5729
+ /**
5730
+ * Says on the bubble what a compaction is doing to the message it will sit on. The
5731
+ * viewport answers for this in a chat with no framework; here the framework owns the
5732
+ * list and the host is the one that knows which bubble is whose.
5733
+ */
5734
+ _checkpointState = new CheckpointStateWatcher({
5735
+ ownId: () => this.binding.hostId,
5736
+ bubbleOf: (id) => this._bubbleById(id),
5737
+ owns: (id) => this.binding.getMessages().some((m) => m.id === id)
5738
+ });
5282
5739
  /** Sibling info from the last path-change, applied after the next render. */
5283
5740
  _pendingSiblings;
5284
5741
  /** How many render passes the sibling write has waited for — see `_applyPendingSiblings`. */
@@ -5302,6 +5759,12 @@ class AparteChatHost {
5302
5759
  _tokenBuffer = null;
5303
5760
  /** Handle of the scheduled coalesced sync, or null when none is pending. */
5304
5761
  _flushHandle = null;
5762
+ /**
5763
+ * Lists reported through `onMessagesChange` that the parent has not handed back yet, oldest
5764
+ * first — what {@link takeEcho} recognises. Bounded: a parent that never hands a list back
5765
+ * (an uncontrolled chat) must not make it grow for the life of the page.
5766
+ */
5767
+ _pendingEchoes = [];
5305
5768
  _conversationId = null;
5306
5769
  _controller;
5307
5770
  _teardown = [];
@@ -5311,6 +5774,34 @@ class AparteChatHost {
5311
5774
  this.options = options;
5312
5775
  this._conversationId = options.conversationId ?? null;
5313
5776
  }
5777
+ /** Every report goes through here, so {@link takeEcho} knows what the parent may hand back. */
5778
+ _report(next) {
5779
+ this._pendingEchoes.push(next);
5780
+ if (this._pendingEchoes.length > 64) this._pendingEchoes.shift();
5781
+ this.binding.onMessagesChange?.(next);
5782
+ }
5783
+ /**
5784
+ * Whether a list the parent hands back is this host's own report coming back — the echo of
5785
+ * `onMessagesChange` through a two-way binding (`bind:messages`, `v-model:messages`,
5786
+ * `[(messages)]`, a controlled React prop). A wrapper asks before applying the list it is
5787
+ * given, because applying an echo rolls back what the host did since without reporting (an
5788
+ * optimistic append).
5789
+ *
5790
+ * True for a list reported and not handed back yet, however late it lands — a parent whose
5791
+ * render comes after the host's next write hands back an EARLIER report — and that list and
5792
+ * every older one are taken. False for any other list, the parent's own word, which clears
5793
+ * them: a list the parent restores later (a reopened conversation, an undo) is applied,
5794
+ * where remembering every list ever reported swallowed it.
5795
+ */
5796
+ takeEcho(list) {
5797
+ const i = this._pendingEchoes.indexOf(list);
5798
+ if (i < 0) {
5799
+ this._pendingEchoes.length = 0;
5800
+ return false;
5801
+ }
5802
+ this._pendingEchoes.splice(0, i + 1);
5803
+ return true;
5804
+ }
5314
5805
  // ── lifecycle ──────────────────────────────────────────────────────────
5315
5806
  /**
5316
5807
  * Install the host-method surface + event listeners and create the
@@ -5320,19 +5811,20 @@ class AparteChatHost {
5320
5811
  bind() {
5321
5812
  if (this._bound) return () => this.unbind();
5322
5813
  this._bound = true;
5323
- const host = this.binding.host;
5324
- if (!host.id) host.id = this.binding.hostId;
5814
+ const host2 = this.binding.host;
5815
+ if (!host2.id) host2.id = this.binding.hostId;
5325
5816
  if (this.options.config) attachConfig(this.binding.host, this.options.config);
5326
5817
  const vp = this._vp();
5327
5818
  vp?.setFrameworkManagedDOM?.(true);
5328
5819
  if (this.options.layoutTransitionMs && this.options.layoutTransitionMs > 0) {
5329
5820
  vp?.configure?.({ layoutTransitionMs: this.options.layoutTransitionMs });
5330
5821
  }
5331
- this._installHostMethods(host);
5332
- this._installLifecycleListeners(host);
5333
- this._teardown.push(gateGlide(host));
5822
+ this._installHostMethods(host2);
5823
+ this._installLifecycleListeners(host2);
5824
+ this._teardown.push(this._checkpointState.start());
5825
+ this._teardown.push(gateGlide(host2));
5334
5826
  this._installViewportListeners();
5335
- this._createController(host);
5827
+ this._createController(host2);
5336
5828
  return () => this.unbind();
5337
5829
  }
5338
5830
  unbind() {
@@ -5378,10 +5870,10 @@ class AparteChatHost {
5378
5870
  return this._controller?.setConversationId(id) ?? Promise.resolve();
5379
5871
  }
5380
5872
  _unbindController;
5381
- _createController(host) {
5873
+ _createController(host2) {
5382
5874
  const convBinding = {
5383
5875
  hostId: this.binding.hostId,
5384
- host,
5876
+ host: host2,
5385
5877
  // Controller-driven flushes mirror AparteConversationManager state, not
5386
5878
  // user intent — deliberately NOT emitted as `messagesChange` to
5387
5879
  // avoid a feedback loop with hosts using a two-way `messages` input.
@@ -5443,6 +5935,7 @@ class AparteChatHost {
5443
5935
  this._setStreamingId(null);
5444
5936
  this.binding.onTypingChange?.(false);
5445
5937
  this._renderedIds.clear();
5938
+ this._checkpointState.clear();
5446
5939
  this.binding.resetComposer?.();
5447
5940
  }
5448
5941
  // ── host method surface (also installed on the host element) ───────────
@@ -5488,10 +5981,17 @@ class AparteChatHost {
5488
5981
  const merged = { ...next[index], ...updates };
5489
5982
  next[index] = merged;
5490
5983
  this.binding.setMessages(next);
5491
- this.binding.onMessagesChange?.(next);
5984
+ this._report(next);
5492
5985
  if (updates.status) {
5493
5986
  this._bubbleById(messageId)?.updateMessage?.({ status: updates.status });
5494
5987
  }
5988
+ if (index !== msgs.length - 1 && RENDERED_FIELDS.some((key) => key in updates)) {
5989
+ this._renderedIds.delete(messageId);
5990
+ const bubble = this._bubbleById(messageId);
5991
+ if (bubble && ("contextSummary" in updates || "compaction" in updates)) {
5992
+ bubble.setContextSummary?.(merged.contextSummary, { legacy: merged.compaction === true });
5993
+ }
5994
+ }
5495
5995
  if (isTerminalStatus(merged.status) && merged.segments) {
5496
5996
  for (const id of openSegmentIds(merged.segments)) {
5497
5997
  this.updateSegment(id, { isStreaming: false });
@@ -5508,7 +6008,7 @@ class AparteChatHost {
5508
6008
  const newContent = options?.append ? (last.content || "") + content : content;
5509
6009
  const next = [...msgs.slice(0, -1), { ...last, content: newContent }];
5510
6010
  this.binding.setMessages(next);
5511
- this.binding.onMessagesChange?.(next);
6011
+ this._report(next);
5512
6012
  }
5513
6013
  /** Add a segment to the last message (immediate bubble + state sync). */
5514
6014
  addSegment(segment) {
@@ -5533,7 +6033,7 @@ class AparteChatHost {
5533
6033
  const segments = [...last.segments || [], stamped];
5534
6034
  const next = [...msgs.slice(0, -1), { ...last, segments }];
5535
6035
  this.binding.setMessages(next);
5536
- this.binding.onMessagesChange?.(next);
6036
+ this._report(next);
5537
6037
  }
5538
6038
  /** Update a segment in the last message in place. */
5539
6039
  updateSegment(segmentId, updates) {
@@ -5550,7 +6050,7 @@ class AparteChatHost {
5550
6050
  );
5551
6051
  const next = [...msgs.slice(0, -1), { ...last, segments }];
5552
6052
  this.binding.setMessages(next);
5553
- this.binding.onMessagesChange?.(next);
6053
+ this._report(next);
5554
6054
  }
5555
6055
  /** Remove a transient segment (a status row the host added itself). */
5556
6056
  removeSegment(segmentId) {
@@ -5564,7 +6064,7 @@ class AparteChatHost {
5564
6064
  renumberSegments(segments);
5565
6065
  const next = [...msgs.slice(0, -1), { ...last, segments }];
5566
6066
  this.binding.setMessages(next);
5567
- this.binding.onMessagesChange?.(next);
6067
+ this._report(next);
5568
6068
  }
5569
6069
  /**
5570
6070
  * Append text to a segment's content in the last message — the streaming path
@@ -5620,8 +6120,8 @@ class AparteChatHost {
5620
6120
  this._flushHandle = null;
5621
6121
  }
5622
6122
  const tokens = this._tokenBuffer;
5623
- const pending = this._segmentBuffer.size > 0 ? new Map(this._segmentBuffer) : null;
5624
- if (!tokens && !pending) return;
6123
+ const pending2 = this._segmentBuffer.size > 0 ? new Map(this._segmentBuffer) : null;
6124
+ if (!tokens && !pending2) return;
5625
6125
  this._tokenBuffer = null;
5626
6126
  this._segmentBuffer.clear();
5627
6127
  let msgs = this.binding.getMessages();
@@ -5635,11 +6135,11 @@ class AparteChatHost {
5635
6135
  changed = true;
5636
6136
  }
5637
6137
  }
5638
- if (pending) {
6138
+ if (pending2) {
5639
6139
  const last = msgs[msgs.length - 1];
5640
6140
  if (last?.segments) {
5641
6141
  const segments = last.segments.map((s) => {
5642
- const buffered = pending.get(s.id);
6142
+ const buffered = pending2.get(s.id);
5643
6143
  if (buffered === void 0 || !("content" in s)) return s;
5644
6144
  return {
5645
6145
  ...s,
@@ -5653,7 +6153,7 @@ class AparteChatHost {
5653
6153
  }
5654
6154
  if (!changed) return;
5655
6155
  this.binding.setMessages(msgs);
5656
- this.binding.onMessagesChange?.(msgs);
6156
+ this._report(msgs);
5657
6157
  }
5658
6158
  /** Read the current message list. */
5659
6159
  getMessages() {
@@ -5686,7 +6186,7 @@ class AparteChatHost {
5686
6186
  const idx = msgs.findIndex((m) => m.id === messageId);
5687
6187
  const next = idx >= 0 ? msgs.slice(0, idx) : msgs;
5688
6188
  this.binding.setMessages(next);
5689
- this.binding.onMessagesChange?.(next);
6189
+ this._report(next);
5690
6190
  }
5691
6191
  /** Keep up to and including a user message, drop later responses (retry). */
5692
6192
  truncateResponsesAfter(userMessageId) {
@@ -5695,7 +6195,7 @@ class AparteChatHost {
5695
6195
  const idx = msgs.findIndex((m) => m.id === userMessageId);
5696
6196
  const next = idx >= 0 ? msgs.slice(0, idx + 1) : msgs;
5697
6197
  this.binding.setMessages(next);
5698
- this.binding.onMessagesChange?.(next);
6198
+ this._report(next);
5699
6199
  }
5700
6200
  // ── manual token streaming (agnostic core of injectTokenStream) ─────────
5701
6201
  /**
@@ -5723,14 +6223,14 @@ class AparteChatHost {
5723
6223
  }
5724
6224
  ac.signal.addEventListener("abort", () => resolve("aborted"), { once: true });
5725
6225
  });
5726
- const { host, hostId } = this.binding;
6226
+ const { host: host2, hostId } = this.binding;
5727
6227
  const onAbort = (e) => {
5728
6228
  const target = e.detail?.targetId;
5729
6229
  if ((!target || target === hostId) && this._streamAbort === ac) this.stopTokenStream();
5730
6230
  };
5731
6231
  window.addEventListener("aparte-abort", onAbort);
5732
6232
  const replaced = () => this._streamAbort !== void 0 && this._streamAbort !== ac;
5733
- dispatchLifecycleEvent(host, "aparte-message-start", { messageId, role: "assistant" }, hostId);
6233
+ dispatchLifecycleEvent(host2, "aparte-message-start", { messageId, role: "assistant" }, hostId);
5734
6234
  try {
5735
6235
  for (; ; ) {
5736
6236
  const res = await Promise.race([iterator.next(), aborted]);
@@ -5743,7 +6243,7 @@ class AparteChatHost {
5743
6243
  if (!replaced()) {
5744
6244
  this._setStreamingId(null);
5745
6245
  dispatchLifecycleEvent(
5746
- host,
6246
+ host2,
5747
6247
  ac.signal.aborted ? "aparte-message-aborted" : "aparte-message-done",
5748
6248
  ac.signal.aborted ? { messageId } : { messageId, role: "assistant" },
5749
6249
  hostId
@@ -5753,7 +6253,7 @@ class AparteChatHost {
5753
6253
  this._flushStreamState();
5754
6254
  if (!replaced()) {
5755
6255
  this._setStreamingId(null);
5756
- dispatchLifecycleEvent(host, "aparte-message-error", { messageId, error: err }, hostId);
6256
+ dispatchLifecycleEvent(host2, "aparte-message-error", { messageId, error: err }, hostId);
5757
6257
  }
5758
6258
  throw err;
5759
6259
  } finally {
@@ -5790,18 +6290,20 @@ class AparteChatHost {
5790
6290
  /**
5791
6291
  * Reconcile rendered bubbles with the current message list. The framework
5792
6292
  * calls this from its reactive message-change hook after rendering; the
5793
- * last message is always re-synced (streaming), others only once.
6293
+ * last message is always re-synced (streaming), the others whenever the list
6294
+ * holds a different object for them than the one they were drawn from.
5794
6295
  */
5795
6296
  syncBubbles() {
5796
6297
  const msgs = this.binding.getMessages();
5797
6298
  msgs.forEach((message, index) => {
5798
6299
  const isLast = index === msgs.length - 1;
5799
- if (this._renderedIds.has(message.id) && !isLast) return;
6300
+ if (this._renderedIds.get(message.id) === message && !isLast) return;
5800
6301
  const bubble = this._bubbleById(message.id);
5801
6302
  if (!bubble) return;
5802
6303
  populateBubbleFromMessage(bubble, message);
5803
- this._renderedIds.add(message.id);
6304
+ this._renderedIds.set(message.id, message);
5804
6305
  });
6306
+ this._checkpointState.reapply();
5805
6307
  }
5806
6308
  /**
5807
6309
  * Rebuild the viewport's internal repo from the message list before branch
@@ -5911,22 +6413,23 @@ class AparteChatHost {
5911
6413
  const all = vp.querySelectorAll("aparte-chat-bubble");
5912
6414
  return all.length ? all[all.length - 1] : null;
5913
6415
  }
5914
- _installHostMethods(host) {
5915
- host["appendMessage"] = (m) => this.appendMessage(m);
5916
- host["updateMessage"] = (id, u) => this.updateMessage(id, u);
5917
- host["updateLastMessage"] = (c, o) => this.updateLastMessage(c, o);
5918
- host["addSegment"] = (s) => this.addSegment(s);
5919
- host["updateSegment"] = (id, u) => this.updateSegment(id, u);
5920
- host["removeSegment"] = (id) => this.removeSegment(id);
5921
- host["appendToSegment"] = (id, c) => this.appendToSegment(id, c);
5922
- host["getMessages"] = () => this.getMessages();
5923
- host["addBranch"] = (id) => this.addBranch(id);
5924
- host["addSiblingOf"] = (id, m) => this.addSiblingOf(id, m);
5925
- host["truncateFrom"] = (id) => this.truncateFrom(id);
5926
- host["truncateResponsesAfter"] = (id) => this.truncateResponsesAfter(id);
5927
- host["clearAll"] = (options) => this.clearMessages(options);
5928
- }
5929
- _installLifecycleListeners(host) {
6416
+ _installHostMethods(host2) {
6417
+ host2["appendMessage"] = (m) => this.appendMessage(m);
6418
+ host2["updateMessage"] = (id, u) => this.updateMessage(id, u);
6419
+ host2["updateLastMessage"] = (c, o) => this.updateLastMessage(c, o);
6420
+ host2["addSegment"] = (s) => this.addSegment(s);
6421
+ host2["updateSegment"] = (id, u) => this.updateSegment(id, u);
6422
+ host2["removeSegment"] = (id) => this.removeSegment(id);
6423
+ host2["appendToSegment"] = (id, c) => this.appendToSegment(id, c);
6424
+ host2["getMessages"] = () => this.getMessages();
6425
+ host2["getMessage"] = (id) => this.binding.getMessages().find((m) => m.id === id);
6426
+ host2["addBranch"] = (id) => this.addBranch(id);
6427
+ host2["addSiblingOf"] = (id, m) => this.addSiblingOf(id, m);
6428
+ host2["truncateFrom"] = (id) => this.truncateFrom(id);
6429
+ host2["truncateResponsesAfter"] = (id) => this.truncateResponsesAfter(id);
6430
+ host2["clearAll"] = (options) => this.clearMessages(options);
6431
+ }
6432
+ _installLifecycleListeners(host2) {
5930
6433
  const onStart = (e) => {
5931
6434
  const d = e?.detail;
5932
6435
  if (d?.messageId) this._setStreamingId(d.messageId);
@@ -5946,7 +6449,7 @@ class AparteChatHost {
5946
6449
  (m) => m.id === d.messageId ? { ...m, usage: d.usage } : m
5947
6450
  );
5948
6451
  this.binding.setMessages(next);
5949
- this.binding.onMessagesChange?.(next);
6452
+ this._report(next);
5950
6453
  this._bubbleById(d.messageId)?.setUsage?.(d.usage);
5951
6454
  };
5952
6455
  const onEnd = (e) => {
@@ -5954,15 +6457,15 @@ class AparteChatHost {
5954
6457
  this.binding.onTypingChange?.(false);
5955
6458
  this._setStreamingId(null);
5956
6459
  };
5957
- host.addEventListener("aparte-message-start", onStart);
5958
- host.addEventListener("aparte-message-done", onDone);
5959
- host.addEventListener("aparte-message-error", onEnd);
5960
- host.addEventListener("aparte-message-aborted", onEnd);
6460
+ host2.addEventListener("aparte-message-start", onStart);
6461
+ host2.addEventListener("aparte-message-done", onDone);
6462
+ host2.addEventListener("aparte-message-error", onEnd);
6463
+ host2.addEventListener("aparte-message-aborted", onEnd);
5961
6464
  this._teardown.push(() => {
5962
- host.removeEventListener("aparte-message-start", onStart);
5963
- host.removeEventListener("aparte-message-done", onDone);
5964
- host.removeEventListener("aparte-message-error", onEnd);
5965
- host.removeEventListener("aparte-message-aborted", onEnd);
6465
+ host2.removeEventListener("aparte-message-start", onStart);
6466
+ host2.removeEventListener("aparte-message-done", onDone);
6467
+ host2.removeEventListener("aparte-message-error", onEnd);
6468
+ host2.removeEventListener("aparte-message-aborted", onEnd);
5966
6469
  });
5967
6470
  }
5968
6471
  _installViewportListeners() {
@@ -5979,7 +6482,7 @@ class AparteChatHost {
5979
6482
  return old?.usage !== void 0 ? { ...m, usage: old.usage } : m;
5980
6483
  });
5981
6484
  this.binding.setMessages(merged);
5982
- this.binding.onMessagesChange?.(merged);
6485
+ this._report(merged);
5983
6486
  if (evt.detail.siblings.some((s) => s.count > 1)) {
5984
6487
  this._pendingSiblings = evt.detail.siblings;
5985
6488
  this.binding.afterRender(() => this._applyPendingSiblings());
@@ -5994,6 +6497,37 @@ class AparteChatHost {
5994
6497
  });
5995
6498
  }
5996
6499
  }
6500
+ function historyFromCheckpoint(path) {
6501
+ for (let i = path.length - 1; i >= 0; i--) {
6502
+ const summary = path[i].contextSummary?.summary;
6503
+ if (summary !== void 0) return { summary, messages: path.slice(i) };
6504
+ }
6505
+ return { messages: path };
6506
+ }
6507
+ const COMPACTION_PREAMBLE = "The earlier part of this conversation was compacted. This is a summary of what came before; continue from it.";
6508
+ const underPreamble = (text) => `${COMPACTION_PREAMBLE}
6509
+
6510
+ ${text}`;
6511
+ function mergeAdjacentUserMessages(messages) {
6512
+ const asParts = (c) => typeof c === "string" ? [{ type: "text", text: c }] : c;
6513
+ const out = [];
6514
+ for (const m of messages) {
6515
+ const prev = out[out.length - 1];
6516
+ if (m.role !== "user" || prev?.role !== "user") {
6517
+ out.push(m);
6518
+ } else if (typeof prev.content === "string" && typeof m.content === "string") {
6519
+ out[out.length - 1] = { ...prev, content: `${prev.content}
6520
+
6521
+ ${m.content}` };
6522
+ } else {
6523
+ out[out.length - 1] = {
6524
+ ...prev,
6525
+ content: [...asParts(prev.content), { type: "text", text: "\n\n" }, ...asParts(m.content)]
6526
+ };
6527
+ }
6528
+ }
6529
+ return out;
6530
+ }
5997
6531
  const STOPPED_TOOL_TEXT = "The tool call was stopped before it ran.";
5998
6532
  const EMPTY_TOOL_TEXT = "The tool returned no output.";
5999
6533
  function answerOf(segment) {
@@ -6044,6 +6578,139 @@ function wireTurns(message, textOf, options = {}) {
6044
6578
  flush();
6045
6579
  return out;
6046
6580
  }
6581
+ const isInFlight = (m) => m.status === "streaming" || m.status === "pending";
6582
+ function segmentsToWireText(segments) {
6583
+ if (!segments?.length) return "";
6584
+ const fence = "```";
6585
+ const parts = [];
6586
+ for (const segment of segments) {
6587
+ const content = segment.content;
6588
+ if (segment.type === "text") {
6589
+ if (content) parts.push(content);
6590
+ } else if (segment.type === "code") {
6591
+ const lang = segment.language ?? "";
6592
+ parts.push(`${fence}${lang}
6593
+ ${content ?? ""}
6594
+ ${fence}`);
6595
+ } else if (segment.type === "thinking" || segment.type === "tool_call" || segment.type === "error") {
6596
+ continue;
6597
+ } else if (typeof content === "string" && content) {
6598
+ parts.push(content);
6599
+ } else {
6600
+ const fallback = segment.fallback;
6601
+ if (fallback) parts.push(fallback);
6602
+ }
6603
+ }
6604
+ return parts.join("\n").trim();
6605
+ }
6606
+ function extractWireText(message) {
6607
+ const rendered = segmentsToWireText(message.segments);
6608
+ if (rendered) return rendered;
6609
+ if (typeof message.content === "string" && message.content) return message.content;
6610
+ return "";
6611
+ }
6612
+ function messageWireText(message) {
6613
+ const text = extractWireText(message);
6614
+ return message.compaction ? underPreamble(text) : text;
6615
+ }
6616
+ function wireMessages(messages) {
6617
+ return messages.filter((m) => {
6618
+ if (m.role !== "user" && m.role !== "assistant") return false;
6619
+ return m.status !== "error" && !isInFlight(m);
6620
+ }).flatMap((m) => {
6621
+ if (m.role === "assistant" && m.segments?.some((s) => s.type === "tool_call")) {
6622
+ return wireTurns(m, segmentsToWireText);
6623
+ }
6624
+ const content = messageWireText(m);
6625
+ return content ? [{ role: m.role, content }] : [];
6626
+ });
6627
+ }
6628
+ function estimateTokens(text) {
6629
+ if (!text) return 0;
6630
+ return Math.ceil(text.length / 3.8);
6631
+ }
6632
+ const estimateJson = (value) => {
6633
+ if (!value) return 0;
6634
+ try {
6635
+ return estimateTokens(JSON.stringify(value));
6636
+ } catch {
6637
+ return 0;
6638
+ }
6639
+ };
6640
+ function estimateChatTokens(messages) {
6641
+ let total = 0;
6642
+ for (const message of messages) {
6643
+ total += estimateTokens(contentToText(message.content));
6644
+ if (message.toolCalls?.length) total += estimateJson(message.toolCalls);
6645
+ }
6646
+ return total;
6647
+ }
6648
+ function carriesNonTextParts(messages) {
6649
+ return messages.some((m) => typeof m.content !== "string" && m.content.some((p) => p.type !== "text"));
6650
+ }
6651
+ function estimateRequestTokens(request) {
6652
+ const declared = (request.tools ?? []).map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }));
6653
+ return estimateChatTokens(request.messages) + (declared.length ? estimateJson(declared) : 0);
6654
+ }
6655
+ function estimateWireTokens(messages) {
6656
+ return estimateChatTokens(wireMessages(messages));
6657
+ }
6658
+ const usageTokens = (usage) => usage.totalTokens ?? usage.inputTokens + usage.outputTokens;
6659
+ function toolsForCurrentModel(config) {
6660
+ const capabilities = config.getCurrentModel()?.capabilities;
6661
+ const declinesTools = capabilities ? !capabilities.includes("function_calling") : false;
6662
+ return declinesTools ? [] : config.getTools();
6663
+ }
6664
+ function windowOf(config, windowOverride) {
6665
+ if (typeof windowOverride === "number" && Number.isFinite(windowOverride) && windowOverride > 0) return windowOverride;
6666
+ const declared = config.getCurrentModel()?.contextWindow;
6667
+ return declared && declared > 0 ? declared : void 0;
6668
+ }
6669
+ const hasSegment = (message, type) => message.segments?.some((s) => s.type === type) === true;
6670
+ function calibration(reply) {
6671
+ const sent = reply.usage?.inputTokens;
6672
+ const recorded = reply.contextEstimate;
6673
+ if (hasSegment(reply, "tool_call")) return 1;
6674
+ return sent && recorded && sent > 0 && recorded > 0 ? Math.min(3, Math.max(0.5, sent / recorded)) : 1;
6675
+ }
6676
+ function nextRequestTokens(reply, k) {
6677
+ const usage = reply.usage;
6678
+ if (!hasSegment(reply, "thinking") || !(usage.inputTokens > 0)) return usageTokens(usage);
6679
+ const segments = reply.segments ?? [];
6680
+ const lastCall = segments.map((s) => s.type).lastIndexOf("tool_call");
6681
+ const said = Math.round(k * estimateTokens(segmentsToWireText(segments.slice(lastCall + 1))));
6682
+ return usage.inputTokens + (usage.outputTokens > 0 ? Math.min(said, usage.outputTokens) : said);
6683
+ }
6684
+ function measureContext(input) {
6685
+ const { messages, config } = input;
6686
+ const view = historyFromCheckpoint(messages);
6687
+ const system = estimateTokens(config.resolveSystemPrompt()) + estimateTokens(config.resolveToolSystemPrompts());
6688
+ const declared = toolsForCurrentModel(config).map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }));
6689
+ const tools = declared.length ? estimateJson(declared) : 0;
6690
+ const summary = view.summary === void 0 ? 0 : estimateTokens(underPreamble(view.summary));
6691
+ const parts = { system, tools, summary, messages: estimateWireTokens(view.messages) };
6692
+ const total = system + tools + summary + parts.messages;
6693
+ let replyIndex = -1;
6694
+ for (let i = messages.length - 1; i >= 0; i--) {
6695
+ if (messages[i].role === "assistant" && messages[i].usage) {
6696
+ replyIndex = i;
6697
+ break;
6698
+ }
6699
+ }
6700
+ const reply = replyIndex >= 0 ? messages[replyIndex] : void 0;
6701
+ const window2 = windowOf(config, input.windowOverride);
6702
+ const result = (used, estimated) => ({
6703
+ ...window2 !== void 0 ? { window: window2 } : {},
6704
+ used,
6705
+ estimated,
6706
+ parts,
6707
+ other: Math.max(0, used - total)
6708
+ });
6709
+ const unchanged = reply !== void 0 && !input.viewChanged && replyIndex === messages.length - 1 && !messages.some((m) => m.contextSummary !== void 0 && m.contextSummary.createdAt > reply.timestamp);
6710
+ const k = reply ? calibration(reply) : 1;
6711
+ if (reply && unchanged) return result(nextRequestTokens(reply, k), false);
6712
+ return result(Math.round(k * total), true);
6713
+ }
6047
6714
  function eventOrigin(e) {
6048
6715
  return e.composedPath?.()[0] ?? e.target;
6049
6716
  }
@@ -6580,7 +7247,6 @@ function buildApprovalPanel(message, options, onChange, details) {
6580
7247
  function requestUserInput(request) {
6581
7248
  return resolveConfig(request.target ?? null).requestUserInput(request);
6582
7249
  }
6583
- const COMPACTION_PREAMBLE = "The earlier part of this conversation was compacted. This is a summary of what came before; continue from it.";
6584
7250
  const NOT_RUN_AFTER_A_REFUSAL = "Not run: an earlier call of the same step was refused.";
6585
7251
  const approvalKey = (call) => JSON.stringify([call.id, call.name, call.input]);
6586
7252
  const warnedUninlinedFiles = /* @__PURE__ */ new Set();
@@ -6600,12 +7266,37 @@ function remoteProvider(id) {
6600
7266
  }
6601
7267
  class AparteClient {
6602
7268
  _boundHandler = null;
7269
+ /** Between `start()` and `stop()`: the listeners are on `window`. The send handler alone is
7270
+ * not the sign — the constructor prepares it without listening. */
7271
+ _listening = false;
6603
7272
  _boundAbortHandler = null;
6604
7273
  _boundLoadedHandler = null;
6605
7274
  _boundRetryHandler = null;
6606
7275
  _boundEditHandler = null;
6607
7276
  _releaseKeyResolver = null;
6608
7277
  _isAborted = false;
7278
+ /** Provider and model pairs whose window was already read again after a turn (see `_readWindowAfterTurn`). */
7279
+ _windowReadFor = /* @__PURE__ */ new Set();
7280
+ /**
7281
+ * The current model's window, read again once after a turn when it had none.
7282
+ *
7283
+ * A local server that loads a model on its first request — LM Studio's just-in-time
7284
+ * loading, Ollama — has no loaded window to report when the model list is fetched at
7285
+ * start-up, so the model comes without one, and the gauge and the compaction have
7286
+ * nothing to measure against. After a reply the model is loaded, and the provider's
7287
+ * own probe finds it. Once per provider and model: a server that never says costs one
7288
+ * request, not one per turn. The config announces the window when it appears.
7289
+ */
7290
+ async _readWindowAfterTurn() {
7291
+ const config = this._config;
7292
+ const model = config.getCurrentModel();
7293
+ const providerId = config.getModelConfig().defaultProvider;
7294
+ if (!model || model.contextWindow || !providerId) return;
7295
+ const key = `${providerId}\0${model.id}`;
7296
+ if (this._windowReadFor.has(key)) return;
7297
+ this._windowReadFor.add(key);
7298
+ await config.refreshProviderModels(providerId);
7299
+ }
6609
7300
  /**
6610
7301
  * Does this window event belong to this client?
6611
7302
  *
@@ -6734,11 +7425,12 @@ class AparteClient {
6734
7425
  if (!this._boundLoadedHandler) {
6735
7426
  this._boundLoadedHandler = (e) => {
6736
7427
  if (!this._isForThisInstance(e)) return;
6737
- const host = e.target instanceof HTMLElement ? e.target : void 0;
6738
- void this.resumeDeferred(host ?? e.detail?.targetId);
7428
+ const host2 = e.target instanceof HTMLElement ? e.target : void 0;
7429
+ void this.resumeDeferred(host2 ?? e.detail?.targetId);
6739
7430
  };
6740
7431
  }
6741
7432
  window.addEventListener("aparte-conversation-loaded", this._boundLoadedHandler);
7433
+ this._listening = true;
6742
7434
  }
6743
7435
  /**
6744
7436
  * Stop answering: abort the reply in flight, take this client's `keyResolver` off the
@@ -6748,7 +7440,8 @@ class AparteClient {
6748
7440
  this.abort();
6749
7441
  this._releaseKeyResolver?.();
6750
7442
  this._releaseKeyResolver = null;
6751
- if (!this._boundHandler) return;
7443
+ if (!this._listening || !this._boundHandler) return;
7444
+ this._listening = false;
6752
7445
  window.removeEventListener("aparte-send", this._boundHandler);
6753
7446
  this._boundHandler = null;
6754
7447
  if (this._boundAbortHandler) {
@@ -6885,19 +7578,23 @@ class AparteClient {
6885
7578
  async _streamTurn(targetElement, messageId, provider, messages, modelId, authConfig, opts) {
6886
7579
  const chatId = this._targetIdOf(targetElement);
6887
7580
  const registeredTools = this._toolsForCurrentModel();
6888
- let baseRequest = {
6889
- messages,
6890
- modelId,
6891
- stream: true,
6892
- tools: registeredTools.length ? registeredTools : void 0,
6893
- ...opts?.temperature !== void 0 ? { temperature: opts.temperature } : {}
7581
+ const buildRequest = async (requestMessages2) => {
7582
+ let built = {
7583
+ messages: requestMessages2,
7584
+ modelId,
7585
+ stream: true,
7586
+ tools: registeredTools.length ? registeredTools : void 0,
7587
+ ...opts?.temperature !== void 0 ? { temperature: opts.temperature } : {}
7588
+ };
7589
+ if (this.options.requestInterceptor) {
7590
+ built = await this.options.requestInterceptor(built);
7591
+ }
7592
+ if (built.toolChoice === "none") {
7593
+ built = { ...built, tools: void 0 };
7594
+ }
7595
+ return built;
6894
7596
  };
6895
- if (this.options.requestInterceptor) {
6896
- baseRequest = await this.options.requestInterceptor(baseRequest);
6897
- }
6898
- if (baseRequest.toolChoice === "none") {
6899
- baseRequest = { ...baseRequest, tools: void 0 };
6900
- }
7597
+ let baseRequest = await buildRequest(messages);
6901
7598
  if (this._isAborted) {
6902
7599
  dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId }, chatId);
6903
7600
  this._updateMessage(targetElement, messageId, { status: "completed" });
@@ -6918,7 +7615,23 @@ class AparteClient {
6918
7615
  if (this._streamController !== streamController) this._streamController?.abort();
6919
7616
  this._streamController = streamController;
6920
7617
  this._turnsInFlight.add(messageId);
6921
- try {
7618
+ const compactor = opts?.rebuild ? resolveConfig(targetElement).getCompactor() ?? this._config.getCompactor() : null;
7619
+ const turnIds = () => {
7620
+ const path = targetElement.getMessages?.() ?? [];
7621
+ const at = path.findIndex((m) => m.id === messageId);
7622
+ const asked = at > 0 ? path[at - 1] : void 0;
7623
+ return asked?.role === "user" ? [asked.id, messageId] : [messageId];
7624
+ };
7625
+ const requestMessages = () => opts?.resume ? this._resumeMessages(targetElement, messageId, baseRequest.messages) : baseRequest.messages;
7626
+ const requestEstimate = () => estimateRequestTokens({ messages: requestMessages(), tools: baseRequest.tools });
7627
+ const recordedEstimate = () => carriesNonTextParts(requestMessages()) ? void 0 : requestEstimate();
7628
+ const endStopped = () => {
7629
+ this._closeOpenCalls(targetElement, messageId);
7630
+ dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId }, chatId);
7631
+ this._updateMessage(targetElement, messageId, { status: "completed" });
7632
+ };
7633
+ const isStopped = () => this._isAborted || streamController.signal.aborted;
7634
+ const runOnce = async () => {
6922
7635
  let result = opts?.resume ? { usage: void 0, suspended: opts.resume } : await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController);
6923
7636
  const maxResumes = this.options.maxTurns ?? 10;
6924
7637
  let resumes = 0;
@@ -6961,21 +7674,80 @@ class AparteClient {
6961
7674
  );
6962
7675
  }
6963
7676
  const usage = result.usage;
7677
+ const finishReason = result.finishReason;
6964
7678
  if (this._isAborted || streamController.signal.aborted) {
6965
7679
  this._closeOpenCalls(targetElement, messageId);
6966
7680
  this._updateMessage(targetElement, messageId, { status: "completed" });
6967
7681
  } else {
6968
- dispatchLifecycleEvent(targetElement, "aparte-message-done", { messageId, role: "assistant", usage }, chatId);
7682
+ dispatchLifecycleEvent(targetElement, "aparte-message-done", { messageId, role: "assistant", usage, ...finishReason ? { finishReason } : {} }, chatId);
7683
+ void this._readWindowAfterTurn();
6969
7684
  }
6970
- } catch (error) {
6971
- this._closeOpenCalls(targetElement, messageId);
6972
- if (this._isAborted || streamController.signal.aborted) {
6973
- dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId }, chatId);
6974
- this._updateMessage(targetElement, messageId, { status: "completed" });
6975
- return;
7685
+ };
7686
+ const resendAfterOverflow = async (error) => {
7687
+ if (error.code !== AparteErrorCode.USAGE_CONTEXT_EXCEEDED || !compactor || !opts?.rebuild) return "stands";
7688
+ const segments = targetElement.getMessage?.(messageId)?.segments ?? targetElement.getMessages?.()?.find((m) => m.id === messageId)?.segments ?? [];
7689
+ if (segments.some((s) => s.type === "text" || s.type === "tool_call")) return "stands";
7690
+ try {
7691
+ const outcome = await compactor.compact({
7692
+ target: targetElement,
7693
+ ...chatId ? { targetId: chatId } : {},
7694
+ force: true,
7695
+ reason: "overflow",
7696
+ exclude: turnIds(),
7697
+ signal: streamController.signal
7698
+ });
7699
+ if (isStopped()) return "stopped";
7700
+ if (!outcome.ok || outcome.skipped) return "stands";
7701
+ baseRequest = await buildRequest(await opts.rebuild());
7702
+ } catch {
7703
+ return "stands";
7704
+ }
7705
+ this._updateMessage(targetElement, messageId, { status: "pending" });
7706
+ return "resend";
7707
+ };
7708
+ try {
7709
+ for (let attempt = 0; ; attempt++) {
7710
+ try {
7711
+ if (attempt === 0 && compactor?.beforeSend && opts?.rebuild) {
7712
+ let compacted = false;
7713
+ try {
7714
+ compacted = await compactor.beforeSend({
7715
+ target: targetElement,
7716
+ ...chatId ? { targetId: chatId } : {},
7717
+ exclude: turnIds(),
7718
+ requestTokens: requestEstimate(),
7719
+ signal: streamController.signal
7720
+ });
7721
+ } catch (failure) {
7722
+ console.warn("[AparteClient] The compactor threw before a send; sending the request as built.", failure);
7723
+ }
7724
+ if (isStopped()) {
7725
+ endStopped();
7726
+ return;
7727
+ }
7728
+ if (compacted) baseRequest = await buildRequest(await opts.rebuild());
7729
+ }
7730
+ this._updateMessage(targetElement, messageId, { contextEstimate: recordedEstimate() });
7731
+ await runOnce();
7732
+ return;
7733
+ } catch (error) {
7734
+ this._closeOpenCalls(targetElement, messageId);
7735
+ if (isStopped()) {
7736
+ dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId }, chatId);
7737
+ this._updateMessage(targetElement, messageId, { status: "completed" });
7738
+ return;
7739
+ }
7740
+ const aparteError = AparteError.from(error, AparteErrorCode.UNKNOWN_ERROR);
7741
+ const next = attempt === 0 ? await resendAfterOverflow(aparteError) : "stands";
7742
+ if (next === "resend") continue;
7743
+ if (next === "stopped") {
7744
+ endStopped();
7745
+ return;
7746
+ }
7747
+ this._handleLifecycleError(targetElement, messageId, aparteError);
7748
+ return;
7749
+ }
6976
7750
  }
6977
- const aparteError = AparteError.from(error, AparteErrorCode.UNKNOWN_ERROR);
6978
- this._handleLifecycleError(targetElement, messageId, aparteError);
6979
7751
  } finally {
6980
7752
  this._turnsInFlight.delete(messageId);
6981
7753
  }
@@ -6990,29 +7762,12 @@ class AparteClient {
6990
7762
  * using the same conversation history minus the retried reply.
6991
7763
  */
6992
7764
  /**
6993
- * The registered tools, unless the current model says it cannot call one.
6994
- *
6995
- * Single source for the gate, so send / retry / edit cannot drift.
6996
- *
6997
- * The question is "did this model say it cannot", not "did it say it can": the
6998
- * app sets a model id by hand (`setModelConfig`) or a selector sets it from a
6999
- * fetched list, and in both cases the model's declared capabilities are commonly
7000
- * unknown. `undefined` capabilities read as "can", or `tools: []` would go on
7001
- * the wire while `getTools()` held the tool the app had explicitly registered,
7002
- * and the model would answer that it had no such tool. Registering a tool is an explicit act by the app; silently dropping it
7003
- * because a `/models` listing is terse is the library second-guessing the
7004
- * developer. A model that declares its capabilities and omits function calling
7005
- * is still honoured — that is a statement, and it is respected.
7006
- *
7007
- * Failure modes, both ways round: over-sending means a model that cannot use
7008
- * tools does not call one, or its own endpoint rejects the array with an error
7009
- * the developer can read. Under-sending would be silent, total, and look like a
7010
- * lying model.
7765
+ * The registered tools, unless the current model says it cannot call one — the gate
7766
+ * lives in `toolsForCurrentModel` (with its reasoning), where the context measure reads
7767
+ * the same rule, so send / retry / edit and the gauge cannot drift.
7011
7768
  */
7012
7769
  _toolsForCurrentModel() {
7013
- const capabilities = this._config.getCurrentModel()?.capabilities;
7014
- const declinesTools = capabilities ? !capabilities.includes("function_calling") : false;
7015
- return declinesTools ? [] : this._config.getTools();
7770
+ return toolsForCurrentModel(this._config);
7016
7771
  }
7017
7772
  async _handleRetry(event) {
7018
7773
  const { messageId, targetId } = event.detail ?? {};
@@ -7047,10 +7802,16 @@ class AparteClient {
7047
7802
  targetElement,
7048
7803
  newMessageId,
7049
7804
  provider,
7050
- [...systemMessages, ...chatMessages],
7805
+ [...systemMessages, ...mergeAdjacentUserMessages(chatMessages)],
7051
7806
  config.defaultModel || "",
7052
7807
  authConfig,
7053
- { temperature: 0.4 }
7808
+ {
7809
+ temperature: 0.4,
7810
+ rebuild: async () => [
7811
+ ...this._systemMessages(),
7812
+ ...mergeAdjacentUserMessages(this._messagesToChatMessages(this._current(targetElement, historyMessages)))
7813
+ ]
7814
+ }
7054
7815
  );
7055
7816
  }
7056
7817
  /**
@@ -7113,11 +7874,27 @@ class AparteClient {
7113
7874
  targetElement,
7114
7875
  newMessageId,
7115
7876
  provider,
7116
- [...systemMessages, ...chatMessages],
7877
+ [...systemMessages, ...mergeAdjacentUserMessages(chatMessages)],
7117
7878
  config.defaultModel || "",
7118
- authConfig
7879
+ authConfig,
7880
+ {
7881
+ rebuild: async () => [
7882
+ ...this._systemMessages(),
7883
+ ...mergeAdjacentUserMessages(this._messagesToChatMessages(this._current(targetElement, historyMessages)))
7884
+ ]
7885
+ }
7119
7886
  );
7120
7887
  }
7888
+ /**
7889
+ * These messages as the transcript holds them NOW. A retry or an edit slices the path
7890
+ * before it streams; a compaction that runs during the turn writes its checkpoint onto
7891
+ * one of those messages in the transcript, and the slice still holds the objects as they
7892
+ * were. Looked up by id, tree-wide, because a retry has moved the path since.
7893
+ */
7894
+ _current(target, messages) {
7895
+ const live2 = new Map((target.getMessages?.() ?? []).map((m) => [m.id, m]));
7896
+ return messages.map((m) => target.getMessage?.(m.id) ?? live2.get(m.id) ?? m);
7897
+ }
7121
7898
  /**
7122
7899
  * The chain the gesture really came up, nearest node first.
7123
7900
  *
@@ -7251,29 +8028,17 @@ class AparteClient {
7251
8028
  * imitating.
7252
8029
  */
7253
8030
  _messagesToChatMessages(messages) {
7254
- return this._wireHistory(messages);
8031
+ const { summary, messages: kept } = historyFromCheckpoint(messages);
8032
+ return [...this._summaryMessage(summary), ...wireMessages(kept)];
7255
8033
  }
7256
8034
  /**
7257
- * The one rule for what a message contributes to the wire.
7258
- *
7259
- * Not the slice — the contribution: a role the wire has (`user`/`assistant`), a
7260
- * turn that neither failed nor is still in flight, and text that is not empty.
7261
- *
7262
- * `_wireText` (not `m.content`): assistant replies stream their text into
7263
- * `segments`, leaving `content` as '', so without flattening every reply would be
7264
- * empty here and the drop below would swallow the whole transcript.
7265
- */
7266
- _wireHistory(messages) {
7267
- return messages.filter((m) => {
7268
- if (m.role !== "user" && m.role !== "assistant") return false;
7269
- return m.status !== "error" && !AparteClient._inFlight(m);
7270
- }).flatMap((m) => {
7271
- if (m.role === "assistant" && m.segments?.some((s) => s.type === "tool_call")) {
7272
- return wireTurns(m, (segments) => this._segmentsToText(segments));
7273
- }
7274
- const content = this._wireText(m);
7275
- return content ? [{ role: m.role, content }] : [];
7276
- });
8035
+ * What the model reads in place of everything before a checkpoint: the summary, as a
8036
+ * `user` message under the same preamble a legacy compaction message gets, so it is
8037
+ * context and not something the person said. It may sit right before a user turn;
8038
+ * `mergeAdjacentUserMessages` joins the two where the history is assembled.
8039
+ */
8040
+ _summaryMessage(summary) {
8041
+ return summary === void 0 ? [] : [{ role: "user", content: underPreamble(summary) }];
7277
8042
  }
7278
8043
  /** The "no model selected" warning is said once per client, not once per dropped send. */
7279
8044
  _warnedNoModel = false;
@@ -7363,12 +8128,14 @@ class AparteClient {
7363
8128
  });
7364
8129
  let authConfig;
7365
8130
  let messages;
8131
+ let fileParts;
7366
8132
  try {
7367
8133
  authConfig = providerId ? await this._resolveAuth(providerId) : void 0;
7368
8134
  const rawFiles = Array.isArray(event.detail?.files) ? event.detail.files : [];
7369
8135
  const filesToInject = this._selectFilesToInject(rawFiles);
7370
8136
  const contentParts = filesToInject.length > 0 ? await this._filesToContentParts(filesToInject) : [];
7371
- messages = this._buildMessages(content, targetElement, contentParts.length > 0 ? contentParts : void 0);
8137
+ fileParts = contentParts.length > 0 ? contentParts : void 0;
8138
+ messages = this._buildMessages(content, targetElement, fileParts);
7372
8139
  } catch (error) {
7373
8140
  this._handleLifecycleError(targetElement, messageId, AparteError.from(error, AparteErrorCode.UNKNOWN_ERROR));
7374
8141
  return;
@@ -7379,7 +8146,8 @@ class AparteClient {
7379
8146
  provider,
7380
8147
  messages,
7381
8148
  modelId || config.defaultModel || "",
7382
- authConfig
8149
+ authConfig,
8150
+ { rebuild: async () => this._buildMessages(content, targetElement, fileParts) }
7383
8151
  );
7384
8152
  }
7385
8153
  /**
@@ -7396,101 +8164,18 @@ class AparteClient {
7396
8164
  }
7397
8165
  const userMsg = parts && parts.length > 0 ? [{ type: "text", text: userContent }, ...parts] : userContent;
7398
8166
  messages.push({ role: "user", content: userMsg });
7399
- return messages;
7400
- }
7401
- /**
7402
- * A turn that has not finished yet. `status` is OPTIONAL on `AparteMessage`, so
7403
- * the question the history gates can ask is "is this still in flight?", never
7404
- * "is this completed?".
7405
- *
7406
- * The gates below ask the first, because a host that SEEDS a transcript
7407
- * — restoring a saved conversation, hydrating a server-rendered one — has no
7408
- * reason to invent a status for turns that are already over. Asking "is this
7409
- * completed?" would leave the cutoff at 0 and slice away the whole transcript: the
7410
- * model would get the new question with no context, and nothing in the UI would say
7411
- * so, because the viewport still shows every message.
7412
- */
7413
- static _inFlight(m) {
7414
- return m.status === "streaming" || m.status === "pending";
8167
+ return mergeAdjacentUserMessages(messages);
7415
8168
  }
7416
- _toHistoryMessages(messages) {
8169
+ _toHistoryMessages(path) {
8170
+ const { summary, messages } = historyFromCheckpoint(path);
7417
8171
  let cutoff = 0;
7418
8172
  for (let i = 0; i < messages.length; i++) {
7419
8173
  const m = messages[i];
7420
- if (m.role === "assistant" && m.status !== "error" && !AparteClient._inFlight(m)) {
8174
+ if (m.role === "assistant" && m.status !== "error" && !isInFlight(m)) {
7421
8175
  cutoff = i + 1;
7422
8176
  }
7423
8177
  }
7424
- return this._wireHistory(messages.slice(0, cutoff));
7425
- }
7426
- /**
7427
- * A message's text as the MODEL should read it — the one mapper both history
7428
- * paths use, so the compaction preamble cannot be applied on retry and edit and
7429
- * forgotten on the ordinary send, which is what the first review of this feature
7430
- * caught: the summary went out on every normal turn as something the user said.
7431
- */
7432
- _wireText(message) {
7433
- const text = this._extractText(message);
7434
- return message.compaction ? `${COMPACTION_PREAMBLE}
7435
-
7436
- ${text}` : text;
7437
- }
7438
- /**
7439
- * What the model is told it said last turn — reconstructed from what was
7440
- * RENDERED, not from the raw-text field.
7441
- *
7442
- * The segments are what the reader saw, so they come first. `content` is the
7443
- * fallback, and that is not vestigial: a non-streaming transport writes the whole
7444
- * reply there and creates no segments at all.
7445
- */
7446
- _extractText(message) {
7447
- const rendered = this._segmentsToText(message.segments);
7448
- if (rendered) return rendered;
7449
- if (typeof message.content === "string" && message.content) return message.content;
7450
- return "";
7451
- }
7452
- /**
7453
- * Segments → the text a model can read back.
7454
- *
7455
- * Fences and the language tag are kept. Without them the model re-reads its own
7456
- * code as prose — which is how it starts explaining a snippet it believes it
7457
- * wrote in English.
7458
- *
7459
- * A type core does not know — a registered block grammar's (an artifact, a
7460
- * citation, a file), a consumer's custom segment — follows one rule: its
7461
- * `content` if it has one, else its `fallback`, else nothing. Dropping such a
7462
- * segment would make a document the model had just produced invisible on the very
7463
- * next turn, so it could not be asked to change the thing it had built. An artifact
7464
- * is a plugin's type like any other and follows this rule.
7465
- *
7466
- * `thinking` and `tool_call` stay out on purpose — the first is the model's own
7467
- * scratchpad, and most APIs neither want it back nor bill for it kindly; the
7468
- * second is not text: `_wireHistory` sends a reply's calls as the assistant's
7469
- * `toolCalls` and the `tool` messages answering them (`wire-history.ts`).
7470
- */
7471
- _segmentsToText(segments) {
7472
- if (!segments?.length) return "";
7473
- const fence = "```";
7474
- const parts = [];
7475
- for (const segment of segments) {
7476
- const content = segment.content;
7477
- if (segment.type === "text") {
7478
- if (content) parts.push(content);
7479
- } else if (segment.type === "code") {
7480
- const lang = segment.language ?? "";
7481
- parts.push(`${fence}${lang}
7482
- ${content ?? ""}
7483
- ${fence}`);
7484
- } else if (segment.type === "thinking" || segment.type === "tool_call" || segment.type === "error") {
7485
- continue;
7486
- } else if (typeof content === "string" && content) {
7487
- parts.push(content);
7488
- } else {
7489
- const fallback = segment.fallback;
7490
- if (fallback) parts.push(fallback);
7491
- }
7492
- }
7493
- return parts.join("\n").trim();
8178
+ return [...this._summaryMessage(summary), ...wireMessages(messages.slice(0, cutoff))];
7494
8179
  }
7495
8180
  /**
7496
8181
  * Which of the send's pending files get inlined into the request: the
@@ -7597,8 +8282,10 @@ ${content}
7597
8282
  });
7598
8283
  const prefixSegments = baseRequest._meta?.["prefixSegments"];
7599
8284
  let suspended;
8285
+ let finishReason;
7600
8286
  const emitter = (event) => {
7601
8287
  adapter(event);
8288
+ if (event.type === "run-done") finishReason = event.finishReason;
7602
8289
  if (event.type === "run-suspended") {
7603
8290
  const ticket = event.ticket;
7604
8291
  suspended = { pending: event.pending, ...typeof ticket === "string" ? { ticket } : {} };
@@ -7651,7 +8338,11 @@ ${content}
7651
8338
  // BARE uuid for the synthetic tool (the adapter renders `tool-<id>`).
7652
8339
  idGen: (prefix) => prefix === "synthetic-tool" ? uuid() : `${prefix}-${uuid()}`
7653
8340
  });
7654
- return { usage: usage ?? void 0, ...suspended ? { suspended } : {} };
8341
+ return {
8342
+ usage: usage ?? void 0,
8343
+ ...finishReason ? { finishReason } : {},
8344
+ ...suspended ? { suspended } : {}
8345
+ };
7655
8346
  }
7656
8347
  /**
7657
8348
  * The approval channel a run asks through — and the one a paused server run's calls
@@ -7681,15 +8372,15 @@ ${content}
7681
8372
  * nobody answered — the turn was stopped, or the channel gave up — and there is no
7682
8373
  * resume. A channel that CRASHES fails the turn, as it does for a run in the page.
7683
8374
  */
7684
- async _answerDeferred(targetElement, pending, signal, rowEvents, approvedBefore = /* @__PURE__ */ new Set(), approvedNow = /* @__PURE__ */ new Set()) {
8375
+ async _answerDeferred(targetElement, pending2, signal, rowEvents, approvedBefore = /* @__PURE__ */ new Set(), approvedNow = /* @__PURE__ */ new Set()) {
7685
8376
  const outcomes = {};
7686
8377
  const resolver = this._approvalResolverFor(targetElement, true);
7687
8378
  const refuseTheRest = (after) => {
7688
- for (const rest of pending.slice(after + 1)) {
8379
+ for (const rest of pending2.slice(after + 1)) {
7689
8380
  outcomes[rest.toolCallId] = { kind: "decision", approved: false, reason: NOT_RUN_AFTER_A_REFUSAL, by: "earlier-refusal" };
7690
8381
  }
7691
8382
  };
7692
- for (const [index, p] of pending.entries()) {
8383
+ for (const [index, p] of pending2.entries()) {
7693
8384
  if (signal.aborted) break;
7694
8385
  const call = { id: p.toolCallId, name: p.name, input: p.input ?? {} };
7695
8386
  if (p.reason === "approval") {
@@ -7802,7 +8493,7 @@ ${content}
7802
8493
  */
7803
8494
  _resumeMessages(targetElement, messageId, sent) {
7804
8495
  const reply = targetElement.getMessages?.().find((m) => m.id === messageId);
7805
- return [...sent, ...reply ? wireTurns(reply, (segments) => this._segmentsToText(segments), { openDeferred: true }) : []];
8496
+ return [...sent, ...reply ? wireTurns(reply, segmentsToWireText, { openDeferred: true }) : []];
7806
8497
  }
7807
8498
  /**
7808
8499
  * What the send of a reloaded turn put before its reply, rebuilt the way
@@ -7817,7 +8508,11 @@ ${content}
7817
8508
  const askedAt = before.map((m) => m.role).lastIndexOf("user");
7818
8509
  const historyOption = this.options.history ?? "viewport";
7819
8510
  const history = historyOption === "viewport" ? this._toHistoryMessages(before) : typeof historyOption === "function" ? historyOption(before) : [];
7820
- return [...this._systemMessages(), ...history, ...askedAt >= 0 ? this._wireHistory([before[askedAt]]) : []];
8511
+ return mergeAdjacentUserMessages([
8512
+ ...this._systemMessages(),
8513
+ ...history,
8514
+ ...askedAt >= 0 ? wireMessages([before[askedAt]]) : []
8515
+ ]);
7821
8516
  }
7822
8517
  /**
7823
8518
  * A new message closes a wait nobody answered. The paused calls of the last reply are
@@ -7849,8 +8544,8 @@ ${content}
7849
8544
  * @param target The chat — an element, or its id. Defaults to the one `targetResolver` names, else the first on the page.
7850
8545
  */
7851
8546
  async resumeDeferred(target) {
7852
- const host = typeof target === "string" ? this._byId(target) : target ?? this.options.targetResolver?.() ?? null;
7853
- const targetElement = host ? this._asRenderTarget(host) : this._scanForTarget();
8547
+ const host2 = typeof target === "string" ? this._byId(target) : target ?? this.options.targetResolver?.() ?? null;
8548
+ const targetElement = host2 ? this._asRenderTarget(host2) : this._scanForTarget();
7854
8549
  if (!targetElement) return false;
7855
8550
  const messages = targetElement.getMessages?.() ?? [];
7856
8551
  const last = [...messages].reverse().find((m) => m.role === "assistant");
@@ -7869,7 +8564,7 @@ ${content}
7869
8564
  this._resuming.add(last.id);
7870
8565
  try {
7871
8566
  const authConfig = provider.id ? await this._resolveAuth(provider.id) : void 0;
7872
- const pending = open.map((s) => ({
8567
+ const pending2 = open.map((s) => ({
7873
8568
  toolCallId: s.toolCall.id,
7874
8569
  name: s.toolCall.name,
7875
8570
  input: s.toolCall.input,
@@ -7877,7 +8572,12 @@ ${content}
7877
8572
  }));
7878
8573
  const base = this._resumeBase(messages, messages.indexOf(last));
7879
8574
  await this._streamTurn(targetElement, last.id, provider, base, recorded?.modelId ?? config.defaultModel ?? "", authConfig, {
7880
- resume: { pending, ticket },
8575
+ resume: { pending: pending2, ticket },
8576
+ rebuild: async () => {
8577
+ const now = targetElement.getMessages?.() ?? [];
8578
+ const at = now.findIndex((m) => m.id === last.id);
8579
+ return at >= 0 ? this._resumeBase(now, at) : base;
8580
+ },
7881
8581
  ...recorded?.temperature !== void 0 ? { temperature: recorded.temperature } : {}
7882
8582
  });
7883
8583
  } finally {
@@ -7957,7 +8657,8 @@ function createAgentStreamRunner(options) {
7957
8657
  let ended = false;
7958
8658
  for await (const line of readNdjson(response.body, opts.signal)) {
7959
8659
  if (line.type === "run-error") {
7960
- throw new AparteError(line.message || "The agent run failed.", AparteErrorCode.PROVIDER_ERROR, line.code ? { code: line.code } : void 0);
8660
+ const known2 = Object.values(AparteErrorCode).includes(line.code ?? "") ? line.code : void 0;
8661
+ throw new AparteError(line.message || "The agent run failed.", known2 ?? AparteErrorCode.PROVIDER_ERROR, line.code ? { code: line.code } : void 0);
7961
8662
  }
7962
8663
  if (line.type === "run-done" || line.type === "run-suspended") {
7963
8664
  usage = line.usage;
@@ -8364,8 +9065,95 @@ function installDialogTriggersOnce(doc = document) {
8364
9065
  doc.addEventListener("pointerup", onPointerUp, true);
8365
9066
  doc.addEventListener("click", onClick);
8366
9067
  }
9068
+ const ATTR = "defer-hydration";
9069
+ const waiting = /* @__PURE__ */ new WeakMap();
9070
+ const holderOf = /* @__PURE__ */ new WeakMap();
9071
+ const pending = /* @__PURE__ */ new WeakSet();
9072
+ const live = /* @__PURE__ */ new WeakSet();
9073
+ const observers = /* @__PURE__ */ new WeakMap();
9074
+ const patched = /* @__PURE__ */ new WeakSet();
9075
+ function isHeld(el2) {
9076
+ if (live.has(el2)) return false;
9077
+ if (holderOf.has(el2)) return true;
9078
+ const holder = el2.closest(`[${ATTR}]`);
9079
+ if (!holder) return false;
9080
+ holderOf.set(el2, holder);
9081
+ let list = waiting.get(holder);
9082
+ if (!list) {
9083
+ list = [];
9084
+ waiting.set(holder, list);
9085
+ const observer = new MutationObserver(() => {
9086
+ if (!holder.hasAttribute(ATTR)) release(holder);
9087
+ });
9088
+ observer.observe(holder, { attributes: true, attributeFilter: [ATTR] });
9089
+ observers.set(holder, observer);
9090
+ }
9091
+ list.push(el2);
9092
+ return true;
9093
+ }
9094
+ function release(holder) {
9095
+ observers.get(holder)?.disconnect();
9096
+ observers.delete(holder);
9097
+ const list = waiting.get(holder) ?? [];
9098
+ waiting.delete(holder);
9099
+ for (const el2 of list) {
9100
+ holderOf.delete(el2);
9101
+ if (el2.isConnected && !live.has(el2)) el2.connectedCallback?.();
9102
+ }
9103
+ }
9104
+ function releaseDeferredHydration(root) {
9105
+ if (!root) return;
9106
+ if (root.hasAttribute(ATTR)) root.removeAttribute(ATTR);
9107
+ else if (!waiting.has(root)) return;
9108
+ release(root);
9109
+ }
9110
+ function deferHydrationOf(ctor) {
9111
+ if (patched.has(ctor)) return;
9112
+ patched.add(ctor);
9113
+ const proto = ctor.prototype;
9114
+ const connected = proto.connectedCallback;
9115
+ const changed = proto.attributeChangedCallback;
9116
+ const disconnected = proto.disconnectedCallback;
9117
+ const observed = ctor.observedAttributes ?? [];
9118
+ proto.connectedCallback = function() {
9119
+ if (isHeld(this)) {
9120
+ pending.add(this);
9121
+ return;
9122
+ }
9123
+ if (pending.has(this)) {
9124
+ pending.delete(this);
9125
+ for (const name of observed) {
9126
+ const value = this.getAttribute(name);
9127
+ if (value !== null) changed?.call(this, name, null, value);
9128
+ }
9129
+ }
9130
+ live.add(this);
9131
+ connected?.call(this);
9132
+ };
9133
+ if (changed) {
9134
+ proto.attributeChangedCallback = function(name, oldValue, newValue) {
9135
+ if (isHeld(this)) {
9136
+ pending.add(this);
9137
+ return;
9138
+ }
9139
+ changed.call(this, name, oldValue, newValue);
9140
+ };
9141
+ }
9142
+ proto.disconnectedCallback = function() {
9143
+ const holder = holderOf.get(this);
9144
+ if (holder) {
9145
+ holderOf.delete(this);
9146
+ const list = waiting.get(holder);
9147
+ if (list) list.splice(list.indexOf(this), 1);
9148
+ return;
9149
+ }
9150
+ if (pending.has(this)) return;
9151
+ live.delete(this);
9152
+ disconnected?.call(this);
9153
+ };
9154
+ }
8367
9155
  export {
8368
- registerSegmentRenderer as $,
9156
+ isSegmentSettled as $,
8369
9157
  APARTE_CONFIG_CHANGE as A,
8370
9158
  contextConfig as B,
8371
9159
  copyText as C,
@@ -8376,53 +9164,70 @@ export {
8376
9164
  cssEscape as H,
8377
9165
  declineDefaultRenderers as I,
8378
9166
  defaultSanitizer as J,
8379
- detachConfig as K,
8380
- escapeAttr as L,
8381
- escapeHtml as M,
8382
- filesToAttachments as N,
8383
- getAllRenderers as O,
8384
- getSegmentRenderer as P,
8385
- installDefaultRenderersOnce as Q,
8386
- installDialogTriggersOnce as R,
8387
- isAwaitingReply as S,
8388
- isFormatAdapter as T,
8389
- isSafeUrl as U,
8390
- isSegmentSettled as V,
8391
- parseAparteEventStream as W,
8392
- parseMarkdownToSegments as X,
8393
- populateBubbleFromMessage as Y,
8394
- readableToAsyncIterable as Z,
8395
- registerDefaultRenderers as _,
9167
+ deferHydrationOf as K,
9168
+ defineMountedSegmentRenderer as L,
9169
+ defineMountedToolRenderer as M,
9170
+ detachConfig as N,
9171
+ escapeAttr as O,
9172
+ escapeHtml as P,
9173
+ estimateTokens as Q,
9174
+ estimateWireTokens as R,
9175
+ filesToAttachments as S,
9176
+ getAllRenderers as T,
9177
+ getSegmentRenderer as U,
9178
+ historyFromCheckpoint as V,
9179
+ installDefaultRenderersOnce as W,
9180
+ installDialogTriggersOnce as X,
9181
+ isAwaitingReply as Y,
9182
+ isFormatAdapter as Z,
9183
+ isSafeUrl as _,
8396
9184
  APARTE_CONVERSATION_SCHEMA_VERSION as a,
8397
- requestUserInput as a0,
8398
- resolveConfig as a1,
8399
- revokeAttachmentUrls as a2,
8400
- runWithConfig as a3,
8401
- segmentDuration as a4,
8402
- segmentTiming as a5,
8403
- subscribeConfigChange as a6,
8404
- toolRefusal as a7,
8405
- unregisterSegmentRenderer as a8,
8406
- uuid as a9,
8407
- eventOrigin as aa,
8408
- activeElementIn as ab,
8409
- gateGlide as ac,
8410
- renderingVersionOf as ad,
8411
- mergeSegmentUpdate as ae,
8412
- writeStreamedMarkdown as af,
8413
- stampSegmentActivity as ag,
8414
- stampSegmentOnInsert as ah,
8415
- stampSegmentOnUpdate as ai,
8416
- renumberSegments as aj,
8417
- openSegmentIds as ak,
8418
- isTerminalStatus as al,
8419
- adoptMessageSegments as am,
8420
- APARTE_DERIVED_SEGMENTS as an,
8421
- rememberConversationStatus as ao,
8422
- tellConversationStatus as ap,
8423
- conversationStatusOf as aq,
8424
- chatBoundaryOf as ar,
8425
- rootOf as as,
9185
+ measureContext as a0,
9186
+ overrideSegmentRenderer as a1,
9187
+ parseAparteEventStream as a2,
9188
+ parseMarkdownToSegments as a3,
9189
+ populateBubbleFromMessage as a4,
9190
+ readableToAsyncIterable as a5,
9191
+ registerDefaultRenderers as a6,
9192
+ registerSegmentRenderer as a7,
9193
+ releaseDeferredHydration as a8,
9194
+ requestUserInput as a9,
9195
+ renumberSegments as aA,
9196
+ openSegmentIds as aB,
9197
+ isTerminalStatus as aC,
9198
+ adoptMessageSegments as aD,
9199
+ APARTE_DERIVED_SEGMENTS as aE,
9200
+ rememberConversationStatus as aF,
9201
+ tellConversationStatus as aG,
9202
+ conversationStatusOf as aH,
9203
+ usageTokens as aI,
9204
+ rootOf as aJ,
9205
+ resolveConfig as aa,
9206
+ revokeAttachmentUrls as ab,
9207
+ runWithConfig as ac,
9208
+ segmentDuration as ad,
9209
+ segmentTiming as ae,
9210
+ subscribeConfigChange as af,
9211
+ toToolCallView as ag,
9212
+ toolRefusal as ah,
9213
+ unregisterSegmentRenderer as ai,
9214
+ uuid as aj,
9215
+ eventOrigin as ak,
9216
+ activeElementIn as al,
9217
+ gateGlide as am,
9218
+ chatBoundaryOf as an,
9219
+ toolCallRenderer as ao,
9220
+ toolRendererThatDrew as ap,
9221
+ isMountedRenderer as aq,
9222
+ renderingVersionOf as ar,
9223
+ onRenderersChange as as,
9224
+ mergeSegmentUpdate as at,
9225
+ writeStreamedMarkdown as au,
9226
+ CheckpointStateWatcher as av,
9227
+ chatIdOf as aw,
9228
+ stampSegmentActivity as ax,
9229
+ stampSegmentOnInsert as ay,
9230
+ stampSegmentOnUpdate as az,
8426
9231
  APARTE_DEFAULT_BUBBLE_ACTIONS as b,
8427
9232
  APARTE_DEFAULT_HOST_HANDLERS as c,
8428
9233
  APARTE_DEFAULT_ICON_FALLBACKS as d,
@@ -8449,4 +9254,4 @@ export {
8449
9254
  collectRendererStyles as y,
8450
9255
  contentToText as z
8451
9256
  };
8452
- //# sourceMappingURL=dialog-triggers-BqXRdHWZ.js.map
9257
+ //# sourceMappingURL=defer-hydration-8vI8xNNx.js.map