@aparte/core 0.22.0 → 0.24.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/aparte-client.d.ts +10 -22
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/client/stream-adapter.d.ts +16 -0
- package/dist/client/stream-adapter.d.ts.map +1 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/conversation-list/aparte-conversation-list.d.ts +38 -0
- package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
- package/dist/components/conversation-list/index.d.ts +1 -1
- package/dist/components/conversation-list/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +10 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +76 -19
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/error-renderer.d.ts +3 -1
- package/dist/config/error-renderer.d.ts.map +1 -1
- package/dist/config/locale.d.ts +49 -4
- package/dist/config/locale.d.ts.map +1 -1
- package/dist/config/tool-table.d.ts +30 -0
- package/dist/config/tool-table.d.ts.map +1 -0
- package/dist/conversations/conversation-controller.d.ts +19 -0
- package/dist/conversations/conversation-controller.d.ts.map +1 -1
- package/dist/conversations/conversation-status.d.ts +27 -0
- package/dist/conversations/conversation-status.d.ts.map +1 -0
- package/dist/custom-elements.json +12058 -11106
- package/dist/{dialog-triggers-3Rk7PuZa.js → dialog-triggers-BqXRdHWZ.js} +746 -205
- package/dist/dialog-triggers-BqXRdHWZ.js.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +91 -18
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +3 -2
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +4 -3
- package/dist/index.node.js.map +1 -1
- package/dist/interop/element-props.d.ts +1 -1
- package/dist/interop/element-props.d.ts.map +1 -1
- package/dist/renderers/copy-button.d.ts +12 -0
- package/dist/renderers/copy-button.d.ts.map +1 -0
- package/dist/renderers/segment-renderers.d.ts +6 -3
- package/dist/renderers/segment-renderers.d.ts.map +1 -1
- package/dist/renderers/segments/code.d.ts.map +1 -1
- package/dist/renderers/segments/error.d.ts.map +1 -1
- package/dist/renderers/segments/thinking.d.ts.map +1 -1
- package/dist/renderers/segments/tool-call.d.ts.map +1 -1
- package/dist/transport/agent-handler.d.ts.map +1 -1
- package/dist/types/errors.d.ts +2 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/event-map.d.ts +4 -2
- package/dist/types/event-map.d.ts.map +1 -1
- package/dist/types/events.d.ts +15 -0
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/segments.d.ts +42 -2
- package/dist/types/segments.d.ts.map +1 -1
- package/dist/types/tools.d.ts +77 -0
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/utils/format-duration.d.ts +9 -0
- package/dist/utils/format-duration.d.ts.map +1 -0
- package/dist/utils/tool-line.d.ts +23 -0
- package/dist/utils/tool-line.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/dialog-triggers-3Rk7PuZa.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { unansweredTail, runStreamAgent } from "@aparte/engine";
|
|
1
|
+
import { toolRefusal as toolRefusal$1, unansweredTail, runStreamAgent } from "@aparte/engine";
|
|
2
2
|
import { A as APARTE_ICON_GLYPHS } from "./glyphs-C8z2oJUg.js";
|
|
3
3
|
function contentToText(content) {
|
|
4
4
|
if (typeof content === "string") return content;
|
|
@@ -18,6 +18,7 @@ var AparteErrorCode = /* @__PURE__ */ ((AparteErrorCode2) => {
|
|
|
18
18
|
AparteErrorCode2["PROVIDER_ERROR"] = "PROVIDER_ERROR";
|
|
19
19
|
AparteErrorCode2["PROVIDER_UNAVAILABLE"] = "PROVIDER_UNAVAILABLE";
|
|
20
20
|
AparteErrorCode2["PROVIDER_POLICY"] = "PROVIDER_POLICY";
|
|
21
|
+
AparteErrorCode2["MAX_TURNS_EXCEEDED"] = "MAX_TURNS_EXCEEDED";
|
|
21
22
|
AparteErrorCode2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
|
|
22
23
|
return AparteErrorCode2;
|
|
23
24
|
})(AparteErrorCode || {});
|
|
@@ -98,7 +99,7 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
98
99
|
elicitationNo: "No",
|
|
99
100
|
elicitationAnswerLabel: "Your answer",
|
|
100
101
|
approvalInstructionPlaceholder: "Or tell it what to do instead…",
|
|
101
|
-
approvalAsk: "
|
|
102
|
+
approvalAsk: "Allow {tool}?",
|
|
102
103
|
approvalWaiting: "Waiting",
|
|
103
104
|
scrollToBottom: "Scroll to bottom",
|
|
104
105
|
suggestionsLabel: "Suggested prompts",
|
|
@@ -111,6 +112,18 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
111
112
|
toolFailed: "Failed",
|
|
112
113
|
toolInput: "Input",
|
|
113
114
|
toolOutput: "Output",
|
|
115
|
+
toolRejectedSaid: "You said: “{instruction}”",
|
|
116
|
+
toolRefusedByPolicy: "Refused by policy",
|
|
117
|
+
toolNotRunAfterRefusal: "Not run: an earlier call was refused",
|
|
118
|
+
toolTimedOut: "Timed out",
|
|
119
|
+
toolUnavailable: "Not available here",
|
|
120
|
+
toolNoApprover: "No one could approve it",
|
|
121
|
+
toolTurnLimit: "Turn limit reached",
|
|
122
|
+
maxTurnsReached: "Stopped after {n} turns to avoid an endless loop.",
|
|
123
|
+
questionDeclined: "You declined to answer.",
|
|
124
|
+
artifactNotCreated: "Not created",
|
|
125
|
+
thoughtFor: "Thought for {duration}",
|
|
126
|
+
thought: "Thought",
|
|
114
127
|
approvalOptionsLabel: "Your decision",
|
|
115
128
|
approvalArgsLabel: "Arguments",
|
|
116
129
|
actionUpload: "Attach file",
|
|
@@ -160,6 +173,8 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
160
173
|
archiveConversation: "Archive",
|
|
161
174
|
unarchiveConversation: "Unarchive",
|
|
162
175
|
conversationActions: "Conversation actions",
|
|
176
|
+
conversationRunning: "Answering",
|
|
177
|
+
conversationWaiting: "Waiting for you",
|
|
163
178
|
renameConversation: "Rename",
|
|
164
179
|
conversationTitle: "Conversation title",
|
|
165
180
|
pinConversation: "Pin",
|
|
@@ -1076,25 +1091,33 @@ function dispatchLifecycleEvent(target, name, detail, targetId) {
|
|
|
1076
1091
|
const stamped = detail && typeof detail === "object" ? { targetId: id, ...detail } : detail;
|
|
1077
1092
|
target.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail: stamped }));
|
|
1078
1093
|
}
|
|
1094
|
+
const toolRefusal = toolRefusal$1;
|
|
1079
1095
|
function createStreamAdapter(opts) {
|
|
1080
1096
|
const { target, config, messageId, targetId } = opts;
|
|
1081
1097
|
let parser = new AparteStreamParser({ blocks: config.getStreamBlocks() });
|
|
1082
1098
|
let streaming = /* @__PURE__ */ new Set();
|
|
1083
1099
|
let thinkingId = null;
|
|
1084
1100
|
const toolSegments = /* @__PURE__ */ new Map();
|
|
1085
|
-
const addedTools = /* @__PURE__ */ new Set();
|
|
1086
1101
|
const settle = (toolCallId, update) => {
|
|
1087
1102
|
const id = `tool-${toolCallId}`;
|
|
1088
1103
|
target.updateSegment?.(id, toolSegments.has(id) ? update : { ...update, deferred: void 0 });
|
|
1089
1104
|
};
|
|
1090
1105
|
let thinkingContent = "";
|
|
1091
1106
|
let thinkingCollapsed = false;
|
|
1107
|
+
const settleThinking = (fold) => {
|
|
1108
|
+
if (!thinkingId || thinkingCollapsed) return;
|
|
1109
|
+
const chatConfig = typeof Element !== "undefined" && target instanceof Element ? resolveConfig(target) : config;
|
|
1110
|
+
const keepOpen = !fold || chatConfig.getSegmentDefaults("thinking")?.collapsed === false;
|
|
1111
|
+
target.updateSegment?.(thinkingId, keepOpen ? { isStreaming: false } : { collapsed: true, isStreaming: false });
|
|
1112
|
+
thinkingCollapsed = true;
|
|
1113
|
+
};
|
|
1092
1114
|
return (e) => {
|
|
1093
1115
|
switch (e.type) {
|
|
1094
1116
|
case "run-start":
|
|
1095
1117
|
target.updateMessage?.(messageId, { status: "streaming" });
|
|
1096
1118
|
break;
|
|
1097
1119
|
case "turn-start":
|
|
1120
|
+
settleThinking(false);
|
|
1098
1121
|
parser = new AparteStreamParser({ blocks: config.getStreamBlocks() });
|
|
1099
1122
|
streaming = /* @__PURE__ */ new Set();
|
|
1100
1123
|
thinkingId = null;
|
|
@@ -1107,7 +1130,8 @@ function createStreamAdapter(opts) {
|
|
|
1107
1130
|
const seg = {
|
|
1108
1131
|
id: `think-${uuid()}`,
|
|
1109
1132
|
type: "thinking",
|
|
1110
|
-
content: thinkingContent
|
|
1133
|
+
content: thinkingContent,
|
|
1134
|
+
isStreaming: true
|
|
1111
1135
|
};
|
|
1112
1136
|
thinkingId = seg.id;
|
|
1113
1137
|
streaming.add(seg.id);
|
|
@@ -1118,12 +1142,7 @@ function createStreamAdapter(opts) {
|
|
|
1118
1142
|
break;
|
|
1119
1143
|
}
|
|
1120
1144
|
case "text-delta": {
|
|
1121
|
-
|
|
1122
|
-
const chatConfig = typeof Element !== "undefined" && target instanceof Element ? resolveConfig(target) : config;
|
|
1123
|
-
const keepOpen = chatConfig.getSegmentDefaults("thinking")?.collapsed === false;
|
|
1124
|
-
target.updateSegment?.(thinkingId, keepOpen ? { isStreaming: false } : { collapsed: true, isStreaming: false });
|
|
1125
|
-
thinkingCollapsed = true;
|
|
1126
|
-
}
|
|
1145
|
+
settleThinking(true);
|
|
1127
1146
|
const result = parser.parse(e.delta);
|
|
1128
1147
|
for (const segment of result.segments) {
|
|
1129
1148
|
if (!streaming.has(segment.id)) {
|
|
@@ -1153,6 +1172,7 @@ function createStreamAdapter(opts) {
|
|
|
1153
1172
|
break;
|
|
1154
1173
|
}
|
|
1155
1174
|
case "tool-start": {
|
|
1175
|
+
settleThinking(false);
|
|
1156
1176
|
const toolSeg = {
|
|
1157
1177
|
id: `tool-${e.toolCallId}`,
|
|
1158
1178
|
type: "tool_call",
|
|
@@ -1161,17 +1181,8 @@ function createStreamAdapter(opts) {
|
|
|
1161
1181
|
};
|
|
1162
1182
|
toolSegments.set(toolSeg.id, toolSeg);
|
|
1163
1183
|
const renderer = config.getToolRenderer(e.name);
|
|
1164
|
-
if (renderer)
|
|
1165
|
-
|
|
1166
|
-
const html = renderer.render(toolSeg);
|
|
1167
|
-
if (html) {
|
|
1168
|
-
target.addSegment?.(toolSeg);
|
|
1169
|
-
addedTools.add(toolSeg.id);
|
|
1170
|
-
}
|
|
1171
|
-
} else {
|
|
1172
|
-
target.addSegment?.(toolSeg);
|
|
1173
|
-
addedTools.add(toolSeg.id);
|
|
1174
|
-
}
|
|
1184
|
+
if (renderer) injectToolRendererStyles(e.name, renderer);
|
|
1185
|
+
target.addSegment?.(toolSeg);
|
|
1175
1186
|
break;
|
|
1176
1187
|
}
|
|
1177
1188
|
case "tool-awaiting-approval":
|
|
@@ -1182,7 +1193,11 @@ function createStreamAdapter(opts) {
|
|
|
1182
1193
|
settle(e.toolCallId, { status: "pending" });
|
|
1183
1194
|
break;
|
|
1184
1195
|
case "tool-rejected":
|
|
1185
|
-
settle(e.toolCallId, {
|
|
1196
|
+
settle(e.toolCallId, {
|
|
1197
|
+
status: "rejected",
|
|
1198
|
+
result: e.reason,
|
|
1199
|
+
...e.by ? { refusal: e.instruction ? { by: e.by, instruction: e.instruction } : { by: e.by } } : {}
|
|
1200
|
+
});
|
|
1186
1201
|
break;
|
|
1187
1202
|
case "tool-resolved":
|
|
1188
1203
|
settle(e.toolCallId, {
|
|
@@ -1192,7 +1207,7 @@ function createStreamAdapter(opts) {
|
|
|
1192
1207
|
});
|
|
1193
1208
|
break;
|
|
1194
1209
|
case "tool-aborted":
|
|
1195
|
-
settle(e.toolCallId, { status: "aborted" });
|
|
1210
|
+
settle(e.toolCallId, e.cause ? { status: "aborted", abortCause: e.cause } : { status: "aborted" });
|
|
1196
1211
|
break;
|
|
1197
1212
|
case "tool-failed":
|
|
1198
1213
|
settle(e.toolCallId, { status: "failed", result: e.error });
|
|
@@ -1202,11 +1217,14 @@ function createStreamAdapter(opts) {
|
|
|
1202
1217
|
target.addSegment?.({
|
|
1203
1218
|
id: `max-turns-${uuid()}`,
|
|
1204
1219
|
type: "error",
|
|
1205
|
-
|
|
1206
|
-
|
|
1220
|
+
// The CHAT's text, in its language — resolved from the target, as the
|
|
1221
|
+
// reasoning default is: a client on the global config also answers a
|
|
1222
|
+
// chat with its own.
|
|
1223
|
+
content: (typeof Element !== "undefined" && target instanceof Element ? resolveConfig(target) : config).t("maxTurnsReached").replace("{n}", () => String(e.limit)),
|
|
1224
|
+
code: AparteErrorCode.MAX_TURNS_EXCEEDED
|
|
1207
1225
|
});
|
|
1208
1226
|
} else if (e.toolCallId) {
|
|
1209
|
-
settle(e.toolCallId, { status: "aborted" });
|
|
1227
|
+
settle(e.toolCallId, { status: "aborted", abortCause: "turn-limit" });
|
|
1210
1228
|
}
|
|
1211
1229
|
break;
|
|
1212
1230
|
case "run-aborted":
|
|
@@ -1217,15 +1235,7 @@ function createStreamAdapter(opts) {
|
|
|
1217
1235
|
for (const p of e.pending) {
|
|
1218
1236
|
const id = `tool-${p.toolCallId}`;
|
|
1219
1237
|
const deferred = { reason: p.reason, ...typeof ticket === "string" ? { ticket } : {} };
|
|
1220
|
-
|
|
1221
|
-
target.updateSegment?.(id, { deferred });
|
|
1222
|
-
} else {
|
|
1223
|
-
const seg = toolSegments.get(id);
|
|
1224
|
-
if (seg) {
|
|
1225
|
-
target.addSegment?.({ ...seg, deferred });
|
|
1226
|
-
addedTools.add(id);
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1238
|
+
target.updateSegment?.(id, { deferred });
|
|
1229
1239
|
}
|
|
1230
1240
|
target.updateMessage?.(messageId, { status: "completed" });
|
|
1231
1241
|
if (e.usage) {
|
|
@@ -1494,7 +1504,8 @@ function createAparteAgentHandler(options) {
|
|
|
1494
1504
|
// Reached only for a server `deny`: an `ask` is deferred to the page.
|
|
1495
1505
|
approvalResolver: async (call) => ({
|
|
1496
1506
|
approved: false,
|
|
1497
|
-
reason: rule(call)?.reason?.trim() || "Tool execution was refused by the server policy."
|
|
1507
|
+
reason: rule(call)?.reason?.trim() || "Tool execution was refused by the server policy.",
|
|
1508
|
+
by: "policy"
|
|
1498
1509
|
}),
|
|
1499
1510
|
deferral: { outcomes },
|
|
1500
1511
|
emitter,
|
|
@@ -1848,6 +1859,56 @@ function chatBoundaryOf(el2) {
|
|
|
1848
1859
|
}
|
|
1849
1860
|
return null;
|
|
1850
1861
|
}
|
|
1862
|
+
class ToolTable {
|
|
1863
|
+
exact = /* @__PURE__ */ new Map();
|
|
1864
|
+
// Keyed by the pattern's spelling, so two RegExp instances of one pattern are one
|
|
1865
|
+
// entry; a Map keeps first-insertion order, which is the order patterns are tried in.
|
|
1866
|
+
patterns = /* @__PURE__ */ new Map();
|
|
1867
|
+
any;
|
|
1868
|
+
/** The value registered under exactly this matcher — not a resolution. */
|
|
1869
|
+
get(match) {
|
|
1870
|
+
if (match instanceof RegExp) return this.patterns.get(String(match))?.value;
|
|
1871
|
+
return match === "*" ? this.any : this.exact.get(match);
|
|
1872
|
+
}
|
|
1873
|
+
set(match, value) {
|
|
1874
|
+
if (match instanceof RegExp) {
|
|
1875
|
+
const key = String(match);
|
|
1876
|
+
const entry = this.patterns.get(key);
|
|
1877
|
+
if (entry) entry.value = value;
|
|
1878
|
+
else this.patterns.set(key, { re: match, value });
|
|
1879
|
+
} else if (match === "*") {
|
|
1880
|
+
this.any = value;
|
|
1881
|
+
} else {
|
|
1882
|
+
this.exact.set(match, value);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
delete(match) {
|
|
1886
|
+
if (match instanceof RegExp) this.patterns.delete(String(match));
|
|
1887
|
+
else if (match === "*") this.any = void 0;
|
|
1888
|
+
else this.exact.delete(match);
|
|
1889
|
+
}
|
|
1890
|
+
/** The value that applies to a tool named `name`, and how specifically it names it. */
|
|
1891
|
+
resolve(name) {
|
|
1892
|
+
if (typeof name !== "string") return void 0;
|
|
1893
|
+
const own = this.exact.get(name);
|
|
1894
|
+
if (own !== void 0) return { value: own, level: "exact" };
|
|
1895
|
+
for (const { re, value } of this.patterns.values()) {
|
|
1896
|
+
if (name.search(re) !== -1) return { value, level: "pattern" };
|
|
1897
|
+
}
|
|
1898
|
+
return this.any !== void 0 ? { value: this.any, level: "any" } : void 0;
|
|
1899
|
+
}
|
|
1900
|
+
/** Every value registered, whatever its matcher. */
|
|
1901
|
+
values() {
|
|
1902
|
+
const out = [...this.exact.values(), ...[...this.patterns.values()].map((p) => p.value)];
|
|
1903
|
+
if (this.any !== void 0) out.push(this.any);
|
|
1904
|
+
return out;
|
|
1905
|
+
}
|
|
1906
|
+
clear() {
|
|
1907
|
+
this.exact.clear();
|
|
1908
|
+
this.patterns.clear();
|
|
1909
|
+
this.any = void 0;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1851
1912
|
const APARTE_DEFAULT_BUBBLE_ACTIONS = {
|
|
1852
1913
|
copy: true,
|
|
1853
1914
|
retry: false,
|
|
@@ -1867,6 +1928,7 @@ function renderingVersionOf(cfg) {
|
|
|
1867
1928
|
function bumpRenderingVersion(cfg) {
|
|
1868
1929
|
renderingVersions.set(cfg, renderingVersionOf(cfg) + 1);
|
|
1869
1930
|
}
|
|
1931
|
+
const ELEMENT_SHAPING = ["hidden", "open", "details", "quiet"];
|
|
1870
1932
|
function drawingOf(provider, name) {
|
|
1871
1933
|
if (!provider || !(name in provider) || name in Object.prototype) return void 0;
|
|
1872
1934
|
const fn = provider[name];
|
|
@@ -1975,9 +2037,9 @@ class AparteConfig {
|
|
|
1975
2037
|
_elicitationQueue = /* @__PURE__ */ new Map();
|
|
1976
2038
|
// Tool Registry
|
|
1977
2039
|
_tools = /* @__PURE__ */ new Map();
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2040
|
+
// By exact name, pattern or '*' — see `tool-table.ts` for the one resolution rule.
|
|
2041
|
+
_toolRenderers = new ToolTable();
|
|
2042
|
+
_toolDisplays = new ToolTable();
|
|
1981
2043
|
/** Tagged blocks the stream parser recognises in the prose — see {@link registerStreamBlock}. */
|
|
1982
2044
|
_streamBlocks = /* @__PURE__ */ new Map();
|
|
1983
2045
|
_segmentDefaults = /* @__PURE__ */ new Map();
|
|
@@ -2810,16 +2872,20 @@ class AparteConfig {
|
|
|
2810
2872
|
* instead — see [Custom tool
|
|
2811
2873
|
* renderer](https://apartejs.dev/guides/tools/#custom-tool-renderer).
|
|
2812
2874
|
*
|
|
2813
|
-
*
|
|
2814
|
-
*
|
|
2815
|
-
*
|
|
2875
|
+
* The matcher is an exact name, a RegExp, or `'*'` — the same rule as
|
|
2876
|
+
* {@link registerToolDisplay}: the exact name wins, then the first pattern registered,
|
|
2877
|
+
* then `'*'`, which is one renderer for every tool without its own.
|
|
2816
2878
|
*
|
|
2817
2879
|
* @example
|
|
2818
2880
|
* // Custom pill for a web-search tool
|
|
2819
2881
|
* aparteGlobalConfig.registerToolRenderer('web_search', { render: (seg) => `<div class="aparte-tool-label">Searching...</div>` });
|
|
2882
|
+
*
|
|
2883
|
+
* @example
|
|
2884
|
+
* // One look for every tool an MCP server brings
|
|
2885
|
+
* aparteGlobalConfig.registerToolRenderer(/^mcp__/, { render: (seg) => myMcpCard(seg) });
|
|
2820
2886
|
*/
|
|
2821
|
-
registerToolRenderer(
|
|
2822
|
-
this._toolRenderers.set(
|
|
2887
|
+
registerToolRenderer(match, renderer) {
|
|
2888
|
+
this._toolRenderers.set(match, renderer);
|
|
2823
2889
|
}
|
|
2824
2890
|
/**
|
|
2825
2891
|
* Field defaults for a segment TYPE, filled in when a segment enters a message.
|
|
@@ -2859,13 +2925,93 @@ class AparteConfig {
|
|
|
2859
2925
|
clearSegmentDefaults(type) {
|
|
2860
2926
|
this._segmentDefaults.delete(type);
|
|
2861
2927
|
}
|
|
2862
|
-
/** Unregister
|
|
2863
|
-
unregisterToolRenderer(
|
|
2864
|
-
this._toolRenderers.delete(
|
|
2928
|
+
/** Unregister the renderer registered under this matcher. */
|
|
2929
|
+
unregisterToolRenderer(match) {
|
|
2930
|
+
this._toolRenderers.delete(match);
|
|
2865
2931
|
}
|
|
2866
|
-
/**
|
|
2932
|
+
/** The renderer that draws a tool named `toolName` — exact, pattern or `'*'` — or undefined. */
|
|
2867
2933
|
getToolRenderer(toolName) {
|
|
2868
|
-
return this._toolRenderers.
|
|
2934
|
+
return this._toolRenderers.resolve(toolName)?.value;
|
|
2935
|
+
}
|
|
2936
|
+
/**
|
|
2937
|
+
* Say how a tool's calls are shown: a label in place of its identifier, an icon, what
|
|
2938
|
+
* the call targets, how its arguments read, whether its row is drawn at all, and
|
|
2939
|
+
* whether it opens.
|
|
2940
|
+
*
|
|
2941
|
+
* The matcher is an exact name, a RegExp, or `'*'` — "every tool nothing more
|
|
2942
|
+
* specific describes". One entry per matcher, and calls MERGE into it; the entry that
|
|
2943
|
+
* applies to a tool is the exact name's, else the first pattern registered that
|
|
2944
|
+
* matches, else `'*'`'s — whole, never a mix of two. Registering tells the chats on
|
|
2945
|
+
* the page, so a label reaches rows already drawn.
|
|
2946
|
+
*
|
|
2947
|
+
* Core never makes a label up: without one the row shows the identifier, which is the
|
|
2948
|
+
* app's wire format.
|
|
2949
|
+
*
|
|
2950
|
+
* @example
|
|
2951
|
+
* aparteGlobalConfig.registerToolDisplay('web_search', {
|
|
2952
|
+
* label: 'Web search',
|
|
2953
|
+
* icon: 'search',
|
|
2954
|
+
* summary: (segment) => String(segment.toolCall.input['query'] ?? ''),
|
|
2955
|
+
* });
|
|
2956
|
+
*
|
|
2957
|
+
* @example
|
|
2958
|
+
* // A consumer chat: the reply is what matters; the calls stay in the history.
|
|
2959
|
+
* aparteGlobalConfig.registerToolDisplay('*', { hidden: true });
|
|
2960
|
+
*
|
|
2961
|
+
* @example
|
|
2962
|
+
* // The line, and no raw JSON behind it: nothing, or a detail of the app's own.
|
|
2963
|
+
* aparteGlobalConfig.registerToolDisplay('read_file', { label: 'Reading a file', details: false });
|
|
2964
|
+
* aparteGlobalConfig.registerToolDisplay('web_search', {
|
|
2965
|
+
* label: 'Searched the web',
|
|
2966
|
+
* details: (segment) => sourcesList(segment.result),
|
|
2967
|
+
* });
|
|
2968
|
+
*/
|
|
2969
|
+
registerToolDisplay(match, display) {
|
|
2970
|
+
const before = this._toolDisplays.get(match);
|
|
2971
|
+
const next = { ...before, ...display };
|
|
2972
|
+
this._toolDisplays.set(match, next);
|
|
2973
|
+
const shapes = ELEMENT_SHAPING.some((field) => before?.[field] !== next[field]) || before === void 0 && this._anotherShapes(next);
|
|
2974
|
+
if (shapes) bumpRenderingVersion(this);
|
|
2975
|
+
this._notify();
|
|
2976
|
+
}
|
|
2977
|
+
/** Forget the display registered under this matcher. */
|
|
2978
|
+
unregisterToolDisplay(match) {
|
|
2979
|
+
const before = this._toolDisplays.get(match);
|
|
2980
|
+
this._toolDisplays.delete(match);
|
|
2981
|
+
const shapes = ELEMENT_SHAPING.some((field) => before?.[field] !== void 0) || before !== void 0 && this._anotherShapes(before);
|
|
2982
|
+
if (shapes) bumpRenderingVersion(this);
|
|
2983
|
+
this._notify();
|
|
2984
|
+
}
|
|
2985
|
+
/** Whether an entry other than `own` sets a field that shapes a row. */
|
|
2986
|
+
_anotherShapes(own) {
|
|
2987
|
+
return this._toolDisplays.values().some((entry) => entry !== own && ELEMENT_SHAPING.some((field) => entry[field] !== void 0));
|
|
2988
|
+
}
|
|
2989
|
+
/** How a tool named `toolName` is shown — the entry that applies to it — or undefined. */
|
|
2990
|
+
getToolDisplay(toolName) {
|
|
2991
|
+
return this._toolDisplays.resolve(toolName)?.value;
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Whether a tool's calls draw nothing: its display says `hidden`, and no renderer is
|
|
2995
|
+
* registered for it MORE specifically. A `'*'` hide therefore leaves a tool with its
|
|
2996
|
+
* own renderer drawn (a plugin's receipt), while a hide as specific as the renderer
|
|
2997
|
+
* wins — the app said so for that tool. A hidden call stays in the message, in storage
|
|
2998
|
+
* and in the model's history; only its row is empty.
|
|
2999
|
+
*/
|
|
3000
|
+
isToolHidden(toolName) {
|
|
3001
|
+
const display = this._toolDisplays.resolve(toolName);
|
|
3002
|
+
if (!display?.value.hidden) return false;
|
|
3003
|
+
const renderer = this._toolRenderers.resolve(toolName);
|
|
3004
|
+
const rank = { exact: 2, pattern: 1, any: 0 };
|
|
3005
|
+
return !renderer || rank[renderer.level] <= rank[display.level];
|
|
3006
|
+
}
|
|
3007
|
+
/** Drop one field of the entry under `match`, and the entry when nothing is left. */
|
|
3008
|
+
_dropToolDisplayField(match, field) {
|
|
3009
|
+
const entry = this._toolDisplays.get(match);
|
|
3010
|
+
if (!entry || !(field in entry)) return;
|
|
3011
|
+
const { [field]: _dropped, ...rest } = entry;
|
|
3012
|
+
if (Object.keys(rest).length) this._toolDisplays.set(match, rest);
|
|
3013
|
+
else this._toolDisplays.delete(match);
|
|
3014
|
+
this._notify();
|
|
2869
3015
|
}
|
|
2870
3016
|
/**
|
|
2871
3017
|
* Say what a call targets, in the default row, after the tool's name.
|
|
@@ -2882,42 +3028,46 @@ class AparteConfig {
|
|
|
2882
3028
|
* on every config change — so registering one after a row is on screen shows up on
|
|
2883
3029
|
* that row's next update.
|
|
2884
3030
|
*
|
|
3031
|
+
* The `summary` field of {@link registerToolDisplay}, on its own: same matcher, same
|
|
3032
|
+
* entry.
|
|
3033
|
+
*
|
|
2885
3034
|
* @example
|
|
2886
3035
|
* // read_file skills.json
|
|
2887
3036
|
* aparteGlobalConfig.registerToolSummary('read_file', (segment) =>
|
|
2888
3037
|
* String(segment.toolCall.input['path'] ?? ''));
|
|
2889
3038
|
*/
|
|
2890
|
-
registerToolSummary(
|
|
2891
|
-
this.
|
|
3039
|
+
registerToolSummary(match, summary) {
|
|
3040
|
+
this.registerToolDisplay(match, { summary });
|
|
2892
3041
|
}
|
|
2893
|
-
/**
|
|
2894
|
-
unregisterToolSummary(
|
|
2895
|
-
this.
|
|
3042
|
+
/** Drop the summary registered under this matcher; the rest of its display stays. */
|
|
3043
|
+
unregisterToolSummary(match) {
|
|
3044
|
+
this._dropToolDisplayField(match, "summary");
|
|
2896
3045
|
}
|
|
2897
|
-
/**
|
|
3046
|
+
/** The summary that applies to a tool named `toolName`, or undefined. */
|
|
2898
3047
|
getToolSummary(toolName) {
|
|
2899
|
-
return this.
|
|
3048
|
+
return this.getToolDisplay(toolName)?.summary;
|
|
2900
3049
|
}
|
|
2901
3050
|
/**
|
|
2902
3051
|
* Show a tool's arguments as a person reads them, in the approval panel and in the
|
|
2903
3052
|
* default tool row's disclosure — one formatter for both. Without one, both show the
|
|
2904
|
-
* JSON the model sent.
|
|
3053
|
+
* JSON the model sent. The `formatInput` field of {@link registerToolDisplay}, on its
|
|
3054
|
+
* own: same matcher, same entry.
|
|
2905
3055
|
*
|
|
2906
3056
|
* @example
|
|
2907
3057
|
* // Approve "write_file notes.md (2 KB)" rather than a JSON object.
|
|
2908
3058
|
* aparteGlobalConfig.registerToolInputFormatter('write_file', (input) =>
|
|
2909
3059
|
* `${String(input['path'] ?? '')} (${Math.ceil(String(input['content'] ?? '').length / 1024)} KB)`);
|
|
2910
3060
|
*/
|
|
2911
|
-
registerToolInputFormatter(
|
|
2912
|
-
this.
|
|
3061
|
+
registerToolInputFormatter(match, formatter) {
|
|
3062
|
+
this.registerToolDisplay(match, { formatInput: formatter });
|
|
2913
3063
|
}
|
|
2914
|
-
/**
|
|
2915
|
-
unregisterToolInputFormatter(
|
|
2916
|
-
this.
|
|
3064
|
+
/** Drop the input formatter registered under this matcher; the rest of its display stays. */
|
|
3065
|
+
unregisterToolInputFormatter(match) {
|
|
3066
|
+
this._dropToolDisplayField(match, "formatInput");
|
|
2917
3067
|
}
|
|
2918
|
-
/**
|
|
3068
|
+
/** The input formatter that applies to a tool named `toolName`, or undefined. */
|
|
2919
3069
|
getToolInputFormatter(toolName) {
|
|
2920
|
-
return this.
|
|
3070
|
+
return this.getToolDisplay(toolName)?.formatInput;
|
|
2921
3071
|
}
|
|
2922
3072
|
/**
|
|
2923
3073
|
* Teach the stream parser a tagged block: `<tag attr="…">…</tag>` in the model's
|
|
@@ -3128,8 +3278,7 @@ class AparteConfig {
|
|
|
3128
3278
|
this._fetchedModels.clear();
|
|
3129
3279
|
this._tools.clear();
|
|
3130
3280
|
this._toolRenderers.clear();
|
|
3131
|
-
this.
|
|
3132
|
-
this._toolInputFormatters.clear();
|
|
3281
|
+
this._toolDisplays.clear();
|
|
3133
3282
|
this._streamBlocks.clear();
|
|
3134
3283
|
this._modelConfig = {};
|
|
3135
3284
|
this._requireModelSelection = false;
|
|
@@ -3262,9 +3411,36 @@ const textRenderer = {
|
|
|
3262
3411
|
},
|
|
3263
3412
|
getStyles: () => ``
|
|
3264
3413
|
};
|
|
3414
|
+
function formatDuration(ms, tag) {
|
|
3415
|
+
const total = Math.max(1, Math.round(ms / 1e3));
|
|
3416
|
+
try {
|
|
3417
|
+
const unit = (n, u) => new Intl.NumberFormat(tag, { style: "unit", unit: u, unitDisplay: "narrow" }).format(n);
|
|
3418
|
+
if (total < 60) return unit(total, "second");
|
|
3419
|
+
const minutes = Math.floor(total / 60);
|
|
3420
|
+
const seconds = total % 60;
|
|
3421
|
+
if (!seconds) return unit(minutes, "minute");
|
|
3422
|
+
return new Intl.ListFormat(tag, { style: "narrow", type: "unit" }).format([unit(minutes, "minute"), unit(seconds, "second")]);
|
|
3423
|
+
} catch {
|
|
3424
|
+
return `${total}s`;
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3265
3427
|
const THINKING_ANCHOR_SLACK = 24;
|
|
3266
3428
|
function labelOf(segment) {
|
|
3267
|
-
|
|
3429
|
+
if (segment.label && segment.label !== "Thinking") return segment.label;
|
|
3430
|
+
const cfg = contextConfig();
|
|
3431
|
+
if (!isSegmentSettled(segment)) return cfg.t("thinking");
|
|
3432
|
+
const show = segment.showDuration ?? cfg.getSegmentDefaults("thinking")?.["showDuration"] === true;
|
|
3433
|
+
const ms = show ? segmentDuration(segment) : void 0;
|
|
3434
|
+
if (ms === void 0) return cfg.t("thought");
|
|
3435
|
+
return cfg.t("thoughtFor").replace("{duration}", () => formatDuration(ms, cfg.getLocale().tag || void 0));
|
|
3436
|
+
}
|
|
3437
|
+
const arriving = (segment) => segment.isStreaming === true;
|
|
3438
|
+
function setLabel(el2, segment) {
|
|
3439
|
+
const text = labelOf(segment);
|
|
3440
|
+
const label = el2.querySelector(".aparte-thinking-label");
|
|
3441
|
+
if (label && label.textContent !== text) label.textContent = text;
|
|
3442
|
+
label?.classList.toggle("aparte-shimmer", arriving(segment));
|
|
3443
|
+
el2.querySelector(".aparte-thinking-content")?.setAttribute("aria-label", text);
|
|
3268
3444
|
}
|
|
3269
3445
|
const thinkingRenderer = {
|
|
3270
3446
|
type: "thinking",
|
|
@@ -3296,18 +3472,14 @@ const thinkingRenderer = {
|
|
|
3296
3472
|
* only part that is genuinely about reasoning rather than about disclosure.
|
|
3297
3473
|
* `.aparte-thinking-label` stays too: `relabel` below queries it.
|
|
3298
3474
|
*/
|
|
3299
|
-
render: (segment) => `<details class="aparte-segment aparte-segment-thinking aparte-accordion__item" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed === false ? "open" : ""}><summary class="aparte-accordion__header aparte-thinking-header"><span class="aparte-thinking-label">${escapeHtml(labelOf(segment))}</span><span class="aparte-accordion__icon">${contextConfig().getIcon("expand")}</span></summary><div class="aparte-accordion__panel aparte-thinking-content" role="region" tabindex="0" aria-label="${escapeAttr(labelOf(segment))}">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
|
|
3475
|
+
render: (segment) => `<details class="aparte-segment aparte-segment-thinking aparte-accordion__item" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed === false ? "open" : ""}><summary class="aparte-accordion__header aparte-thinking-header"><span class="aparte-thinking-label${escapeAttr(arriving(segment) ? " aparte-shimmer" : "")}">${escapeHtml(labelOf(segment))}</span><span class="aparte-accordion__icon">${contextConfig().getIcon("expand")}</span></summary><div class="aparte-accordion__panel aparte-thinking-content" role="region" tabindex="0" aria-label="${escapeAttr(labelOf(segment))}">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
|
|
3300
3476
|
/**
|
|
3301
3477
|
* The default label is the only config-derived text here — and `segment.label`
|
|
3302
3478
|
* still wins, exactly as in `render`, because that string is the app's.
|
|
3303
3479
|
*/
|
|
3304
|
-
relabel: (el2, segment) =>
|
|
3305
|
-
const text = labelOf(segment);
|
|
3306
|
-
const label = el2.querySelector(".aparte-thinking-label");
|
|
3307
|
-
if (label) label.textContent = text;
|
|
3308
|
-
el2.querySelector(".aparte-thinking-content")?.setAttribute("aria-label", text);
|
|
3309
|
-
},
|
|
3480
|
+
relabel: (el2, segment) => setLabel(el2, segment),
|
|
3310
3481
|
update: (el2, segment) => {
|
|
3482
|
+
setLabel(el2, segment);
|
|
3311
3483
|
const contentEl = el2.querySelector(".aparte-thinking-content");
|
|
3312
3484
|
if (!contentEl) return;
|
|
3313
3485
|
const wasAtBottom = contentEl.scrollHeight - contentEl.scrollTop - contentEl.clientHeight <= THINKING_ANCHOR_SLACK;
|
|
@@ -3352,6 +3524,35 @@ function copyText(text) {
|
|
|
3352
3524
|
else reject(new Error("[aparte] Clipboard write failed"));
|
|
3353
3525
|
});
|
|
3354
3526
|
}
|
|
3527
|
+
const CONFIRM_MS = 1500;
|
|
3528
|
+
function nameButton(button, label) {
|
|
3529
|
+
button.setAttribute("aria-label", label);
|
|
3530
|
+
button.setAttribute("title", label);
|
|
3531
|
+
}
|
|
3532
|
+
function copyButtonMarkup(className) {
|
|
3533
|
+
const label = escapeAttr(contextConfig().t("copy"));
|
|
3534
|
+
const icon = contextConfig().getIcon("copy");
|
|
3535
|
+
return `<button type="button" class="aparte-btn aparte-btn--icon aparte-btn--sm ${escapeAttr(className)}" data-action="copy" aria-label="${label}" title="${label}">${icon}</button>`;
|
|
3536
|
+
}
|
|
3537
|
+
function wireCopyButton(button, read) {
|
|
3538
|
+
button.addEventListener("click", () => {
|
|
3539
|
+
void copyText(read()).catch(() => {
|
|
3540
|
+
});
|
|
3541
|
+
button.innerHTML = contextConfig(button).getIcon("check");
|
|
3542
|
+
nameButton(button, contextConfig(button).t("copied"));
|
|
3543
|
+
button.dataset["copied"] = "1";
|
|
3544
|
+
setTimeout(() => {
|
|
3545
|
+
button.innerHTML = contextConfig(button).getIcon("copy");
|
|
3546
|
+
nameButton(button, contextConfig(button).t("copy"));
|
|
3547
|
+
delete button.dataset["copied"];
|
|
3548
|
+
}, CONFIRM_MS);
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
3551
|
+
function relabelCopyButton(button) {
|
|
3552
|
+
if (button.dataset["copied"]) return;
|
|
3553
|
+
nameButton(button, contextConfig().t("copy"));
|
|
3554
|
+
button.innerHTML = contextConfig().getIcon("copy");
|
|
3555
|
+
}
|
|
3355
3556
|
const HIGHLIGHT_DEBOUNCE_MS = 400;
|
|
3356
3557
|
const MAX_THROTTLE_ENTRIES = 256;
|
|
3357
3558
|
const _lastHighlightAt = /* @__PURE__ */ new Map();
|
|
@@ -3399,32 +3600,27 @@ function streamHighlight(element, paneSelector, content, lang, segId) {
|
|
|
3399
3600
|
}).catch(() => {
|
|
3400
3601
|
});
|
|
3401
3602
|
}
|
|
3402
|
-
function nameButton(button, label) {
|
|
3403
|
-
button.setAttribute("aria-label", label);
|
|
3404
|
-
button.setAttribute("title", label);
|
|
3405
|
-
}
|
|
3406
3603
|
const codeRenderer = {
|
|
3407
3604
|
type: "code",
|
|
3408
|
-
render: (segment) =>
|
|
3605
|
+
render: (segment) => {
|
|
3606
|
+
const copy = copyButtonMarkup("aparte-code-copy");
|
|
3607
|
+
return `
|
|
3409
3608
|
<div class="aparte-segment aparte-segment-code" data-segment-id="${escapeHtml(segment.id)}">
|
|
3410
3609
|
<div class="aparte-code-header">
|
|
3411
3610
|
${segment.filename ? `<span class="aparte-code-filename">${escapeHtml(segment.filename)}</span>` : `<span class="aparte-code-header-filler"></span>`}
|
|
3412
3611
|
<span class="aparte-code-language">${escapeHtml(segment.language || "")}</span>
|
|
3413
|
-
|
|
3414
|
-
${contextConfig().getIcon("copy")}
|
|
3415
|
-
</button>
|
|
3612
|
+
${copy}
|
|
3416
3613
|
</div>
|
|
3417
3614
|
<div class="aparte-code-content-wrapper">
|
|
3418
3615
|
<pre><code class="language-${escapeHtml(segment.language || "text")}">${escapeHtml(segment.content)}</code></pre>
|
|
3419
3616
|
</div>
|
|
3420
3617
|
</div>
|
|
3421
|
-
|
|
3618
|
+
`;
|
|
3619
|
+
},
|
|
3422
3620
|
/** The copy button's icon and tooltip — nothing else here comes from config. */
|
|
3423
3621
|
relabel: (element) => {
|
|
3424
3622
|
const copyBtn = element.querySelector(".aparte-code-copy");
|
|
3425
|
-
if (
|
|
3426
|
-
nameButton(copyBtn, contextConfig().t("copy"));
|
|
3427
|
-
copyBtn.innerHTML = contextConfig().getIcon("copy");
|
|
3623
|
+
if (copyBtn) relabelCopyButton(copyBtn);
|
|
3428
3624
|
},
|
|
3429
3625
|
setup: (element, segment) => {
|
|
3430
3626
|
const wrapper = element.querySelector(".aparte-code-content-wrapper");
|
|
@@ -3435,21 +3631,7 @@ const codeRenderer = {
|
|
|
3435
3631
|
});
|
|
3436
3632
|
}
|
|
3437
3633
|
const copyBtn = element.querySelector(".aparte-code-copy");
|
|
3438
|
-
if (copyBtn)
|
|
3439
|
-
copyBtn.addEventListener("click", () => {
|
|
3440
|
-
const source = element.querySelector(".aparte-code-content-wrapper")?.textContent ?? "";
|
|
3441
|
-
void copyText(source).catch(() => {
|
|
3442
|
-
});
|
|
3443
|
-
copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
|
|
3444
|
-
nameButton(copyBtn, contextConfig(copyBtn).t("copied"));
|
|
3445
|
-
copyBtn.dataset.copied = "1";
|
|
3446
|
-
setTimeout(() => {
|
|
3447
|
-
copyBtn.innerHTML = contextConfig(copyBtn).getIcon("copy");
|
|
3448
|
-
nameButton(copyBtn, contextConfig(copyBtn).t("copy"));
|
|
3449
|
-
delete copyBtn.dataset.copied;
|
|
3450
|
-
}, 1500);
|
|
3451
|
-
});
|
|
3452
|
-
}
|
|
3634
|
+
if (copyBtn) wireCopyButton(copyBtn, () => element.querySelector(".aparte-code-content-wrapper")?.textContent ?? "");
|
|
3453
3635
|
},
|
|
3454
3636
|
update: (element, segment) => {
|
|
3455
3637
|
if (segment.isStreaming) {
|
|
@@ -3471,7 +3653,11 @@ const errorRenderer = {
|
|
|
3471
3653
|
render: (segment) => {
|
|
3472
3654
|
const custom = contextConfig().getErrorRenderer?.();
|
|
3473
3655
|
if (custom) {
|
|
3474
|
-
const out = custom({
|
|
3656
|
+
const out = custom({
|
|
3657
|
+
message: segment.content,
|
|
3658
|
+
...segment.details !== void 0 ? { details: segment.details } : {},
|
|
3659
|
+
...segment.code !== void 0 ? { code: segment.code } : {}
|
|
3660
|
+
});
|
|
3475
3661
|
if (out instanceof HTMLElement) {
|
|
3476
3662
|
out.setAttribute("data-segment-id", segment.id);
|
|
3477
3663
|
return out;
|
|
@@ -3479,7 +3665,7 @@ const errorRenderer = {
|
|
|
3479
3665
|
return out;
|
|
3480
3666
|
}
|
|
3481
3667
|
return `
|
|
3482
|
-
<div class="aparte-segment aparte-alert aparte-alert--danger aparte-segment-error" data-segment-id="${escapeHtml(segment.id)}">
|
|
3668
|
+
<div class="aparte-segment aparte-alert aparte-alert--danger aparte-segment-error" data-segment-id="${escapeHtml(segment.id)}"${segment.code ? ` data-error-code="${escapeAttr(segment.code)}"` : ""}>
|
|
3483
3669
|
<span class="aparte-alert__icon">${contextConfig().getIcon("error") || "⚠"}</span>
|
|
3484
3670
|
<div class="aparte-alert__body">
|
|
3485
3671
|
<div class="aparte-alert__title">${escapeHtml(contextConfig().t("error"))}</div>
|
|
@@ -3518,22 +3704,150 @@ function describeToolInput(input, format) {
|
|
|
3518
3704
|
return "";
|
|
3519
3705
|
}
|
|
3520
3706
|
}
|
|
3707
|
+
function toolNameOf(segment, config) {
|
|
3708
|
+
const identifier = segment.toolCall?.name ?? "tool";
|
|
3709
|
+
const label = config.getToolDisplay(identifier)?.label;
|
|
3710
|
+
if (label !== void 0) {
|
|
3711
|
+
try {
|
|
3712
|
+
const text = typeof label === "function" ? label(segment) : label;
|
|
3713
|
+
if (typeof text === "string" && text.trim()) return { text: text.trim(), isLabel: true };
|
|
3714
|
+
} catch {
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
return { text: identifier, isLabel: false };
|
|
3718
|
+
}
|
|
3719
|
+
function toolTargetOf(segment, config) {
|
|
3720
|
+
const summary = config.getToolSummary(segment.toolCall?.name ?? "");
|
|
3721
|
+
if (!summary) return "";
|
|
3722
|
+
try {
|
|
3723
|
+
const text = summary(segment);
|
|
3724
|
+
return typeof text === "string" ? text.trim() : "";
|
|
3725
|
+
} catch {
|
|
3726
|
+
return "";
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3521
3729
|
function describeInput(segment) {
|
|
3522
3730
|
return describeToolInput(segment.toolCall?.input, contextConfig().getToolInputFormatter(segment.toolCall?.name ?? ""));
|
|
3523
3731
|
}
|
|
3524
3732
|
function stateBadge(segment) {
|
|
3525
3733
|
const cfg = contextConfig();
|
|
3526
3734
|
const status = segment.status ?? "pending";
|
|
3527
|
-
const word = (
|
|
3528
|
-
if (status === "awaiting-approval") return `${cfg.getIcon("pause")}${word("approvalWaiting")}`;
|
|
3529
|
-
if (status === "pending") return word("toolRunning");
|
|
3530
|
-
if (status === "resolved") return `${cfg.getIcon("check")}${word("toolCompleted")}`;
|
|
3531
|
-
if (status === "rejected") return `${cfg.getIcon("close")}${word("toolRejected")}`;
|
|
3532
|
-
if (status === "aborted") return `${cfg.getIcon("stop")}${word("toolStopped")}`;
|
|
3533
|
-
if (status === "failed") return `${cfg.getIcon("close")}${word("toolFailed")}`;
|
|
3735
|
+
const word = (text) => escapeHtml(text);
|
|
3736
|
+
if (status === "awaiting-approval") return `${cfg.getIcon("pause")}${word(cfg.t("approvalWaiting"))}`;
|
|
3737
|
+
if (status === "pending") return word(cfg.t("toolRunning"));
|
|
3738
|
+
if (status === "resolved") return `${cfg.getIcon("check")}${word(cfg.t("toolCompleted"))}`;
|
|
3739
|
+
if (status === "rejected") return `${cfg.getIcon("close")}${word(cfg.t("toolRejected"))}`;
|
|
3740
|
+
if (status === "aborted") return `${cfg.getIcon("stop")}${word(cfg.t("toolStopped"))}`;
|
|
3741
|
+
if (status === "failed") return `${cfg.getIcon("close")}${word(cfg.t("toolFailed"))}`;
|
|
3534
3742
|
return "";
|
|
3535
3743
|
}
|
|
3536
|
-
function
|
|
3744
|
+
function noteOf(segment) {
|
|
3745
|
+
const cfg = contextConfig();
|
|
3746
|
+
if (segment.status === "rejected") {
|
|
3747
|
+
const by = segment.refusal?.by;
|
|
3748
|
+
if (by === "policy") return cfg.t("toolRefusedByPolicy");
|
|
3749
|
+
if (by === "earlier-refusal") return cfg.t("toolNotRunAfterRefusal");
|
|
3750
|
+
const said = segment.refusal?.instruction?.trim();
|
|
3751
|
+
return said ? cfg.t("toolRejectedSaid").replace("{instruction}", () => said) : "";
|
|
3752
|
+
}
|
|
3753
|
+
if (segment.status === "aborted") {
|
|
3754
|
+
switch (segment.abortCause) {
|
|
3755
|
+
case "timeout":
|
|
3756
|
+
return cfg.t("toolTimedOut");
|
|
3757
|
+
case "no-handler":
|
|
3758
|
+
return cfg.t("toolUnavailable");
|
|
3759
|
+
case "no-resolver":
|
|
3760
|
+
return cfg.t("toolNoApprover");
|
|
3761
|
+
case "turn-limit":
|
|
3762
|
+
return cfg.t("toolTurnLimit");
|
|
3763
|
+
default:
|
|
3764
|
+
return "";
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
return "";
|
|
3768
|
+
}
|
|
3769
|
+
function outcomeOf(segment) {
|
|
3770
|
+
const text = displayText(segment.result ?? "");
|
|
3771
|
+
if (!text) return null;
|
|
3772
|
+
if (segment.status === "rejected" && segment.refusal) return null;
|
|
3773
|
+
return { key: "toolOutput", text };
|
|
3774
|
+
}
|
|
3775
|
+
function behindOf(segment) {
|
|
3776
|
+
const details = contextConfig().getToolDisplay(segment.toolCall?.name ?? "")?.details;
|
|
3777
|
+
if (details === false) return { kind: "none" };
|
|
3778
|
+
if (typeof details !== "function") return { kind: "parts" };
|
|
3779
|
+
try {
|
|
3780
|
+
const out = details(segment);
|
|
3781
|
+
if (!out) return { kind: "none" };
|
|
3782
|
+
return { kind: "body", body: typeof out === "string" ? contextConfig().sanitizeHtml(out) : out };
|
|
3783
|
+
} catch {
|
|
3784
|
+
return { kind: "none" };
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
const appDetailOf = (element) => element.querySelector(':scope > .aparte-tool-detail[data-details="app"]');
|
|
3788
|
+
function setBody(element, body) {
|
|
3789
|
+
const detail = element.querySelector(".aparte-tool-detail");
|
|
3790
|
+
if (!detail) return;
|
|
3791
|
+
if (typeof body === "string") detail.innerHTML = body;
|
|
3792
|
+
else detail.replaceChildren(body);
|
|
3793
|
+
drawnBodies.set(detail, serialized(body));
|
|
3794
|
+
}
|
|
3795
|
+
const drawnBodies = /* @__PURE__ */ new WeakMap();
|
|
3796
|
+
const serialized = (body) => typeof body === "string" ? body : body.outerHTML;
|
|
3797
|
+
function relabelBody(detail, body) {
|
|
3798
|
+
if (typeof body !== "string" && detail.contains(body)) {
|
|
3799
|
+
drawnBodies.set(detail, body.outerHTML);
|
|
3800
|
+
return;
|
|
3801
|
+
}
|
|
3802
|
+
const before = drawnBodies.get(detail);
|
|
3803
|
+
const now = serialized(body);
|
|
3804
|
+
if (before === now) return;
|
|
3805
|
+
const next = document.createElement("div");
|
|
3806
|
+
if (typeof body === "string") next.innerHTML = body;
|
|
3807
|
+
else next.append(body);
|
|
3808
|
+
const prev = document.createElement("div");
|
|
3809
|
+
if (before !== void 0) prev.innerHTML = before;
|
|
3810
|
+
if (before !== void 0 && sameShape(prev, next) && sameShape(prev, detail)) patchChanges(detail, prev, next);
|
|
3811
|
+
else detail.replaceChildren(...Array.from(next.childNodes));
|
|
3812
|
+
drawnBodies.set(detail, now);
|
|
3813
|
+
}
|
|
3814
|
+
function sameShape(a, b) {
|
|
3815
|
+
if (a.childNodes.length !== b.childNodes.length) return false;
|
|
3816
|
+
for (let i = 0; i < a.childNodes.length; i++) {
|
|
3817
|
+
const x = a.childNodes[i];
|
|
3818
|
+
const y = b.childNodes[i];
|
|
3819
|
+
if (x.nodeType !== y.nodeType || x.nodeName !== y.nodeName || !sameShape(x, y)) return false;
|
|
3820
|
+
}
|
|
3821
|
+
return true;
|
|
3822
|
+
}
|
|
3823
|
+
function patchChanges(live, prev, next) {
|
|
3824
|
+
for (let i = 0; i < live.childNodes.length; i++) {
|
|
3825
|
+
const l = live.childNodes[i];
|
|
3826
|
+
const p = prev.childNodes[i];
|
|
3827
|
+
const n = next.childNodes[i];
|
|
3828
|
+
if (l instanceof Element && p instanceof Element && n instanceof Element) {
|
|
3829
|
+
for (const { name, value } of Array.from(n.attributes)) {
|
|
3830
|
+
if (p.getAttribute(name) !== value) l.setAttribute(name, value);
|
|
3831
|
+
}
|
|
3832
|
+
for (const { name } of Array.from(p.attributes)) {
|
|
3833
|
+
if (!n.hasAttribute(name)) l.removeAttribute(name);
|
|
3834
|
+
}
|
|
3835
|
+
patchChanges(l, p, n);
|
|
3836
|
+
} else if (p.nodeValue !== n.nodeValue) {
|
|
3837
|
+
l.nodeValue = n.nodeValue;
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
function setNote(element, segment) {
|
|
3842
|
+
const el2 = element.querySelector(".aparte-tool-note");
|
|
3843
|
+
if (!el2) return;
|
|
3844
|
+
const text = noteOf(segment);
|
|
3845
|
+
if (el2.textContent !== text) el2.textContent = text;
|
|
3846
|
+
el2.toggleAttribute("hidden", !text);
|
|
3847
|
+
if (text) el2.setAttribute("title", text);
|
|
3848
|
+
else el2.removeAttribute("title");
|
|
3849
|
+
}
|
|
3850
|
+
function setPart(element, part, text, labelKey) {
|
|
3537
3851
|
const existing = element.querySelector(`[data-part="${part}"]`);
|
|
3538
3852
|
if (!text) {
|
|
3539
3853
|
existing?.remove();
|
|
@@ -3542,6 +3856,10 @@ function setPart(element, part, text) {
|
|
|
3542
3856
|
if (existing) {
|
|
3543
3857
|
const code2 = existing.querySelector("code");
|
|
3544
3858
|
if (code2 && code2.textContent !== text) code2.textContent = text;
|
|
3859
|
+
const label2 = existing.querySelector(".aparte-tool-part-label");
|
|
3860
|
+
const word = contextConfig().t(labelKey);
|
|
3861
|
+
if (label2 && label2.textContent !== word) label2.textContent = word;
|
|
3862
|
+
existing.querySelector(".aparte-tool-part-body")?.setAttribute("aria-label", word);
|
|
3545
3863
|
return;
|
|
3546
3864
|
}
|
|
3547
3865
|
const detail = element.querySelector(".aparte-tool-detail");
|
|
@@ -3550,25 +3868,57 @@ function setPart(element, part, text) {
|
|
|
3550
3868
|
const wrap = document.createElement("div");
|
|
3551
3869
|
wrap.className = "aparte-tool-part";
|
|
3552
3870
|
wrap.dataset["part"] = part;
|
|
3871
|
+
const head = document.createElement("div");
|
|
3872
|
+
head.className = "aparte-tool-part-head";
|
|
3553
3873
|
const label = document.createElement("span");
|
|
3554
3874
|
label.className = "aparte-tool-part-label";
|
|
3555
|
-
label.textContent = cfg.t(
|
|
3875
|
+
label.textContent = cfg.t(labelKey);
|
|
3876
|
+
const tpl = document.createElement("template");
|
|
3877
|
+
tpl.innerHTML = copyButtonMarkup("aparte-tool-copy");
|
|
3878
|
+
head.append(label, tpl.content);
|
|
3556
3879
|
const body = document.createElement("div");
|
|
3557
3880
|
body.className = "aparte-tool-part-body";
|
|
3881
|
+
body.setAttribute("role", "region");
|
|
3882
|
+
body.tabIndex = 0;
|
|
3883
|
+
body.setAttribute("aria-label", label.textContent);
|
|
3558
3884
|
const pre = document.createElement("pre");
|
|
3559
3885
|
const code = document.createElement("code");
|
|
3560
3886
|
code.textContent = text;
|
|
3561
3887
|
pre.appendChild(code);
|
|
3562
3888
|
body.appendChild(pre);
|
|
3563
|
-
wrap.append(
|
|
3889
|
+
wrap.append(head, body);
|
|
3564
3890
|
if (part === "input") detail.prepend(wrap);
|
|
3565
3891
|
else detail.appendChild(wrap);
|
|
3892
|
+
wireParts(wrap);
|
|
3893
|
+
}
|
|
3894
|
+
function wireParts(root) {
|
|
3895
|
+
for (const button of root.querySelectorAll(".aparte-tool-copy")) {
|
|
3896
|
+
if (button.dataset["wired"]) continue;
|
|
3897
|
+
button.dataset["wired"] = "1";
|
|
3898
|
+
const part = button.closest(".aparte-tool-part");
|
|
3899
|
+
wireCopyButton(button, () => part?.querySelector(".aparte-tool-part-body")?.textContent ?? "");
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
const JSON_PART_LIMIT = 2e5;
|
|
3903
|
+
function jsonStructureOf(text) {
|
|
3904
|
+
const trimmed = text.trim();
|
|
3905
|
+
if (trimmed.length > JSON_PART_LIMIT || !(trimmed.startsWith("{") || trimmed.startsWith("["))) return void 0;
|
|
3906
|
+
try {
|
|
3907
|
+
const value = JSON.parse(trimmed);
|
|
3908
|
+
return value !== null && typeof value === "object" ? value : void 0;
|
|
3909
|
+
} catch {
|
|
3910
|
+
return void 0;
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
function displayText(raw) {
|
|
3914
|
+
const value = jsonStructureOf(raw);
|
|
3915
|
+
return value === void 0 ? raw : JSON.stringify(value, null, 2);
|
|
3566
3916
|
}
|
|
3567
3917
|
function highlightParts(element) {
|
|
3568
3918
|
const cfg = contextConfig();
|
|
3569
3919
|
for (const body of element.querySelectorAll(".aparte-tool-part-body")) {
|
|
3570
3920
|
const text = body.querySelector("code")?.textContent ?? "";
|
|
3571
|
-
if (!text) continue;
|
|
3921
|
+
if (!text || jsonStructureOf(text) === void 0) continue;
|
|
3572
3922
|
void cfg.highlightCode(text, "json").then((html) => {
|
|
3573
3923
|
if ((body.querySelector("code")?.textContent ?? "") !== text) return;
|
|
3574
3924
|
body.innerHTML = html;
|
|
@@ -3577,14 +3927,7 @@ function highlightParts(element) {
|
|
|
3577
3927
|
}
|
|
3578
3928
|
}
|
|
3579
3929
|
function toolTarget(segment) {
|
|
3580
|
-
|
|
3581
|
-
if (!summary) return "";
|
|
3582
|
-
try {
|
|
3583
|
-
const text = summary(segment);
|
|
3584
|
-
return typeof text === "string" ? text.trim() : "";
|
|
3585
|
-
} catch {
|
|
3586
|
-
return "";
|
|
3587
|
-
}
|
|
3930
|
+
return toolTargetOf(segment, contextConfig());
|
|
3588
3931
|
}
|
|
3589
3932
|
function setTarget(element, segment) {
|
|
3590
3933
|
const el2 = element.querySelector(".aparte-tool-target");
|
|
@@ -3593,58 +3936,117 @@ function setTarget(element, segment) {
|
|
|
3593
3936
|
if (el2.textContent !== text) el2.textContent = text;
|
|
3594
3937
|
el2.toggleAttribute("hidden", !text);
|
|
3595
3938
|
}
|
|
3596
|
-
function
|
|
3939
|
+
function nameOf(segment) {
|
|
3940
|
+
return toolNameOf(segment, contextConfig());
|
|
3941
|
+
}
|
|
3942
|
+
function iconOf(segment) {
|
|
3943
|
+
const cfg = contextConfig();
|
|
3944
|
+
const key = cfg.getToolDisplay(segment.toolCall?.name ?? "")?.icon;
|
|
3945
|
+
return (key ? cfg.getIcon(key) : void 0) || cfg.getIcon("tool");
|
|
3946
|
+
}
|
|
3947
|
+
const isQuiet = (segment) => contextConfig().getToolDisplay(segment.toolCall?.name ?? "")?.quiet === true;
|
|
3948
|
+
function quietOf(segment) {
|
|
3949
|
+
const status = segment.status ?? "pending";
|
|
3950
|
+
const quiet = isQuiet(segment);
|
|
3951
|
+
return {
|
|
3952
|
+
shimmer: quiet && status === "pending",
|
|
3953
|
+
spinner: !quiet && status === "pending",
|
|
3954
|
+
stateHidden: quiet && (status === "pending" || status === "resolved")
|
|
3955
|
+
};
|
|
3956
|
+
}
|
|
3957
|
+
function setQuiet(element, segment) {
|
|
3958
|
+
const quiet = quietOf(segment);
|
|
3959
|
+
element.toggleAttribute("data-quiet", isQuiet(segment));
|
|
3960
|
+
element.querySelector(".aparte-tool-spinner")?.toggleAttribute("hidden", !quiet.spinner);
|
|
3961
|
+
element.querySelector(".aparte-tool-name")?.classList.toggle("aparte-shimmer", quiet.shimmer);
|
|
3962
|
+
element.querySelector(".aparte-tool-state")?.classList.toggle("aparte-sr-only", quiet.stateHidden);
|
|
3963
|
+
}
|
|
3964
|
+
function setName(element, segment) {
|
|
3965
|
+
const el2 = element.querySelector(".aparte-tool-name");
|
|
3966
|
+
if (!el2) return;
|
|
3967
|
+
const { text, isLabel } = nameOf(segment);
|
|
3968
|
+
if (el2.textContent !== text) el2.textContent = text;
|
|
3969
|
+
el2.classList.toggle("aparte-tool-name--label", isLabel);
|
|
3970
|
+
}
|
|
3971
|
+
function pillMarkup(segment) {
|
|
3597
3972
|
const badge = stateBadge(segment);
|
|
3973
|
+
const icon = iconOf(segment);
|
|
3598
3974
|
const target = toolTarget(segment);
|
|
3975
|
+
const note = noteOf(segment);
|
|
3976
|
+
const name = nameOf(segment);
|
|
3977
|
+
const quiet = quietOf(segment);
|
|
3599
3978
|
return `
|
|
3600
3979
|
<span class="aparte-tool-label">
|
|
3601
|
-
<span class="aparte-tool-icon">${
|
|
3602
|
-
<span class="aparte-tool-name">${escapeHtml(name)}</span>
|
|
3980
|
+
<span class="aparte-tool-icon">${icon}</span>
|
|
3981
|
+
<span class="aparte-tool-name${name.isLabel ? " aparte-tool-name--label" : ""}${quiet.shimmer ? " aparte-shimmer" : ""}">${escapeHtml(name.text)}</span>
|
|
3603
3982
|
<span class="aparte-tool-target"${target ? "" : " hidden"}>${escapeHtml(target)}</span>
|
|
3604
3983
|
</span>
|
|
3605
|
-
<span class="aparte-spinner aparte-tool-spinner" aria-hidden="true"${
|
|
3606
|
-
<span class="aparte-tool-state">${badge}</span
|
|
3984
|
+
<span class="aparte-spinner aparte-tool-spinner" aria-hidden="true"${quiet.spinner ? "" : " hidden"}></span>
|
|
3985
|
+
<span class="aparte-tool-state${quiet.stateHidden ? " aparte-sr-only" : ""}">${badge}</span>
|
|
3986
|
+
<span class="aparte-tool-note"${note ? ` title="${escapeAttr(note)}"` : " hidden"}>${escapeHtml(note)}</span>`;
|
|
3987
|
+
}
|
|
3988
|
+
function placeholderMarkup(segment) {
|
|
3989
|
+
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
|
+
}
|
|
3991
|
+
const isPlaceholder = (element) => element.hidden && element.classList.contains("aparte-segment-tool-call") && element.childElementCount === 0;
|
|
3992
|
+
function drawDefault(segment, behind) {
|
|
3993
|
+
const name = segment.toolCall?.name ?? "tool";
|
|
3994
|
+
const status = segment.status ?? "pending";
|
|
3995
|
+
const toolCallId = segment.toolCall?.id ?? "";
|
|
3996
|
+
const pill = pillMarkup(segment);
|
|
3997
|
+
const quiet = isQuiet(segment) ? " data-quiet" : "";
|
|
3998
|
+
const toggle = contextConfig().getIcon("expand");
|
|
3999
|
+
const input = behind.kind === "parts" ? describeInput(segment) : "";
|
|
4000
|
+
const outcome = behind.kind === "parts" ? outcomeOf(segment) : null;
|
|
4001
|
+
if (behind.kind === "none" || behind.kind === "parts" && !input && !outcome) {
|
|
4002
|
+
return `
|
|
4003
|
+
<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
|
+
</div>
|
|
4005
|
+
`;
|
|
4006
|
+
}
|
|
4007
|
+
const cfg = contextConfig();
|
|
4008
|
+
const copy = copyButtonMarkup("aparte-tool-copy");
|
|
4009
|
+
const open = segment.collapsed !== void 0 ? !segment.collapsed : cfg.getToolDisplay(name)?.open === true;
|
|
4010
|
+
if (behind.kind === "body") {
|
|
4011
|
+
const tpl = document.createElement("template");
|
|
4012
|
+
tpl.innerHTML = `
|
|
4013
|
+
<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" : ""}>
|
|
4014
|
+
<summary class="aparte-tool-summary"><span class="aparte-tool-toggle">${toggle}</span>${pill}</summary>
|
|
4015
|
+
<div class="aparte-tool-detail" data-details="app"></div>
|
|
4016
|
+
</details>`.trim();
|
|
4017
|
+
const row = tpl.content.firstElementChild;
|
|
4018
|
+
setBody(row, behind.body);
|
|
4019
|
+
return row;
|
|
4020
|
+
}
|
|
4021
|
+
return `
|
|
4022
|
+
<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" : ""}>
|
|
4023
|
+
<summary class="aparte-tool-summary"><span class="aparte-tool-toggle">${toggle}</span>${pill}</summary>
|
|
4024
|
+
<div class="aparte-tool-detail">
|
|
4025
|
+
${input ? `
|
|
4026
|
+
<div class="aparte-tool-part" data-part="input">
|
|
4027
|
+
<div class="aparte-tool-part-head"><span class="aparte-tool-part-label">${escapeHtml(cfg.t("toolInput"))}</span>${copy}</div>
|
|
4028
|
+
<div class="aparte-tool-part-body" role="region" tabindex="0" aria-label="${escapeAttr(cfg.t("toolInput"))}"><pre><code>${escapeHtml(input)}</code></pre></div>
|
|
4029
|
+
</div>` : ""}
|
|
4030
|
+
${outcome ? `
|
|
4031
|
+
<div class="aparte-tool-part" data-part="output">
|
|
4032
|
+
<div class="aparte-tool-part-head"><span class="aparte-tool-part-label">${escapeHtml(cfg.t(outcome.key))}</span>${copy}</div>
|
|
4033
|
+
<div class="aparte-tool-part-body" role="region" tabindex="0" aria-label="${escapeAttr(cfg.t(outcome.key))}"><pre><code>${escapeHtml(outcome.text)}</code></pre></div>
|
|
4034
|
+
</div>` : ""}
|
|
4035
|
+
</div>
|
|
4036
|
+
</details>
|
|
4037
|
+
`;
|
|
3607
4038
|
}
|
|
3608
4039
|
const toolCallRenderer = {
|
|
3609
4040
|
type: "tool_call",
|
|
3610
4041
|
render: (segment) => {
|
|
3611
4042
|
const name = segment.toolCall?.name ?? "tool";
|
|
3612
|
-
|
|
3613
|
-
const
|
|
3614
|
-
const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
|
|
4043
|
+
if (contextConfig().isToolHidden(name)) return placeholderMarkup(segment);
|
|
4044
|
+
const customRenderer = contextConfig().getToolRenderer(name);
|
|
3615
4045
|
if (customRenderer) {
|
|
3616
|
-
injectToolRendererStyles(
|
|
3617
|
-
|
|
3618
|
-
if (out) return out;
|
|
3619
|
-
}
|
|
3620
|
-
const pill = pillMarkup(segment, name, status);
|
|
3621
|
-
const toggle = contextConfig().getIcon("expand");
|
|
3622
|
-
const input = describeInput(segment);
|
|
3623
|
-
const result = segment.result;
|
|
3624
|
-
if (!input && !result) {
|
|
3625
|
-
return `
|
|
3626
|
-
<div class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}">${pill}
|
|
3627
|
-
</div>
|
|
3628
|
-
`;
|
|
4046
|
+
injectToolRendererStyles(name, customRenderer);
|
|
4047
|
+
return customRenderer.render(segment) || placeholderMarkup(segment);
|
|
3629
4048
|
}
|
|
3630
|
-
|
|
3631
|
-
return `
|
|
3632
|
-
<details class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}">
|
|
3633
|
-
<summary class="aparte-tool-summary"><span class="aparte-tool-toggle">${toggle}</span>${pill}</summary>
|
|
3634
|
-
<div class="aparte-tool-detail">
|
|
3635
|
-
${input ? `
|
|
3636
|
-
<div class="aparte-tool-part" data-part="input">
|
|
3637
|
-
<span class="aparte-tool-part-label">${escapeHtml(cfg.t("toolInput"))}</span>
|
|
3638
|
-
<div class="aparte-tool-part-body"><pre><code>${escapeHtml(input)}</code></pre></div>
|
|
3639
|
-
</div>` : ""}
|
|
3640
|
-
${result ? `
|
|
3641
|
-
<div class="aparte-tool-part" data-part="output">
|
|
3642
|
-
<span class="aparte-tool-part-label">${escapeHtml(cfg.t("toolOutput"))}</span>
|
|
3643
|
-
<div class="aparte-tool-part-body"><pre><code>${escapeHtml(result)}</code></pre></div>
|
|
3644
|
-
</div>` : ""}
|
|
3645
|
-
</div>
|
|
3646
|
-
</details>
|
|
3647
|
-
`;
|
|
4049
|
+
return drawDefault(segment, behindOf(segment));
|
|
3648
4050
|
},
|
|
3649
4051
|
/**
|
|
3650
4052
|
* The pill's glyphs, and the label it wears while a person is deciding.
|
|
@@ -3657,6 +4059,7 @@ const toolCallRenderer = {
|
|
|
3657
4059
|
* No child node is added or removed, per the relabel contract.
|
|
3658
4060
|
*/
|
|
3659
4061
|
relabel: (element, segment) => {
|
|
4062
|
+
if (isPlaceholder(element)) return;
|
|
3660
4063
|
const cfg = contextConfig();
|
|
3661
4064
|
const custom = cfg.getToolRenderer(segment.toolCall?.name);
|
|
3662
4065
|
if (custom) {
|
|
@@ -3664,10 +4067,26 @@ const toolCallRenderer = {
|
|
|
3664
4067
|
return;
|
|
3665
4068
|
}
|
|
3666
4069
|
const icon = element.querySelector(".aparte-tool-icon");
|
|
3667
|
-
if (icon) icon.innerHTML =
|
|
4070
|
+
if (icon) icon.innerHTML = iconOf(segment);
|
|
4071
|
+
setName(element, segment);
|
|
3668
4072
|
setTarget(element, segment);
|
|
4073
|
+
setNote(element, segment);
|
|
4074
|
+
const inputLabel = element.querySelector('[data-part="input"] .aparte-tool-part-label');
|
|
4075
|
+
if (inputLabel) inputLabel.textContent = cfg.t("toolInput");
|
|
4076
|
+
element.querySelector('[data-part="input"] .aparte-tool-part-body')?.setAttribute("aria-label", cfg.t("toolInput"));
|
|
4077
|
+
const outcome = outcomeOf(segment);
|
|
4078
|
+
const outputLabel = element.querySelector('[data-part="output"] .aparte-tool-part-label');
|
|
4079
|
+
if (outputLabel && outcome) outputLabel.textContent = cfg.t(outcome.key);
|
|
4080
|
+
if (outcome) element.querySelector('[data-part="output"] .aparte-tool-part-body')?.setAttribute("aria-label", cfg.t(outcome.key));
|
|
4081
|
+
for (const button of element.querySelectorAll(".aparte-tool-copy")) relabelCopyButton(button);
|
|
4082
|
+
const appDetail = appDetailOf(element);
|
|
4083
|
+
if (appDetail) {
|
|
4084
|
+
const behind = behindOf(segment);
|
|
4085
|
+
if (behind.kind === "body") relabelBody(appDetail, behind.body);
|
|
4086
|
+
}
|
|
3669
4087
|
const statusEl = element.querySelector(".aparte-tool-state");
|
|
3670
4088
|
if (statusEl) statusEl.innerHTML = stateBadge(segment);
|
|
4089
|
+
setQuiet(element, segment);
|
|
3671
4090
|
},
|
|
3672
4091
|
/**
|
|
3673
4092
|
* Patch in place. NEVER re-render, and never touch `open`.
|
|
@@ -3684,34 +4103,57 @@ const toolCallRenderer = {
|
|
|
3684
4103
|
*/
|
|
3685
4104
|
update: (element, segment) => {
|
|
3686
4105
|
const status = segment.status ?? "pending";
|
|
3687
|
-
const
|
|
3688
|
-
|
|
4106
|
+
const hidden = contextConfig().isToolHidden(segment.toolCall?.name ?? "tool");
|
|
4107
|
+
if (hidden && isPlaceholder(element)) {
|
|
4108
|
+
element.setAttribute("data-status", status);
|
|
4109
|
+
return;
|
|
4110
|
+
}
|
|
3689
4111
|
const custom = contextConfig().getToolRenderer(segment.toolCall?.name);
|
|
3690
|
-
if (custom?.update) {
|
|
4112
|
+
if (custom?.update && !hidden && !isPlaceholder(element)) {
|
|
3691
4113
|
custom.update(element, segment);
|
|
3692
4114
|
return;
|
|
3693
4115
|
}
|
|
3694
|
-
|
|
3695
|
-
const out = toolCallRenderer.render(segment);
|
|
4116
|
+
const rebuild = (out) => {
|
|
3696
4117
|
const tpl = document.createElement("template");
|
|
3697
4118
|
tpl.innerHTML = typeof out === "string" ? out.trim() : "";
|
|
3698
4119
|
const rebuilt = typeof out === "string" ? tpl.content.firstElementChild : out;
|
|
3699
4120
|
if (rebuilt) {
|
|
4121
|
+
if (!rebuilt.hasAttribute("data-segment-id")) rebuilt.setAttribute("data-segment-id", segment.id);
|
|
3700
4122
|
element.replaceWith(rebuilt);
|
|
3701
4123
|
toolCallRenderer.setup?.(rebuilt, segment);
|
|
3702
4124
|
}
|
|
4125
|
+
};
|
|
4126
|
+
if (custom || hidden || isPlaceholder(element)) {
|
|
4127
|
+
rebuild(toolCallRenderer.render(segment));
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4130
|
+
const behind = behindOf(segment);
|
|
4131
|
+
const input = behind.kind === "parts" ? describeInput(segment) : "";
|
|
4132
|
+
const outcome = behind.kind === "parts" ? outcomeOf(segment) : null;
|
|
4133
|
+
const wantsDetail = behind.kind === "body" || !!input || !!outcome;
|
|
4134
|
+
const holdsAppDetail = !!appDetailOf(element);
|
|
4135
|
+
if (wantsDetail !== (element.tagName === "DETAILS") || wantsDetail && behind.kind === "body" !== holdsAppDetail) {
|
|
4136
|
+
rebuild(drawDefault(segment, behind));
|
|
3703
4137
|
return;
|
|
3704
4138
|
}
|
|
3705
4139
|
element.setAttribute("data-status", status);
|
|
3706
4140
|
const badge = element.querySelector(".aparte-tool-state");
|
|
3707
4141
|
if (badge) badge.innerHTML = stateBadge(segment);
|
|
3708
|
-
element
|
|
4142
|
+
setQuiet(element, segment);
|
|
4143
|
+
setName(element, segment);
|
|
3709
4144
|
setTarget(element, segment);
|
|
3710
|
-
|
|
3711
|
-
|
|
4145
|
+
setNote(element, segment);
|
|
4146
|
+
if (behind.kind === "body") {
|
|
4147
|
+
setBody(element, behind.body);
|
|
4148
|
+
return;
|
|
4149
|
+
}
|
|
4150
|
+
setPart(element, "input", input, "toolInput");
|
|
4151
|
+
setPart(element, "output", outcome?.text ?? "", outcome?.key ?? "toolOutput");
|
|
3712
4152
|
highlightParts(element);
|
|
3713
4153
|
},
|
|
3714
4154
|
setup: (element, segment) => {
|
|
4155
|
+
if (isPlaceholder(element)) return;
|
|
4156
|
+
wireParts(element);
|
|
3715
4157
|
highlightParts(element);
|
|
3716
4158
|
const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
|
|
3717
4159
|
customRenderer?.setup?.(element, segment);
|
|
@@ -3784,11 +4226,16 @@ function injectRendererStyles(config = contextConfig()) {
|
|
|
3784
4226
|
}
|
|
3785
4227
|
if (added) styleElement.textContent = Array.from(injectedRendererStyles).join("\n");
|
|
3786
4228
|
}
|
|
4229
|
+
const toolRendererSheetIds = /* @__PURE__ */ new WeakMap();
|
|
3787
4230
|
function injectToolRendererStyles(toolName, renderer) {
|
|
3788
4231
|
if (typeof document === "undefined") return;
|
|
3789
4232
|
const css = renderer.getStyles?.();
|
|
3790
4233
|
if (!css) return;
|
|
3791
|
-
|
|
4234
|
+
let id = toolRendererSheetIds.get(renderer);
|
|
4235
|
+
if (!id) {
|
|
4236
|
+
id = `aparte-tool-renderer-${toolName}`;
|
|
4237
|
+
toolRendererSheetIds.set(renderer, id);
|
|
4238
|
+
}
|
|
3792
4239
|
if (document.getElementById(id)) return;
|
|
3793
4240
|
const el2 = document.createElement("style");
|
|
3794
4241
|
el2.id = id;
|
|
@@ -4309,6 +4756,21 @@ function revokeAttachmentUrls(attachments) {
|
|
|
4309
4756
|
if (attachment.url?.startsWith("blob:")) URL.revokeObjectURL(attachment.url);
|
|
4310
4757
|
}
|
|
4311
4758
|
}
|
|
4759
|
+
const known = /* @__PURE__ */ new Map();
|
|
4760
|
+
function rememberConversationStatus(id, status) {
|
|
4761
|
+
if (status) known.set(id, status);
|
|
4762
|
+
else known.delete(id);
|
|
4763
|
+
}
|
|
4764
|
+
function tellConversationStatus(id, status) {
|
|
4765
|
+
rememberConversationStatus(id, status);
|
|
4766
|
+
try {
|
|
4767
|
+
window.dispatchEvent(new CustomEvent("aparte-conversation-status", { detail: { id, status } }));
|
|
4768
|
+
} catch {
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
function conversationStatusOf(id) {
|
|
4772
|
+
return known.get(id) ?? null;
|
|
4773
|
+
}
|
|
4312
4774
|
const MAX_COMPACTION_SAVE_WAITS = 6;
|
|
4313
4775
|
class AparteConversationController {
|
|
4314
4776
|
_binding;
|
|
@@ -4317,6 +4779,10 @@ class AparteConversationController {
|
|
|
4317
4779
|
_isStreaming = false;
|
|
4318
4780
|
_isLoadingConversation = false;
|
|
4319
4781
|
_ensureInFlight = null;
|
|
4782
|
+
/** The conversation the turn in flight started in, and what the lists were told of it. */
|
|
4783
|
+
_turn = null;
|
|
4784
|
+
/** The reply the turn in flight writes into, from its `aparte-message-start`. */
|
|
4785
|
+
_turnMessage = null;
|
|
4320
4786
|
_onSendCapture = null;
|
|
4321
4787
|
_onPathChanged = null;
|
|
4322
4788
|
_onMessageStart = null;
|
|
@@ -4324,6 +4790,7 @@ class AparteConversationController {
|
|
|
4324
4790
|
_onMessageError = null;
|
|
4325
4791
|
_onMessageAborted = null;
|
|
4326
4792
|
_onMessagePaused = null;
|
|
4793
|
+
_onMessageResumed = null;
|
|
4327
4794
|
_onSelectConversation = null;
|
|
4328
4795
|
_onCompactDone = null;
|
|
4329
4796
|
_unsubscribeManager = null;
|
|
@@ -4404,6 +4871,33 @@ class AparteConversationController {
|
|
|
4404
4871
|
"[aparte] This chat resolves its own config, but the conversation manager was registered on aparteGlobalConfig — so nothing is being persisted. Pass the same config to init(): `init(adapter, myConfig)`, using the config you gave the chat."
|
|
4405
4872
|
);
|
|
4406
4873
|
}
|
|
4874
|
+
/**
|
|
4875
|
+
* Tell every conversation list on the page what the turn is doing, through
|
|
4876
|
+
* `aparte-conversation-status` on `window` — the mirror of the list's
|
|
4877
|
+
* `aparte-conversation-select`, which this controller hears. Always about the
|
|
4878
|
+
* conversation the turn started in: the person may have opened another since. A turn
|
|
4879
|
+
* that starts before its conversation exists (a first message) is told once it does.
|
|
4880
|
+
*/
|
|
4881
|
+
_setTurn(status) {
|
|
4882
|
+
if (!status) {
|
|
4883
|
+
if (this._turn) tellConversationStatus(this._turn.id, null);
|
|
4884
|
+
this._turn = null;
|
|
4885
|
+
return;
|
|
4886
|
+
}
|
|
4887
|
+
const id = this._turn?.id ?? (status === "running" ? this._activeId ?? this._manager()?.activeId ?? null : null);
|
|
4888
|
+
if (!id) return;
|
|
4889
|
+
this._turn = { id, status };
|
|
4890
|
+
tellConversationStatus(id, status);
|
|
4891
|
+
}
|
|
4892
|
+
/**
|
|
4893
|
+
* Whether a lifecycle event is about the turn in flight. A turn a newer send replaced
|
|
4894
|
+
* (a chip, a retry, an edit) says it stopped AFTER the new one started: its end must not
|
|
4895
|
+
* end the new turn's state. An event without a message id is taken as this turn's.
|
|
4896
|
+
*/
|
|
4897
|
+
_ofThisTurn(e) {
|
|
4898
|
+
const id = e.detail?.messageId;
|
|
4899
|
+
return !id || !this._turnMessage || id === this._turnMessage;
|
|
4900
|
+
}
|
|
4407
4901
|
/**
|
|
4408
4902
|
* Attach event listeners to the binding host. Returns an unbind function.
|
|
4409
4903
|
*/
|
|
@@ -4434,29 +4928,42 @@ class AparteConversationController {
|
|
|
4434
4928
|
}
|
|
4435
4929
|
};
|
|
4436
4930
|
host.addEventListener("aparte-path-changed", this._onPathChanged);
|
|
4437
|
-
this._onMessageStart = () => {
|
|
4931
|
+
this._onMessageStart = (e) => {
|
|
4438
4932
|
this._isStreaming = true;
|
|
4933
|
+
this._turnMessage = e.detail?.messageId ?? null;
|
|
4934
|
+
this._setTurn("running");
|
|
4439
4935
|
};
|
|
4440
|
-
|
|
4936
|
+
const ended = (e) => {
|
|
4937
|
+
if (!this._ofThisTurn(e)) return;
|
|
4441
4938
|
this._isStreaming = false;
|
|
4939
|
+
this._setTurn(null);
|
|
4940
|
+
this._turnMessage = null;
|
|
4941
|
+
};
|
|
4942
|
+
this._onMessageDone = (e) => {
|
|
4943
|
+
ended(e);
|
|
4442
4944
|
queueMicrotask(() => this._persistActive());
|
|
4443
4945
|
};
|
|
4444
|
-
this._onMessageError = () => {
|
|
4445
|
-
|
|
4946
|
+
this._onMessageError = (e) => {
|
|
4947
|
+
ended(e);
|
|
4446
4948
|
queueMicrotask(() => this._persistActive());
|
|
4447
4949
|
};
|
|
4448
|
-
this._onMessageAborted = () => {
|
|
4449
|
-
|
|
4950
|
+
this._onMessageAborted = (e) => {
|
|
4951
|
+
ended(e);
|
|
4450
4952
|
queueMicrotask(() => this._persistActive());
|
|
4451
4953
|
};
|
|
4452
|
-
this._onMessagePaused = () => {
|
|
4954
|
+
this._onMessagePaused = (e) => {
|
|
4955
|
+
if (this._ofThisTurn(e)) this._setTurn("waiting");
|
|
4453
4956
|
queueMicrotask(() => this._persistActive());
|
|
4454
4957
|
};
|
|
4958
|
+
this._onMessageResumed = (e) => {
|
|
4959
|
+
if (this._ofThisTurn(e)) this._setTurn("running");
|
|
4960
|
+
};
|
|
4455
4961
|
host.addEventListener("aparte-message-start", this._onMessageStart);
|
|
4456
4962
|
host.addEventListener("aparte-message-done", this._onMessageDone);
|
|
4457
4963
|
host.addEventListener("aparte-message-error", this._onMessageError);
|
|
4458
4964
|
host.addEventListener("aparte-message-aborted", this._onMessageAborted);
|
|
4459
4965
|
host.addEventListener("aparte-message-paused", this._onMessagePaused);
|
|
4966
|
+
host.addEventListener("aparte-message-resumed", this._onMessageResumed);
|
|
4460
4967
|
this._onSelectConversation = (e) => {
|
|
4461
4968
|
const evt = e;
|
|
4462
4969
|
const targetId = evt.detail?.targetId;
|
|
@@ -4483,6 +4990,9 @@ class AparteConversationController {
|
|
|
4483
4990
|
this._bound = false;
|
|
4484
4991
|
this._subscribedTo = null;
|
|
4485
4992
|
this._persistActive();
|
|
4993
|
+
if (this._turn?.status === "running") this._setTurn(null);
|
|
4994
|
+
this._turn = null;
|
|
4995
|
+
this._turnMessage = null;
|
|
4486
4996
|
this._stopHydrationRetry?.();
|
|
4487
4997
|
this._stopHydrationRetry = null;
|
|
4488
4998
|
const host = this._binding.host;
|
|
@@ -4514,6 +5024,10 @@ class AparteConversationController {
|
|
|
4514
5024
|
host.removeEventListener("aparte-message-paused", this._onMessagePaused);
|
|
4515
5025
|
this._onMessagePaused = null;
|
|
4516
5026
|
}
|
|
5027
|
+
if (this._onMessageResumed) {
|
|
5028
|
+
host.removeEventListener("aparte-message-resumed", this._onMessageResumed);
|
|
5029
|
+
this._onMessageResumed = null;
|
|
5030
|
+
}
|
|
4517
5031
|
if (this._onCompactDone && typeof window !== "undefined") {
|
|
4518
5032
|
window.removeEventListener("aparte-compact-done", this._onCompactDone);
|
|
4519
5033
|
this._onCompactDone = null;
|
|
@@ -4542,6 +5056,8 @@ class AparteConversationController {
|
|
|
4542
5056
|
const isSwitch = id !== this._activeId;
|
|
4543
5057
|
const manager = this._manager();
|
|
4544
5058
|
if (isSwitch && this._isStreaming) {
|
|
5059
|
+
if (this._turn?.status === "waiting") this._turn = null;
|
|
5060
|
+
else this._setTurn(null);
|
|
4545
5061
|
this._persistActive();
|
|
4546
5062
|
try {
|
|
4547
5063
|
window.dispatchEvent(new CustomEvent("aparte-abort", {
|
|
@@ -4723,6 +5239,7 @@ class AparteConversationController {
|
|
|
4723
5239
|
}
|
|
4724
5240
|
this._activeId = convId;
|
|
4725
5241
|
this._options.onConversationCreated?.(convId);
|
|
5242
|
+
if (this._isStreaming && !this._turn) this._setTurn("running");
|
|
4726
5243
|
} else if (this._activeId !== null && this._activeId !== convId) {
|
|
4727
5244
|
await manager.addMessage(convId, userMsg).catch(() => {
|
|
4728
5245
|
});
|
|
@@ -6291,7 +6808,16 @@ class AparteClient {
|
|
|
6291
6808
|
*/
|
|
6292
6809
|
message: () => {
|
|
6293
6810
|
const loc = this._config.getLocale();
|
|
6294
|
-
|
|
6811
|
+
const chat = resolveConfig(target);
|
|
6812
|
+
const call = {
|
|
6813
|
+
id: `tool-${event.id}`,
|
|
6814
|
+
type: "tool_call",
|
|
6815
|
+
status: "awaiting-approval",
|
|
6816
|
+
toolCall: { id: event.id, name: event.name, input: event.input }
|
|
6817
|
+
};
|
|
6818
|
+
const name = toolNameOf(call, chat).text;
|
|
6819
|
+
const what = toolTargetOf(call, chat);
|
|
6820
|
+
return (loc.approvalAsk ?? "Allow {tool}?").replace("{tool}", () => what ? `${name} ${what}` : name);
|
|
6295
6821
|
},
|
|
6296
6822
|
// A plain string, not a function: the arguments are the model's — its JSON, or
|
|
6297
6823
|
// the tool's own formatter's reading of it — and do not change with the
|
|
@@ -6308,11 +6834,11 @@ class AparteClient {
|
|
|
6308
6834
|
signal,
|
|
6309
6835
|
target
|
|
6310
6836
|
});
|
|
6311
|
-
if (answer.action !== "accept") return { approved: false };
|
|
6837
|
+
if (answer.action !== "accept") return { approved: false, by: "person" };
|
|
6312
6838
|
const content = answer.content;
|
|
6313
6839
|
const instruction = content?.instruction?.trim();
|
|
6314
|
-
if (instruction) return { approved: false, instruction };
|
|
6315
|
-
return
|
|
6840
|
+
if (instruction) return { approved: false, instruction, by: "person" };
|
|
6841
|
+
return content?.option === "allow" ? { approved: true } : { approved: false, by: "person" };
|
|
6316
6842
|
}
|
|
6317
6843
|
/**
|
|
6318
6844
|
* Resolve the auth for a provider: `options.keyResolver` takes precedence,
|
|
@@ -6378,6 +6904,7 @@ class AparteClient {
|
|
|
6378
6904
|
return;
|
|
6379
6905
|
}
|
|
6380
6906
|
dispatchLifecycleEvent(targetElement, "aparte-message-start", { messageId, role: "assistant" }, chatId);
|
|
6907
|
+
if (opts?.resume) dispatchLifecycleEvent(targetElement, "aparte-message-paused", { messageId }, chatId);
|
|
6381
6908
|
if (!opts?.resume && provider.id) {
|
|
6382
6909
|
this._updateMessage(targetElement, messageId, {
|
|
6383
6910
|
model: {
|
|
@@ -6390,6 +6917,7 @@ class AparteClient {
|
|
|
6390
6917
|
const streamController = new AbortController();
|
|
6391
6918
|
if (this._streamController !== streamController) this._streamController?.abort();
|
|
6392
6919
|
this._streamController = streamController;
|
|
6920
|
+
this._turnsInFlight.add(messageId);
|
|
6393
6921
|
try {
|
|
6394
6922
|
let result = opts?.resume ? { usage: void 0, suspended: opts.resume } : await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController);
|
|
6395
6923
|
const maxResumes = this.options.maxTurns ?? 10;
|
|
@@ -6421,6 +6949,7 @@ class AparteClient {
|
|
|
6421
6949
|
};
|
|
6422
6950
|
const messages2 = this._resumeMessages(targetElement, messageId, baseRequest.messages);
|
|
6423
6951
|
this._spendPause(targetElement, messageId);
|
|
6952
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-resumed", { messageId }, chatId);
|
|
6424
6953
|
result = await this._streamLoop(
|
|
6425
6954
|
targetElement,
|
|
6426
6955
|
messageId,
|
|
@@ -6447,8 +6976,15 @@ class AparteClient {
|
|
|
6447
6976
|
}
|
|
6448
6977
|
const aparteError = AparteError.from(error, AparteErrorCode.UNKNOWN_ERROR);
|
|
6449
6978
|
this._handleLifecycleError(targetElement, messageId, aparteError);
|
|
6979
|
+
} finally {
|
|
6980
|
+
this._turnsInFlight.delete(messageId);
|
|
6450
6981
|
}
|
|
6451
6982
|
}
|
|
6983
|
+
/**
|
|
6984
|
+
* Replies a turn is running into right now, a pause it waits on included: a second
|
|
6985
|
+
* load of their conversation must not start them again (`resumeDeferred`).
|
|
6986
|
+
*/
|
|
6987
|
+
_turnsInFlight = /* @__PURE__ */ new Set();
|
|
6452
6988
|
/**
|
|
6453
6989
|
* Handle aparte-retry — add a sibling branch to the assistant message and re-stream
|
|
6454
6990
|
* using the same conversation history minus the retried reply.
|
|
@@ -7132,7 +7668,7 @@ ${content}
|
|
|
7132
7668
|
const ruling = gatedElsewhere ? this._config.getApprovalPolicy()?.(call, this._config.getTools().find((t) => t.name === call.name)) ?? { verdict: "ask" } : this._config.ruleOnToolCall(call);
|
|
7133
7669
|
if (ruling.verdict === "allow") return { approved: true };
|
|
7134
7670
|
if (ruling.verdict === "deny") {
|
|
7135
|
-
return { approved: false, reason: ruling.reason?.trim() || "Tool execution was refused by the approval policy." };
|
|
7671
|
+
return { approved: false, reason: ruling.reason?.trim() || "Tool execution was refused by the approval policy.", by: "policy" };
|
|
7136
7672
|
}
|
|
7137
7673
|
return this._askForApproval(call, sig, targetElement);
|
|
7138
7674
|
});
|
|
@@ -7150,7 +7686,7 @@ ${content}
|
|
|
7150
7686
|
const resolver = this._approvalResolverFor(targetElement, true);
|
|
7151
7687
|
const refuseTheRest = (after) => {
|
|
7152
7688
|
for (const rest of pending.slice(after + 1)) {
|
|
7153
|
-
outcomes[rest.toolCallId] = { kind: "decision", approved: false, reason: NOT_RUN_AFTER_A_REFUSAL };
|
|
7689
|
+
outcomes[rest.toolCallId] = { kind: "decision", approved: false, reason: NOT_RUN_AFTER_A_REFUSAL, by: "earlier-refusal" };
|
|
7154
7690
|
}
|
|
7155
7691
|
};
|
|
7156
7692
|
for (const [index, p] of pending.entries()) {
|
|
@@ -7320,7 +7856,7 @@ ${content}
|
|
|
7320
7856
|
const last = [...messages].reverse().find((m) => m.role === "assistant");
|
|
7321
7857
|
const open = (last?.segments ?? []).filter(isOpenDeferred);
|
|
7322
7858
|
const ticket = open.find((s) => s.deferred?.ticket)?.deferred?.ticket;
|
|
7323
|
-
if (!last || open.length === 0 || !ticket || this._resuming.has(last.id)) return false;
|
|
7859
|
+
if (!last || open.length === 0 || !ticket || this._resuming.has(last.id) || this._turnsInFlight.has(last.id)) return false;
|
|
7324
7860
|
const config = this._config.getModelConfig();
|
|
7325
7861
|
const recorded = last.model && this._config.getAIProvider(last.model.providerId) ? last.model : void 0;
|
|
7326
7862
|
const providerId = recorded?.providerId ?? config.defaultProvider;
|
|
@@ -7367,7 +7903,7 @@ ${content}
|
|
|
7367
7903
|
id: `error-${uuid()}`,
|
|
7368
7904
|
type: "error",
|
|
7369
7905
|
content: error.message,
|
|
7370
|
-
|
|
7906
|
+
code: error.code
|
|
7371
7907
|
};
|
|
7372
7908
|
if (target.updateMessage) {
|
|
7373
7909
|
const rendered = target.getMessage?.(messageId)?.segments ?? target.getMessages?.()?.find((m) => m.id === messageId)?.segments ?? [];
|
|
@@ -7753,6 +8289,7 @@ const APARTE_DEFAULT_UI_EVENTS = [
|
|
|
7753
8289
|
"aparte-message-error",
|
|
7754
8290
|
"aparte-message-aborted",
|
|
7755
8291
|
"aparte-message-paused",
|
|
8292
|
+
"aparte-message-resumed",
|
|
7756
8293
|
"aparte-tool-approval-request"
|
|
7757
8294
|
];
|
|
7758
8295
|
function applyElementProps(el2, props, transformValue = (value) => value) {
|
|
@@ -7864,24 +8401,28 @@ export {
|
|
|
7864
8401
|
segmentDuration as a4,
|
|
7865
8402
|
segmentTiming as a5,
|
|
7866
8403
|
subscribeConfigChange as a6,
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
8404
|
+
toolRefusal as a7,
|
|
8405
|
+
unregisterSegmentRenderer as a8,
|
|
8406
|
+
uuid as a9,
|
|
8407
|
+
eventOrigin as aa,
|
|
8408
|
+
activeElementIn as ab,
|
|
8409
|
+
gateGlide as ac,
|
|
8410
|
+
renderingVersionOf as ad,
|
|
8411
|
+
mergeSegmentUpdate as ae,
|
|
8412
|
+
writeStreamedMarkdown as af,
|
|
8413
|
+
stampSegmentActivity as ag,
|
|
8414
|
+
stampSegmentOnInsert as ah,
|
|
8415
|
+
stampSegmentOnUpdate as ai,
|
|
8416
|
+
renumberSegments as aj,
|
|
8417
|
+
openSegmentIds as ak,
|
|
8418
|
+
isTerminalStatus as al,
|
|
8419
|
+
adoptMessageSegments as am,
|
|
8420
|
+
APARTE_DERIVED_SEGMENTS as an,
|
|
8421
|
+
rememberConversationStatus as ao,
|
|
8422
|
+
tellConversationStatus as ap,
|
|
8423
|
+
conversationStatusOf as aq,
|
|
8424
|
+
chatBoundaryOf as ar,
|
|
8425
|
+
rootOf as as,
|
|
7885
8426
|
APARTE_DEFAULT_BUBBLE_ACTIONS as b,
|
|
7886
8427
|
APARTE_DEFAULT_HOST_HANDLERS as c,
|
|
7887
8428
|
APARTE_DEFAULT_ICON_FALLBACKS as d,
|
|
@@ -7908,4 +8449,4 @@ export {
|
|
|
7908
8449
|
collectRendererStyles as y,
|
|
7909
8450
|
contentToText as z
|
|
7910
8451
|
};
|
|
7911
|
-
//# sourceMappingURL=dialog-triggers-
|
|
8452
|
+
//# sourceMappingURL=dialog-triggers-BqXRdHWZ.js.map
|