@aparte/core 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/agent-runner.d.ts.map +1 -1
- package/dist/client/aparte-client.d.ts +38 -76
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/client/stream-adapter.d.ts.map +1 -1
- package/dist/client/wire-history.d.ts +78 -0
- package/dist/client/wire-history.d.ts.map +1 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts +152 -2
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/bubble/bubble-sync.d.ts +5 -1
- package/dist/components/bubble/bubble-sync.d.ts.map +1 -1
- package/dist/components/bubble/checkpoint-separator.d.ts +40 -0
- package/dist/components/bubble/checkpoint-separator.d.ts.map +1 -0
- package/dist/components/chat/aparte-chat.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-toolbar.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer.d.ts.map +1 -1
- package/dist/components/context/aparte-context.d.ts +157 -25
- package/dist/components/context/aparte-context.d.ts.map +1 -1
- package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
- package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
- package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -1
- package/dist/components/sidebar/aparte-sidebar.d.ts.map +1 -1
- package/dist/components/split/aparte-split.d.ts.map +1 -1
- package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
- package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +7 -0
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +47 -1
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/locale.d.ts +62 -4
- package/dist/config/locale.d.ts.map +1 -1
- package/dist/context/checkpoint-state.d.ts +70 -0
- package/dist/context/checkpoint-state.d.ts.map +1 -0
- package/dist/context/checkpoint.d.ts +28 -0
- package/dist/context/checkpoint.d.ts.map +1 -0
- package/dist/context/measure.d.ts +148 -0
- package/dist/context/measure.d.ts.map +1 -0
- package/dist/context/restore.d.ts +24 -0
- package/dist/context/restore.d.ts.map +1 -0
- package/dist/conversations/conversation-controller.d.ts +13 -4
- package/dist/conversations/conversation-controller.d.ts.map +1 -1
- package/dist/conversations/conversation-manager.d.ts +6 -0
- package/dist/conversations/conversation-manager.d.ts.map +1 -1
- package/dist/custom-elements.json +15347 -12878
- package/dist/{dialog-triggers-BqXRdHWZ.js → defer-hydration-8vI8xNNx.js} +1179 -374
- package/dist/defer-hydration-8vI8xNNx.js.map +1 -0
- package/dist/generated/element-attributes.d.ts +8 -2
- package/dist/generated/element-attributes.d.ts.map +1 -1
- package/dist/{glyphs-C8z2oJUg.js → glyphs-2bym5jl5.js} +1 -1
- package/dist/{glyphs-C8z2oJUg.js.map → glyphs-2bym5jl5.js.map} +1 -1
- package/dist/host/aparte-chat-host.d.ts +41 -2
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/icons.js +2 -2
- package/dist/index.css +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1059 -95
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +6 -2
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +39 -29
- package/dist/index.node.js.map +1 -1
- package/dist/interop/defer-hydration.d.ts +34 -0
- package/dist/interop/defer-hydration.d.ts.map +1 -0
- package/dist/primitives/icon/aparte-icon.d.ts.map +1 -1
- package/dist/primitives/progress-spinner/aparte-progress-spinner.d.ts.map +1 -1
- package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
- package/dist/primitives/select/aparte-option.d.ts.map +1 -1
- package/dist/primitives/select/aparte-select.d.ts.map +1 -1
- package/dist/renderers/index.d.ts +2 -1
- package/dist/renderers/index.d.ts.map +1 -1
- package/dist/renderers/mounted-renderer.d.ts +34 -0
- package/dist/renderers/mounted-renderer.d.ts.map +1 -0
- package/dist/renderers/segment-renderers.d.ts +14 -1
- package/dist/renderers/segment-renderers.d.ts.map +1 -1
- package/dist/renderers/segments/tool-call.d.ts +2 -1
- package/dist/renderers/segments/tool-call.d.ts.map +1 -1
- package/dist/transport/agent-handler.d.ts.map +1 -1
- package/dist/transport/backend-handler.d.ts.map +1 -1
- package/dist/transport/types.d.ts +24 -4
- package/dist/transport/types.d.ts.map +1 -1
- package/dist/types/chat.d.ts +16 -1
- package/dist/types/chat.d.ts.map +1 -1
- package/dist/types/compactor.d.ts +104 -0
- package/dist/types/compactor.d.ts.map +1 -0
- package/dist/types/errors.d.ts +2 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/events.d.ts +50 -3
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +5 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/model-provider.d.ts +3 -0
- package/dist/types/model-provider.d.ts.map +1 -1
- package/dist/types/models.d.ts +43 -3
- package/dist/types/models.d.ts.map +1 -1
- package/dist/types/segments.d.ts +22 -4
- package/dist/types/segments.d.ts.map +1 -1
- package/dist/types/tools.d.ts +56 -0
- package/dist/types/tools.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/dialog-triggers-BqXRdHWZ.js.map +0 -1
|
@@ -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-
|
|
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
|
|
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
|
-
|
|
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} ${
|
|
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
|
|
210
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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" && !
|
|
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
|
|
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,
|
|
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
|
|
1074
|
-
return
|
|
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, {
|
|
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:
|
|
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(
|
|
3499
|
+
function writeStreamedMarkdown(host2, contentEl, content, streaming) {
|
|
3333
3500
|
if (streaming) {
|
|
3334
|
-
if (
|
|
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
|
-
|
|
3506
|
+
host2._aparteSmd = { renderer, written: 0 };
|
|
3340
3507
|
} else {
|
|
3341
|
-
|
|
3508
|
+
host2._aparteSmd = null;
|
|
3342
3509
|
}
|
|
3343
3510
|
}
|
|
3344
|
-
const smd =
|
|
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 (
|
|
3354
|
-
const smd =
|
|
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
|
-
|
|
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
|
|
3591
|
-
if (!
|
|
3592
|
-
if (cut <= highlightedLen(
|
|
3593
|
-
|
|
3594
|
-
const codeEl =
|
|
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
|
-
|
|
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(
|
|
3824
|
-
for (let i = 0; i <
|
|
3825
|
-
const l =
|
|
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
|
-
|
|
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 =
|
|
4000
|
-
const outcome =
|
|
4001
|
-
if (
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
|
4085
|
-
if (
|
|
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
|
-
|
|
4113
|
-
|
|
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
|
|
4131
|
-
const input =
|
|
4132
|
-
const outcome =
|
|
4133
|
-
const wantsDetail =
|
|
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 &&
|
|
4136
|
-
rebuild(drawDefault(segment,
|
|
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 (
|
|
4147
|
-
setBody(element,
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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
|
|
4293
|
-
const
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
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
|
|
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
|
|
5325
|
+
const host2 = this._binding.host;
|
|
4999
5326
|
if (this._onSendCapture) {
|
|
5000
|
-
|
|
5327
|
+
host2.removeEventListener("aparte-send", this._onSendCapture, { capture: true });
|
|
5001
5328
|
this._onSendCapture = null;
|
|
5002
5329
|
}
|
|
5003
5330
|
if (this._onPathChanged) {
|
|
5004
|
-
|
|
5331
|
+
host2.removeEventListener("aparte-path-changed", this._onPathChanged);
|
|
5005
5332
|
this._onPathChanged = null;
|
|
5006
5333
|
}
|
|
5007
5334
|
if (this._onMessageStart) {
|
|
5008
|
-
|
|
5335
|
+
host2.removeEventListener("aparte-message-start", this._onMessageStart);
|
|
5009
5336
|
this._onMessageStart = null;
|
|
5010
5337
|
}
|
|
5011
5338
|
if (this._onMessageDone) {
|
|
5012
|
-
|
|
5339
|
+
host2.removeEventListener("aparte-message-done", this._onMessageDone);
|
|
5013
5340
|
this._onMessageDone = null;
|
|
5014
5341
|
}
|
|
5015
5342
|
if (this._onMessageError) {
|
|
5016
|
-
|
|
5343
|
+
host2.removeEventListener("aparte-message-error", this._onMessageError);
|
|
5017
5344
|
this._onMessageError = null;
|
|
5018
5345
|
}
|
|
5019
5346
|
if (this._onMessageAborted) {
|
|
5020
|
-
|
|
5347
|
+
host2.removeEventListener("aparte-message-aborted", this._onMessageAborted);
|
|
5021
5348
|
this._onMessageAborted = null;
|
|
5022
5349
|
}
|
|
5023
5350
|
if (this._onMessagePaused) {
|
|
5024
|
-
|
|
5351
|
+
host2.removeEventListener("aparte-message-paused", this._onMessagePaused);
|
|
5025
5352
|
this._onMessagePaused = null;
|
|
5026
5353
|
}
|
|
5027
5354
|
if (this._onMessageResumed) {
|
|
5028
|
-
|
|
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
|
|
5182
|
-
* a framework store reaches the binding only when that framework renders, so a
|
|
5183
|
-
* the microtask wrote the conversation as it stood before the
|
|
5184
|
-
* run
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
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(
|
|
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
|
-
/**
|
|
5281
|
-
|
|
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
|
|
5324
|
-
if (!
|
|
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(
|
|
5332
|
-
this._installLifecycleListeners(
|
|
5333
|
-
this._teardown.push(
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
5624
|
-
if (!tokens && !
|
|
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 (
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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.
|
|
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.
|
|
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(
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
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.
|
|
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
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
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
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
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.
|
|
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
|
|
6738
|
-
void this.resumeDeferred(
|
|
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
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
6971
|
-
|
|
6972
|
-
if (
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
8031
|
+
const { summary, messages: kept } = historyFromCheckpoint(messages);
|
|
8032
|
+
return [...this._summaryMessage(summary), ...wireMessages(kept)];
|
|
7255
8033
|
}
|
|
7256
8034
|
/**
|
|
7257
|
-
*
|
|
7258
|
-
*
|
|
7259
|
-
*
|
|
7260
|
-
*
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
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
|
-
|
|
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(
|
|
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" && !
|
|
8174
|
+
if (m.role === "assistant" && m.status !== "error" && !isInFlight(m)) {
|
|
7421
8175
|
cutoff = i + 1;
|
|
7422
8176
|
}
|
|
7423
8177
|
}
|
|
7424
|
-
return this.
|
|
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 {
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
7853
|
-
const targetElement =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
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
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
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=
|
|
9257
|
+
//# sourceMappingURL=defer-hydration-8vI8xNNx.js.map
|