@aparte/core 0.10.0 → 0.11.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 +54 -33
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/client/stream-adapter.d.ts +6 -1
- package/dist/client/stream-adapter.d.ts.map +1 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts +25 -4
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/chat/aparte-chat.d.ts +8 -4
- package/dist/components/chat/aparte-chat.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-action.d.ts +5 -5
- package/dist/components/composer/aparte-composer-add-attachment.d.ts +5 -4
- package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-attachments.d.ts +5 -3
- package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-cancel.d.ts +2 -2
- package/dist/components/composer/aparte-composer-input.d.ts +8 -5
- package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-send.d.ts +2 -2
- package/dist/components/composer/aparte-composer-toolbar.d.ts +3 -3
- package/dist/components/composer/aparte-composer.d.ts +54 -10
- package/dist/components/composer/aparte-composer.d.ts.map +1 -1
- package/dist/components/conversation-list/aparte-conversation-list.d.ts +19 -12
- package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
- package/dist/components/elicitation/aparte-elicitation.d.ts +12 -0
- package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
- package/dist/components/status/aparte-chat-status.d.ts +9 -0
- package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +15 -4
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +20 -1
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/locale.d.ts +43 -0
- package/dist/config/locale.d.ts.map +1 -1
- package/dist/custom-elements.json +8517 -7601
- package/dist/elicitation/approval-panel.d.ts +42 -0
- package/dist/elicitation/approval-panel.d.ts.map +1 -0
- package/dist/elicitation/index.d.ts +4 -1
- package/dist/elicitation/index.d.ts.map +1 -1
- package/dist/elicitation/panel.d.ts +1 -1
- package/dist/elicitation/panel.d.ts.map +1 -1
- package/dist/elicitation/types.d.ts +116 -9
- package/dist/elicitation/types.d.ts.map +1 -1
- package/dist/generated/element-attributes.d.ts +173 -0
- package/dist/generated/element-attributes.d.ts.map +1 -0
- package/dist/index.css +615 -92
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +341 -254
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +3 -3
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +46 -44
- package/dist/index.node.js.map +1 -1
- package/dist/interop/element-props.d.ts +26 -1
- package/dist/interop/element-props.d.ts.map +1 -1
- package/dist/{index-BnKBciDG.js → is-awaiting-reply-Cb7H3YfN.js} +1448 -1544
- package/dist/is-awaiting-reply-Cb7H3YfN.js.map +1 -0
- package/dist/primitives/select/aparte-optgroup.d.ts +3 -0
- package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
- package/dist/primitives/select/aparte-option.d.ts +1 -0
- package/dist/primitives/select/aparte-option.d.ts.map +1 -1
- package/dist/primitives/select/aparte-select.d.ts +22 -18
- package/dist/primitives/select/aparte-select.d.ts.map +1 -1
- package/dist/renderers/segment-renderers.d.ts.map +1 -1
- package/dist/renderers/segments/artifact/card.d.ts.map +1 -1
- package/dist/renderers/segments/pipeline-waiting.d.ts.map +1 -1
- package/dist/renderers/segments/tool-call.d.ts.map +1 -1
- package/dist/types/element-attributes.d.ts +42 -0
- package/dist/types/element-attributes.d.ts.map +1 -0
- package/dist/types/event-map.d.ts +1 -2
- package/dist/types/event-map.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/segments.d.ts +1 -8
- package/dist/types/segments.d.ts.map +1 -1
- package/dist/types/tools.d.ts +15 -21
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/utils/segments.d.ts +8 -0
- package/dist/utils/segments.d.ts.map +1 -1
- package/package.json +3 -2
- package/dist/index-BnKBciDG.js.map +0 -1
- package/dist/renderers/segments/progress.d.ts +0 -3
- package/dist/renderers/segments/progress.d.ts.map +0 -1
|
@@ -75,6 +75,16 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
75
75
|
elicitationYes: "Yes",
|
|
76
76
|
elicitationNo: "No",
|
|
77
77
|
elicitationAnswerLabel: "Your answer",
|
|
78
|
+
approvalInstructionPlaceholder: "Or tell it what to do instead…",
|
|
79
|
+
approvalAsk: "Run {tool}?",
|
|
80
|
+
approvalWaiting: "waiting for you",
|
|
81
|
+
toolRunning: "Running",
|
|
82
|
+
toolCompleted: "Done",
|
|
83
|
+
toolRejected: "Rejected",
|
|
84
|
+
toolStopped: "Stopped",
|
|
85
|
+
toolInput: "Input",
|
|
86
|
+
toolOutput: "Output",
|
|
87
|
+
approvalOptionsLabel: "Your decision",
|
|
78
88
|
actionUpload: "Attach file",
|
|
79
89
|
download: "Download",
|
|
80
90
|
previewPending: "Press Preview to run this artifact.",
|
|
@@ -1024,6 +1034,23 @@ const defaultSanitizer = (html) => {
|
|
|
1024
1034
|
sanitizeChildren(doc.body, container, doc);
|
|
1025
1035
|
return container.innerHTML;
|
|
1026
1036
|
};
|
|
1037
|
+
class AparteElicitationAbortError extends DOMException {
|
|
1038
|
+
/**
|
|
1039
|
+
* Which of the two ways it ended.
|
|
1040
|
+
*
|
|
1041
|
+
* `'no-presenter'` is a developer's setup problem and `'aborted'` is a person
|
|
1042
|
+
* stopping something; a host that reports them the same way reports a bug as a
|
|
1043
|
+
* user action. The distinction is why this carries a field at all.
|
|
1044
|
+
*/
|
|
1045
|
+
reason;
|
|
1046
|
+
constructor(reason = "aborted") {
|
|
1047
|
+
super(
|
|
1048
|
+
reason === "no-presenter" ? "No elicitation presenter is mounted, so the request could not be shown." : "The request ended without an answer.",
|
|
1049
|
+
"AbortError"
|
|
1050
|
+
);
|
|
1051
|
+
this.reason = reason;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1027
1054
|
function escapeHtml(value) {
|
|
1028
1055
|
let out = "";
|
|
1029
1056
|
for (let i = 0; i < value.length; i++) {
|
|
@@ -1050,6 +1077,8 @@ const APARTE_DEFAULT_HOST_HANDLERS = {
|
|
|
1050
1077
|
artifactRedownload: false,
|
|
1051
1078
|
artifactRehydrate: false
|
|
1052
1079
|
};
|
|
1080
|
+
const NOOP = () => {
|
|
1081
|
+
};
|
|
1053
1082
|
class AparteConfig {
|
|
1054
1083
|
_markdownProvider;
|
|
1055
1084
|
_streamingMarkdownProvider;
|
|
@@ -1106,6 +1135,23 @@ class AparteConfig {
|
|
|
1106
1135
|
_elicitationPresenter;
|
|
1107
1136
|
/** Warn once per config, not once per request — a tool loop can ask repeatedly. */
|
|
1108
1137
|
_warnedNoPresenter = false;
|
|
1138
|
+
/**
|
|
1139
|
+
* The tail of the queue of requests waiting for the human, or `null` when none is.
|
|
1140
|
+
*
|
|
1141
|
+
* One request reaches the presenter at a time — not because a presenter could not
|
|
1142
|
+
* cope, but because the COMPOSER cannot: it has one panel slot, and a second
|
|
1143
|
+
* request used to clobber the first's DOM. The previous protection lived in the
|
|
1144
|
+
* default presenter, which answered the second request `cancel` on the spot; that
|
|
1145
|
+
* is a refusal invented for a question nobody was ever shown, the same class of
|
|
1146
|
+
* lie as telling the model a stopped turn was refused. WAITING is the honest
|
|
1147
|
+
* behaviour.
|
|
1148
|
+
*
|
|
1149
|
+
* Here rather than in the presenter because this is the only place that sees
|
|
1150
|
+
* EVERY request whatever raised it — a tool handler, an app's own button, and
|
|
1151
|
+
* (soon) the tool-approval gate. A custom presenter registered by a consumer gets
|
|
1152
|
+
* the same protection for free, which it previously had none of.
|
|
1153
|
+
*/
|
|
1154
|
+
_elicitationQueue = null;
|
|
1109
1155
|
// Tool Registry
|
|
1110
1156
|
_tools = /* @__PURE__ */ new Map();
|
|
1111
1157
|
_toolRenderers = /* @__PURE__ */ new Map();
|
|
@@ -1811,7 +1857,7 @@ class AparteConfig {
|
|
|
1811
1857
|
*
|
|
1812
1858
|
* @example
|
|
1813
1859
|
* // Custom pill for a web-search tool
|
|
1814
|
-
* aparteGlobalConfig.registerToolRenderer('web_search', { render: (seg) => `<div class="tool-
|
|
1860
|
+
* aparteGlobalConfig.registerToolRenderer('web_search', { render: (seg) => `<div class="aparte-tool-label">Searching...</div>` });
|
|
1815
1861
|
*/
|
|
1816
1862
|
registerToolRenderer(toolName, renderer) {
|
|
1817
1863
|
this._toolRenderers.set(toolName, renderer);
|
|
@@ -1908,15 +1954,29 @@ class AparteConfig {
|
|
|
1908
1954
|
* itself — nothing to register", which is how this failure mode was found.
|
|
1909
1955
|
*/
|
|
1910
1956
|
requestUserInput(request) {
|
|
1957
|
+
const previous = this._elicitationQueue;
|
|
1958
|
+
const settled = previous === null ? this._present(request) : previous.then(
|
|
1959
|
+
() => this._present(request),
|
|
1960
|
+
() => this._present(request)
|
|
1961
|
+
);
|
|
1962
|
+
const mine = settled.then(NOOP, NOOP).then(() => {
|
|
1963
|
+
if (this._elicitationQueue === mine) this._elicitationQueue = null;
|
|
1964
|
+
});
|
|
1965
|
+
this._elicitationQueue = mine;
|
|
1966
|
+
return settled;
|
|
1967
|
+
}
|
|
1968
|
+
/** Hand ONE request to the presenter. Called only from the queue. */
|
|
1969
|
+
_present(request) {
|
|
1911
1970
|
if (!this._elicitationPresenter) {
|
|
1912
1971
|
if (!this._warnedNoPresenter) {
|
|
1913
1972
|
this._warnedNoPresenter = true;
|
|
1914
1973
|
console.warn(
|
|
1915
|
-
"[aparte] requestUserInput() was called with no elicitation presenter, so it
|
|
1974
|
+
"[aparte] requestUserInput() was called with no elicitation presenter, so it rejected with an AbortError — nothing was ever shown to anybody. Add <aparte-elicitation></aparte-elicitation> inside your <aparte-chat>, or register your own by calling setElicitationPresenter() on the config this chat resolves — the scoped one if you passed a `config`, aparteGlobalConfig otherwise."
|
|
1916
1975
|
);
|
|
1917
1976
|
}
|
|
1918
|
-
return Promise.
|
|
1977
|
+
return Promise.reject(new AparteElicitationAbortError("no-presenter"));
|
|
1919
1978
|
}
|
|
1979
|
+
if (request.signal?.aborted) return Promise.reject(new AparteElicitationAbortError("aborted"));
|
|
1920
1980
|
return this._elicitationPresenter(request);
|
|
1921
1981
|
}
|
|
1922
1982
|
/**
|
|
@@ -1944,6 +2004,7 @@ class AparteConfig {
|
|
|
1944
2004
|
this._keyProvider = void 0;
|
|
1945
2005
|
this._conversationManager = void 0;
|
|
1946
2006
|
this._elicitationPresenter = void 0;
|
|
2007
|
+
this._elicitationQueue = null;
|
|
1947
2008
|
this._locale = APARTE_DEFAULT_LOCALE;
|
|
1948
2009
|
this._actions = [];
|
|
1949
2010
|
this._sanitizer = defaultSanitizer;
|
|
@@ -2094,9 +2155,9 @@ function highlightMarkdownFences(contentEl) {
|
|
|
2094
2155
|
}
|
|
2095
2156
|
const textRenderer = {
|
|
2096
2157
|
type: "text",
|
|
2097
|
-
render: (segment) => `<div class="segment segment-text" data-segment-id="${escapeHtml(segment.id)}"><div class="segment-content">${contextConfig().renderMarkdown(segment.content)}</div></div>`,
|
|
2158
|
+
render: (segment) => `<div class="aparte-segment aparte-segment-text" data-segment-id="${escapeHtml(segment.id)}"><div class="aparte-segment-content">${contextConfig().renderMarkdown(segment.content)}</div></div>`,
|
|
2098
2159
|
update: (el2, segment) => {
|
|
2099
|
-
const contentEl = el2.querySelector(".segment-content");
|
|
2160
|
+
const contentEl = el2.querySelector(".aparte-segment-content");
|
|
2100
2161
|
if (!contentEl) return;
|
|
2101
2162
|
writeStreamedMarkdown(
|
|
2102
2163
|
el2,
|
|
@@ -2124,17 +2185,17 @@ const thinkingRenderer = {
|
|
|
2124
2185
|
* new class of risk. With no Markdown provider registered it degrades to the
|
|
2125
2186
|
* zero-dependency default (escape + `<br>`), which is what it used to be.
|
|
2126
2187
|
*/
|
|
2127
|
-
render: (segment) => `<details class="segment segment-thinking" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed === false ? "open" : ""}><summary class="thinking-header"><span class="thinking-label">${escapeHtml(segment.label || contextConfig().t("thinking"))}</span><span class="thinking-toggle"></span></summary><div class="thinking-content">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
|
|
2188
|
+
render: (segment) => `<details class="aparte-segment aparte-segment-thinking" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed === false ? "open" : ""}><summary class="aparte-thinking-header"><span class="aparte-thinking-label">${escapeHtml(segment.label || contextConfig().t("thinking"))}</span><span class="aparte-thinking-toggle"></span></summary><div class="aparte-thinking-content">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
|
|
2128
2189
|
/**
|
|
2129
2190
|
* The default label is the only config-derived text here — and `segment.label`
|
|
2130
2191
|
* still wins, exactly as in `render`, because that string is the app's.
|
|
2131
2192
|
*/
|
|
2132
2193
|
relabel: (el2, segment) => {
|
|
2133
|
-
const label = el2.querySelector(".thinking-label");
|
|
2194
|
+
const label = el2.querySelector(".aparte-thinking-label");
|
|
2134
2195
|
if (label) label.textContent = segment.label || contextConfig().t("thinking");
|
|
2135
2196
|
},
|
|
2136
2197
|
update: (el2, segment) => {
|
|
2137
|
-
const contentEl = el2.querySelector(".thinking-content");
|
|
2198
|
+
const contentEl = el2.querySelector(".aparte-thinking-content");
|
|
2138
2199
|
if (!contentEl) return;
|
|
2139
2200
|
const wasAtBottom = contentEl.scrollHeight - contentEl.scrollTop - contentEl.clientHeight <= THINKING_ANCHOR_SLACK;
|
|
2140
2201
|
const streaming = segment.isStreaming !== false;
|
|
@@ -2194,38 +2255,38 @@ function streamHighlight(element, paneSelector, content, lang, segId) {
|
|
|
2194
2255
|
const codeRenderer = {
|
|
2195
2256
|
type: "code",
|
|
2196
2257
|
render: (segment) => `
|
|
2197
|
-
<div class="segment segment-code" data-segment-id="${escapeHtml(segment.id)}">
|
|
2198
|
-
<div class="code-header">
|
|
2199
|
-
${segment.filename ? `<span class="code-filename">${escapeHtml(segment.filename)}</span>` : `<span class="code-header-filler"></span>`}
|
|
2200
|
-
<span class="code-language">${escapeHtml(segment.language || "")}</span>
|
|
2201
|
-
<button class="code-copy" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}">
|
|
2258
|
+
<div class="aparte-segment aparte-segment-code" data-segment-id="${escapeHtml(segment.id)}">
|
|
2259
|
+
<div class="aparte-code-header">
|
|
2260
|
+
${segment.filename ? `<span class="aparte-code-filename">${escapeHtml(segment.filename)}</span>` : `<span class="aparte-code-header-filler"></span>`}
|
|
2261
|
+
<span class="aparte-code-language">${escapeHtml(segment.language || "")}</span>
|
|
2262
|
+
<button class="aparte-code-copy" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}">
|
|
2202
2263
|
${contextConfig().getIcon("copy")}
|
|
2203
2264
|
</button>
|
|
2204
2265
|
</div>
|
|
2205
|
-
<div class="code-content-wrapper">
|
|
2266
|
+
<div class="aparte-code-content-wrapper">
|
|
2206
2267
|
<pre><code class="language-${escapeHtml(segment.language || "text")}">${escapeHtml(segment.content)}</code></pre>
|
|
2207
2268
|
</div>
|
|
2208
2269
|
</div>
|
|
2209
2270
|
`,
|
|
2210
2271
|
/** The copy button's icon and tooltip — nothing else here comes from config. */
|
|
2211
2272
|
relabel: (element) => {
|
|
2212
|
-
const copyBtn = element.querySelector(".code-copy");
|
|
2273
|
+
const copyBtn = element.querySelector(".aparte-code-copy");
|
|
2213
2274
|
if (!copyBtn || copyBtn.dataset.copied) return;
|
|
2214
2275
|
copyBtn.setAttribute("title", contextConfig().t("copy"));
|
|
2215
2276
|
copyBtn.innerHTML = contextConfig().getIcon("copy");
|
|
2216
2277
|
},
|
|
2217
2278
|
setup: (element, segment) => {
|
|
2218
|
-
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2279
|
+
const wrapper = element.querySelector(".aparte-code-content-wrapper");
|
|
2219
2280
|
if (wrapper) {
|
|
2220
2281
|
void contextConfig().highlightCode(segment.content, segment.language || "").then((html) => {
|
|
2221
2282
|
wrapper.innerHTML = html;
|
|
2222
2283
|
}).catch(() => {
|
|
2223
2284
|
});
|
|
2224
2285
|
}
|
|
2225
|
-
const copyBtn = element.querySelector(".code-copy");
|
|
2286
|
+
const copyBtn = element.querySelector(".aparte-code-copy");
|
|
2226
2287
|
if (copyBtn) {
|
|
2227
2288
|
copyBtn.addEventListener("click", () => {
|
|
2228
|
-
const source = element.querySelector(".code-content-wrapper")?.textContent ?? "";
|
|
2289
|
+
const source = element.querySelector(".aparte-code-content-wrapper")?.textContent ?? "";
|
|
2229
2290
|
void navigator.clipboard.writeText(source).catch(() => {
|
|
2230
2291
|
});
|
|
2231
2292
|
copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
|
|
@@ -2241,9 +2302,9 @@ const codeRenderer = {
|
|
|
2241
2302
|
},
|
|
2242
2303
|
update: (element, segment) => {
|
|
2243
2304
|
if (segment.isStreaming) {
|
|
2244
|
-
streamHighlight(element, ".code-content-wrapper", segment.content, segment.language || "", segment.id);
|
|
2305
|
+
streamHighlight(element, ".aparte-code-content-wrapper", segment.content, segment.language || "", segment.id);
|
|
2245
2306
|
} else {
|
|
2246
|
-
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2307
|
+
const wrapper = element.querySelector(".aparte-code-content-wrapper");
|
|
2247
2308
|
if (wrapper) {
|
|
2248
2309
|
void contextConfig().highlightCode(segment.content, segment.language || "").then((html) => {
|
|
2249
2310
|
wrapper.innerHTML = html;
|
|
@@ -2267,14 +2328,14 @@ const errorRenderer = {
|
|
|
2267
2328
|
return out;
|
|
2268
2329
|
}
|
|
2269
2330
|
return `
|
|
2270
|
-
<div class="segment segment-error" data-segment-id="${escapeHtml(segment.id)}">
|
|
2271
|
-
<div class="error-icon-wrapper">
|
|
2331
|
+
<div class="aparte-segment aparte-segment-error" data-segment-id="${escapeHtml(segment.id)}">
|
|
2332
|
+
<div class="aparte-error-icon-wrapper">
|
|
2272
2333
|
${contextConfig().getIcon("error") || "⚠"}
|
|
2273
2334
|
</div>
|
|
2274
|
-
<div class="error-content">
|
|
2275
|
-
<div class="error-title">${escapeHtml(contextConfig().t("error"))}</div>
|
|
2276
|
-
<div class="error-message">${escapeHtml(segment.content)}</div>
|
|
2277
|
-
${segment.details ? `<div class="error-details">${escapeHtml(segment.details)}</div>` : ""}
|
|
2335
|
+
<div class="aparte-error-content">
|
|
2336
|
+
<div class="aparte-error-title">${escapeHtml(contextConfig().t("error"))}</div>
|
|
2337
|
+
<div class="aparte-error-message">${escapeHtml(segment.content)}</div>
|
|
2338
|
+
${segment.details ? `<div class="aparte-error-details">${escapeHtml(segment.details)}</div>` : ""}
|
|
2278
2339
|
</div>
|
|
2279
2340
|
</div>
|
|
2280
2341
|
`;
|
|
@@ -2289,171 +2350,204 @@ const errorRenderer = {
|
|
|
2289
2350
|
* language it arrived in. Relabelling it would be inventing content.
|
|
2290
2351
|
*/
|
|
2291
2352
|
relabel: (element) => {
|
|
2292
|
-
const wrap = element.querySelector(".error-icon-wrapper");
|
|
2353
|
+
const wrap = element.querySelector(".aparte-error-icon-wrapper");
|
|
2293
2354
|
if (wrap) wrap.innerHTML = contextConfig().getIcon("error") || "⚠";
|
|
2294
|
-
const title = element.querySelector(".error-title");
|
|
2355
|
+
const title = element.querySelector(".aparte-error-title");
|
|
2295
2356
|
if (title) title.textContent = contextConfig().t("error");
|
|
2296
2357
|
},
|
|
2297
2358
|
getStyles: () => ``
|
|
2298
2359
|
};
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
}
|
|
2360
|
+
function describeInput(segment) {
|
|
2361
|
+
const input = segment.toolCall?.input;
|
|
2362
|
+
if (!input || typeof input !== "object" || Object.keys(input).length === 0) return "";
|
|
2363
|
+
try {
|
|
2364
|
+
return JSON.stringify(input, null, 2);
|
|
2365
|
+
} catch {
|
|
2366
|
+
return "";
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
function stateBadge(segment) {
|
|
2370
|
+
const cfg = contextConfig();
|
|
2371
|
+
const status = segment.status ?? "pending";
|
|
2372
|
+
const word = (key) => escapeHtml(cfg.t(key));
|
|
2373
|
+
if (status === "awaiting-approval") return word("approvalWaiting");
|
|
2374
|
+
if (status === "pending") return word("toolRunning");
|
|
2375
|
+
if (status === "resolved") return `${cfg.getIcon("check")}${word("toolCompleted")}`;
|
|
2376
|
+
if (status === "rejected") return `${cfg.getIcon("close")}${word("toolRejected")}`;
|
|
2377
|
+
if (status === "aborted") return `${cfg.getIcon("close")}${word("toolStopped")}`;
|
|
2378
|
+
return "";
|
|
2379
|
+
}
|
|
2380
|
+
function setPart(element, part, text) {
|
|
2381
|
+
const existing = element.querySelector(`[data-part="${part}"]`);
|
|
2382
|
+
if (!text) {
|
|
2383
|
+
existing?.remove();
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
if (existing) {
|
|
2387
|
+
const code2 = existing.querySelector("code");
|
|
2388
|
+
if (code2 && code2.textContent !== text) code2.textContent = text;
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
const detail = element.querySelector(".aparte-tool-detail");
|
|
2392
|
+
if (!detail) return;
|
|
2393
|
+
const cfg = contextConfig();
|
|
2394
|
+
const wrap = document.createElement("div");
|
|
2395
|
+
wrap.className = "aparte-tool-part";
|
|
2396
|
+
wrap.dataset["part"] = part;
|
|
2397
|
+
const label = document.createElement("span");
|
|
2398
|
+
label.className = "aparte-tool-part-label";
|
|
2399
|
+
label.textContent = cfg.t(part === "input" ? "toolInput" : "toolOutput");
|
|
2400
|
+
const body = document.createElement("div");
|
|
2401
|
+
body.className = "aparte-tool-part-body";
|
|
2402
|
+
const pre = document.createElement("pre");
|
|
2403
|
+
const code = document.createElement("code");
|
|
2404
|
+
code.textContent = text;
|
|
2405
|
+
pre.appendChild(code);
|
|
2406
|
+
body.appendChild(pre);
|
|
2407
|
+
wrap.append(label, body);
|
|
2408
|
+
if (part === "input") detail.prepend(wrap);
|
|
2409
|
+
else detail.appendChild(wrap);
|
|
2410
|
+
}
|
|
2411
|
+
function highlightParts(element) {
|
|
2412
|
+
const cfg = contextConfig();
|
|
2413
|
+
for (const body of element.querySelectorAll(".aparte-tool-part-body")) {
|
|
2414
|
+
const text = body.querySelector("code")?.textContent ?? "";
|
|
2415
|
+
if (!text) continue;
|
|
2416
|
+
void cfg.highlightCode(text, "json").then((html) => {
|
|
2417
|
+
if ((body.querySelector("code")?.textContent ?? "") !== text) return;
|
|
2418
|
+
body.innerHTML = html;
|
|
2419
|
+
}).catch(() => {
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
function pillMarkup(segment, name, status) {
|
|
2424
|
+
const badge = stateBadge(segment);
|
|
2425
|
+
return `
|
|
2426
|
+
<span class="aparte-tool-label">
|
|
2427
|
+
<span class="aparte-tool-icon">${contextConfig().getIcon("tool")}</span>
|
|
2428
|
+
<span class="aparte-tool-name">${escapeHtml(name)}</span>
|
|
2429
|
+
</span>
|
|
2430
|
+
<span class="aparte-tool-spinner" aria-hidden="true"${status === "pending" ? "" : " hidden"}></span>
|
|
2431
|
+
<span class="aparte-tool-state">${badge}</span>`;
|
|
2432
|
+
}
|
|
2308
2433
|
const toolCallRenderer = {
|
|
2309
2434
|
type: "tool_call",
|
|
2310
2435
|
render: (segment) => {
|
|
2311
2436
|
const name = segment.toolCall?.name ?? "tool";
|
|
2312
2437
|
const status = segment.status ?? "pending";
|
|
2313
2438
|
const toolCallId = segment.toolCall?.id ?? "";
|
|
2314
|
-
if (status === "awaiting-approval") {
|
|
2315
|
-
const loc = contextConfig().getLocale();
|
|
2316
|
-
const approve = loc.approveTool ?? "Approve";
|
|
2317
|
-
const reject = loc.rejectTool ?? "Reject";
|
|
2318
|
-
return `
|
|
2319
|
-
<div class="segment segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="awaiting-approval" data-tool-call-id="${escapeAttr(toolCallId)}">
|
|
2320
|
-
<span class="tool-pill">
|
|
2321
|
-
<span class="tool-pill-icon">${contextConfig().getIcon("tool")}</span>
|
|
2322
|
-
<span class="tool-pill-name">${escapeHtml(name)}</span>
|
|
2323
|
-
</span>
|
|
2324
|
-
<span class="tool-approval" role="group" aria-label="${escapeAttr(name)}">
|
|
2325
|
-
<button type="button" class="tool-approve-btn" data-tool-decision="approve" aria-label="${escapeAttr(approve)}">${escapeHtml(approve)}</button>
|
|
2326
|
-
<button type="button" class="tool-reject-btn" data-tool-decision="reject" aria-label="${escapeAttr(reject)}">${escapeHtml(reject)}</button>
|
|
2327
|
-
</span>
|
|
2328
|
-
</div>
|
|
2329
|
-
`;
|
|
2330
|
-
}
|
|
2331
2439
|
const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
|
|
2332
2440
|
if (customRenderer) {
|
|
2333
2441
|
const out = customRenderer.render(segment);
|
|
2334
2442
|
if (out) return out;
|
|
2335
2443
|
}
|
|
2336
|
-
const
|
|
2337
|
-
const
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
<span class="tool-pill-name">${escapeHtml(name)}</span>
|
|
2343
|
-
${spinner}
|
|
2344
|
-
${statusIcon ? `<span class="tool-pill-status">${statusIcon}</span>` : ""}
|
|
2345
|
-
</span>
|
|
2444
|
+
const pill = pillMarkup(segment, name, status);
|
|
2445
|
+
const input = describeInput(segment);
|
|
2446
|
+
const result = segment.result;
|
|
2447
|
+
if (!input && !result) {
|
|
2448
|
+
return `
|
|
2449
|
+
<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}
|
|
2346
2450
|
</div>
|
|
2347
2451
|
`;
|
|
2452
|
+
}
|
|
2453
|
+
const cfg = contextConfig();
|
|
2454
|
+
return `
|
|
2455
|
+
<details class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}">
|
|
2456
|
+
<summary class="aparte-tool-summary"><span class="aparte-tool-toggle"></span>${pill}</summary>
|
|
2457
|
+
<div class="aparte-tool-detail">
|
|
2458
|
+
${input ? `
|
|
2459
|
+
<div class="aparte-tool-part" data-part="input">
|
|
2460
|
+
<span class="aparte-tool-part-label">${escapeHtml(cfg.t("toolInput"))}</span>
|
|
2461
|
+
<div class="aparte-tool-part-body"><pre><code>${escapeHtml(input)}</code></pre></div>
|
|
2462
|
+
</div>` : ""}
|
|
2463
|
+
${result ? `
|
|
2464
|
+
<div class="aparte-tool-part" data-part="output">
|
|
2465
|
+
<span class="aparte-tool-part-label">${escapeHtml(cfg.t("toolOutput"))}</span>
|
|
2466
|
+
<div class="aparte-tool-part-body"><pre><code>${escapeHtml(result)}</code></pre></div>
|
|
2467
|
+
</div>` : ""}
|
|
2468
|
+
</div>
|
|
2469
|
+
</details>
|
|
2470
|
+
`;
|
|
2348
2471
|
},
|
|
2349
2472
|
/**
|
|
2350
|
-
* The pill's glyphs and
|
|
2351
|
-
*
|
|
2352
|
-
*
|
|
2353
|
-
*
|
|
2473
|
+
* The pill's glyphs, and the label it wears while a person is deciding.
|
|
2474
|
+
*
|
|
2475
|
+
* The two highest-stakes strings in the library — the approval labels — are no
|
|
2476
|
+
* longer here: they are on the panel, which relabels itself. This still matters
|
|
2477
|
+
* though, because the request the pill describes stays open for as long as
|
|
2478
|
+
* somebody takes to decide, which is exactly long enough for a language switch.
|
|
2354
2479
|
*
|
|
2355
|
-
* No child node is added or removed,
|
|
2356
|
-
* critically, their FOCUS — this is the one control a keyboard user may be
|
|
2357
|
-
* sitting on when a config change fires.
|
|
2480
|
+
* No child node is added or removed, per the relabel contract.
|
|
2358
2481
|
*/
|
|
2359
2482
|
relabel: (element, segment) => {
|
|
2360
2483
|
const cfg = contextConfig();
|
|
2361
|
-
const icon = element.querySelector(".tool-
|
|
2484
|
+
const icon = element.querySelector(".aparte-tool-icon");
|
|
2362
2485
|
if (icon) icon.innerHTML = cfg.getIcon("tool");
|
|
2363
|
-
const statusEl = element.querySelector(".tool-
|
|
2486
|
+
const statusEl = element.querySelector(".aparte-tool-state");
|
|
2364
2487
|
if (statusEl) {
|
|
2365
2488
|
const s = segment.status;
|
|
2366
2489
|
statusEl.innerHTML = s === "resolved" ? cfg.getIcon("check") : s === "aborted" || s === "rejected" ? cfg.getIcon("close") : "";
|
|
2367
2490
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
if (approve) {
|
|
2371
|
-
const label = loc.approveTool ?? "Approve";
|
|
2372
|
-
approve.textContent = label;
|
|
2373
|
-
approve.setAttribute("aria-label", label);
|
|
2374
|
-
}
|
|
2375
|
-
const reject = element.querySelector(".tool-reject-btn");
|
|
2376
|
-
if (reject) {
|
|
2377
|
-
const label = loc.rejectTool ?? "Reject";
|
|
2378
|
-
reject.textContent = label;
|
|
2379
|
-
reject.setAttribute("aria-label", label);
|
|
2491
|
+
if (statusEl && segment.status === "awaiting-approval") {
|
|
2492
|
+
statusEl.textContent = cfg.t("approvalWaiting");
|
|
2380
2493
|
}
|
|
2381
2494
|
},
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2495
|
+
/**
|
|
2496
|
+
* Patch in place. NEVER re-render, and never touch `open`.
|
|
2497
|
+
*
|
|
2498
|
+
* Without an `update` the bubble replaces the element wholesale — and a tool call
|
|
2499
|
+
* changes status several times per turn, so a disclosure the reader opened would
|
|
2500
|
+
* slam shut under them every time. The same rule the reasoning block has, for the
|
|
2501
|
+
* same reason: `collapsed` is honoured only when an update explicitly carries it.
|
|
2502
|
+
*
|
|
2503
|
+
* The one case that cannot be patched is a change of ELEMENT: a call that had
|
|
2504
|
+
* neither arguments nor a result rendered as a plain pill, and gaining a result
|
|
2505
|
+
* makes it a `<details>`. That falls back to a rebuild, which costs nothing because
|
|
2506
|
+
* there was no disclosure to lose.
|
|
2507
|
+
*/
|
|
2508
|
+
update: (element, segment) => {
|
|
2509
|
+
const status = segment.status ?? "pending";
|
|
2510
|
+
const input = describeInput(segment);
|
|
2511
|
+
const wantsDetail = !!input || !!segment.result;
|
|
2512
|
+
const custom = contextConfig().getToolRenderer(segment.toolCall?.name);
|
|
2513
|
+
if (custom || wantsDetail !== (element.tagName === "DETAILS")) {
|
|
2514
|
+
const out = toolCallRenderer.render(segment);
|
|
2515
|
+
const tpl = document.createElement("template");
|
|
2516
|
+
tpl.innerHTML = typeof out === "string" ? out.trim() : "";
|
|
2517
|
+
const rebuilt = typeof out === "string" ? tpl.content.firstElementChild : out;
|
|
2518
|
+
if (rebuilt) {
|
|
2519
|
+
element.replaceWith(rebuilt);
|
|
2520
|
+
toolCallRenderer.setup?.(rebuilt, segment);
|
|
2521
|
+
}
|
|
2394
2522
|
return;
|
|
2395
2523
|
}
|
|
2524
|
+
element.setAttribute("data-status", status);
|
|
2525
|
+
const badge = element.querySelector(".aparte-tool-state");
|
|
2526
|
+
if (badge) badge.innerHTML = stateBadge(segment);
|
|
2527
|
+
element.querySelector(".aparte-tool-spinner")?.toggleAttribute("hidden", status !== "pending");
|
|
2528
|
+
setPart(element, "input", input);
|
|
2529
|
+
setPart(element, "output", segment.result ?? "");
|
|
2530
|
+
highlightParts(element);
|
|
2531
|
+
},
|
|
2532
|
+
setup: (element, segment) => {
|
|
2533
|
+
highlightParts(element);
|
|
2396
2534
|
const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
|
|
2397
2535
|
customRenderer?.setup?.(element, segment);
|
|
2398
2536
|
},
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
align-items: center;
|
|
2404
|
-
gap: 5px;
|
|
2405
|
-
padding: 3px 10px 3px 7px;
|
|
2406
|
-
border-radius: 99px;
|
|
2407
|
-
font-size: 0.78rem;
|
|
2408
|
-
font-weight: 500;
|
|
2409
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
2410
|
-
background: var(--aparte-surface, #f8f8f8);
|
|
2411
|
-
color: var(--aparte-text-secondary, rgba(0,0,0,0.55));
|
|
2412
|
-
user-select: none;
|
|
2413
|
-
}
|
|
2414
|
-
[data-status="resolved"] .tool-pill {
|
|
2415
|
-
border-color: var(--aparte-success-border, rgba(34,197,94,0.3));
|
|
2416
|
-
background: var(--aparte-success-surface, rgba(34,197,94,0.06));
|
|
2417
|
-
color: var(--aparte-success, rgb(21,128,61));
|
|
2418
|
-
}
|
|
2419
|
-
[data-status="aborted"] .tool-pill,
|
|
2420
|
-
[data-status="rejected"] .tool-pill {
|
|
2421
|
-
border-color: var(--aparte-error-border, rgba(239,68,68,0.3));
|
|
2422
|
-
background: var(--aparte-error-surface, rgba(239,68,68,0.06));
|
|
2423
|
-
color: var(--aparte-error, rgb(185,28,28));
|
|
2424
|
-
}
|
|
2425
|
-
.tool-approval { display: inline-flex; gap: 6px; margin-left: 8px; vertical-align: middle; }
|
|
2426
|
-
.tool-approve-btn, .tool-reject-btn {
|
|
2427
|
-
font: inherit; font-size: 0.78rem; font-weight: 600; line-height: 1;
|
|
2428
|
-
padding: 4px 12px; border-radius: 99px; cursor: pointer;
|
|
2429
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
2430
|
-
background: var(--aparte-surface, #f8f8f8);
|
|
2431
|
-
}
|
|
2432
|
-
.tool-approve-btn { color: var(--aparte-success, rgb(21,128,61)); border-color: var(--aparte-success-border, rgba(34,197,94,0.4)); }
|
|
2433
|
-
.tool-reject-btn { color: var(--aparte-error, rgb(185,28,28)); border-color: var(--aparte-error-border, rgba(239,68,68,0.4)); }
|
|
2434
|
-
.tool-approve-btn:hover { background: var(--aparte-success-surface, rgba(34,197,94,0.1)); }
|
|
2435
|
-
.tool-reject-btn:hover { background: var(--aparte-error-surface, rgba(239,68,68,0.1)); }
|
|
2436
|
-
.tool-pill-spinner {
|
|
2437
|
-
width: 10px; height: 10px;
|
|
2438
|
-
border: 1.5px solid currentColor;
|
|
2439
|
-
border-top-color: transparent;
|
|
2440
|
-
border-radius: 50%;
|
|
2441
|
-
display: inline-block;
|
|
2442
|
-
animation: tool-spin 0.7s linear infinite;
|
|
2443
|
-
}
|
|
2444
|
-
.tool-pill-status { font-size: 0.75rem; }
|
|
2445
|
-
@keyframes tool-spin { to { transform: rotate(360deg); } }
|
|
2446
|
-
`
|
|
2537
|
+
// CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
|
|
2538
|
+
// built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
|
|
2539
|
+
// has no other way onto the page.
|
|
2540
|
+
getStyles: () => ""
|
|
2447
2541
|
};
|
|
2448
2542
|
const waitingLabel = () => contextConfig().t("generating");
|
|
2449
2543
|
const pipelineWaitingRenderer = {
|
|
2450
2544
|
type: "pipeline-waiting",
|
|
2451
2545
|
render: (segment) => {
|
|
2452
2546
|
return `
|
|
2453
|
-
<div class="segment segment-pipeline-waiting" data-segment-id="${escapeHtml(segment.id)}" aria-label="${escapeAttr(waitingLabel())}" role="status">
|
|
2454
|
-
<span class="pw-dot"></span>
|
|
2455
|
-
<span class="pw-dot"></span>
|
|
2456
|
-
<span class="pw-dot"></span>
|
|
2547
|
+
<div class="aparte-segment aparte-segment-pipeline-waiting" data-segment-id="${escapeHtml(segment.id)}" aria-label="${escapeAttr(waitingLabel())}" role="status">
|
|
2548
|
+
<span class="aparte-pw-dot"></span>
|
|
2549
|
+
<span class="aparte-pw-dot"></span>
|
|
2550
|
+
<span class="aparte-pw-dot"></span>
|
|
2457
2551
|
</div>`;
|
|
2458
2552
|
},
|
|
2459
2553
|
update: () => {
|
|
@@ -2478,30 +2572,10 @@ const pipelineWaitingRenderer = {
|
|
|
2478
2572
|
});
|
|
2479
2573
|
observer.observe(parent, { childList: true });
|
|
2480
2574
|
},
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
gap: 5px;
|
|
2486
|
-
padding: 6px 2px;
|
|
2487
|
-
min-height: 28px;
|
|
2488
|
-
}
|
|
2489
|
-
.pw-dot {
|
|
2490
|
-
display: inline-block;
|
|
2491
|
-
width: 8px;
|
|
2492
|
-
height: 8px;
|
|
2493
|
-
border-radius: 50%;
|
|
2494
|
-
background: var(--aparte-text-muted, #6b7280);
|
|
2495
|
-
opacity: 0.3;
|
|
2496
|
-
animation: pw-pulse 1.2s ease-in-out infinite;
|
|
2497
|
-
}
|
|
2498
|
-
.pw-dot:nth-child(2) { animation-delay: 0.2s; }
|
|
2499
|
-
.pw-dot:nth-child(3) { animation-delay: 0.4s; }
|
|
2500
|
-
@keyframes pw-pulse {
|
|
2501
|
-
0%, 80%, 100% { opacity: 0.3; transform: scale(0.85); }
|
|
2502
|
-
40% { opacity: 1; transform: scale(1.1); }
|
|
2503
|
-
}
|
|
2504
|
-
`
|
|
2575
|
+
// CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
|
|
2576
|
+
// built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
|
|
2577
|
+
// has no other way onto the page.
|
|
2578
|
+
getStyles: () => ""
|
|
2505
2579
|
};
|
|
2506
2580
|
function stripCodeFences(content) {
|
|
2507
2581
|
let s = content;
|
|
@@ -2704,7 +2778,7 @@ function renderBinaryFileArtifact(segment, kind) {
|
|
|
2704
2778
|
if (cached && !isStreaming) {
|
|
2705
2779
|
const previewBody = cached.previewHtml ? contextConfig().sanitizeHtml(cached.previewHtml) : `<div class="aparte-art-file__preview-empty">Preview not available for ${escapeHtml(kind)} yet</div>`;
|
|
2706
2780
|
return `
|
|
2707
|
-
<div class="segment segment-artifact-file"
|
|
2781
|
+
<div class="aparte-segment aparte-segment-artifact-file"
|
|
2708
2782
|
data-segment-id="${escapeHtml(segment.id)}"
|
|
2709
2783
|
data-artifact-type="${escapeHtml(kind)}"
|
|
2710
2784
|
data-state="ready">
|
|
@@ -2730,7 +2804,7 @@ function renderBinaryFileArtifact(segment, kind) {
|
|
|
2730
2804
|
const cleanContent = stripCodeFences(segment.content || "");
|
|
2731
2805
|
const subText = isStreaming ? contextConfig().t("generating") : contextConfig().t("rebuildingPreview");
|
|
2732
2806
|
return `
|
|
2733
|
-
<div class="segment segment-artifact-file"
|
|
2807
|
+
<div class="aparte-segment aparte-segment-artifact-file"
|
|
2734
2808
|
data-segment-id="${escapeHtml(segment.id)}"
|
|
2735
2809
|
data-artifact-type="${escapeHtml(kind)}"
|
|
2736
2810
|
data-state="${isStreaming ? "streaming" : "compiling"}">
|
|
@@ -2925,7 +2999,7 @@ const artifactRenderer = {
|
|
|
2925
2999
|
const isBinary = BINARY_FILE_KINDS.has(kind);
|
|
2926
3000
|
const cleanContent = stripCodeFences(segment.content || "");
|
|
2927
3001
|
return `
|
|
2928
|
-
<div class="segment segment-artifact-card"
|
|
3002
|
+
<div class="aparte-segment aparte-segment-artifact-card"
|
|
2929
3003
|
data-segment-id="${escapeHtml(segment.id)}"
|
|
2930
3004
|
data-artifact-type="${escapeHtml(kind)}"
|
|
2931
3005
|
data-streaming="${isStreaming ? "true" : "false"}"
|
|
@@ -2953,7 +3027,7 @@ const artifactRenderer = {
|
|
|
2953
3027
|
</nav>
|
|
2954
3028
|
<div class="aparte-art-card__body">
|
|
2955
3029
|
<div class="aparte-art-card__pane" data-pane="code">
|
|
2956
|
-
<div class="code-content-wrapper">
|
|
3030
|
+
<div class="aparte-code-content-wrapper">
|
|
2957
3031
|
<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>
|
|
2958
3032
|
</div>
|
|
2959
3033
|
</div>
|
|
@@ -3004,7 +3078,7 @@ const artifactRenderer = {
|
|
|
3004
3078
|
setupBinaryFileArtifact(element, segment, kind);
|
|
3005
3079
|
return;
|
|
3006
3080
|
}
|
|
3007
|
-
const wrapper = element.querySelector(".code-content-wrapper");
|
|
3081
|
+
const wrapper = element.querySelector(".aparte-code-content-wrapper");
|
|
3008
3082
|
if (wrapper) {
|
|
3009
3083
|
const displayLang = languageForKind(kind);
|
|
3010
3084
|
const cleanContent = stripCodeFences(segment.content || "");
|
|
@@ -3079,13 +3153,13 @@ const artifactRenderer = {
|
|
|
3079
3153
|
const cleanContent = stripCodeFences(segment.content || "");
|
|
3080
3154
|
if (isStreaming) {
|
|
3081
3155
|
const segId = element.getAttribute("data-segment-id") ?? segment.id;
|
|
3082
|
-
streamHighlight(element, ".code-content-wrapper", cleanContent, languageForKind(kind), segId);
|
|
3156
|
+
streamHighlight(element, ".aparte-code-content-wrapper", cleanContent, languageForKind(kind), segId);
|
|
3083
3157
|
} else {
|
|
3084
|
-
const codeEl = element.querySelector(".code-content-wrapper code");
|
|
3158
|
+
const codeEl = element.querySelector(".aparte-code-content-wrapper code");
|
|
3085
3159
|
if (codeEl) {
|
|
3086
3160
|
codeEl.textContent = cleanContent;
|
|
3087
3161
|
} else {
|
|
3088
|
-
const wrapper = element.querySelector(".code-content-wrapper");
|
|
3162
|
+
const wrapper = element.querySelector(".aparte-code-content-wrapper");
|
|
3089
3163
|
if (wrapper) {
|
|
3090
3164
|
const displayLang = languageForKind(kind);
|
|
3091
3165
|
wrapper.innerHTML = `<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>`;
|
|
@@ -3095,7 +3169,7 @@ const artifactRenderer = {
|
|
|
3095
3169
|
if (wasStreaming && !isStreaming) {
|
|
3096
3170
|
element.setAttribute("data-streaming", "false");
|
|
3097
3171
|
element.querySelector(".aparte-art-card__pulse")?.remove();
|
|
3098
|
-
const wrapper = element.querySelector(".code-content-wrapper");
|
|
3172
|
+
const wrapper = element.querySelector(".aparte-code-content-wrapper");
|
|
3099
3173
|
if (wrapper) {
|
|
3100
3174
|
const displayLang = languageForKind(kind);
|
|
3101
3175
|
void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
|
|
@@ -3108,296 +3182,10 @@ const artifactRenderer = {
|
|
|
3108
3182
|
});
|
|
3109
3183
|
}
|
|
3110
3184
|
},
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
3116
|
-
border-radius: 12px;
|
|
3117
|
-
background: var(--aparte-surface, #fff);
|
|
3118
|
-
overflow: hidden;
|
|
3119
|
-
font: inherit;
|
|
3120
|
-
color: var(--aparte-text, inherit);
|
|
3121
|
-
}
|
|
3122
|
-
.aparte-art-card__header {
|
|
3123
|
-
display: flex; align-items: center; justify-content: space-between;
|
|
3124
|
-
padding: 8px 10px;
|
|
3125
|
-
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
3126
|
-
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
3127
|
-
min-height: 36px;
|
|
3128
|
-
}
|
|
3129
|
-
.aparte-art-card__title-block { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
3130
|
-
.aparte-art-card__kind {
|
|
3131
|
-
font-size: 0.7rem; font-weight: 600;
|
|
3132
|
-
text-transform: uppercase; letter-spacing: 0.04em;
|
|
3133
|
-
padding: 2px 6px; border-radius: 4px;
|
|
3134
|
-
background: var(--aparte-surface, #fff);
|
|
3135
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.1));
|
|
3136
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.6));
|
|
3137
|
-
}
|
|
3138
|
-
.aparte-art-card__title {
|
|
3139
|
-
font-size: 0.85rem; font-weight: 500;
|
|
3140
|
-
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
3141
|
-
}
|
|
3142
|
-
.aparte-art-card__pulse {
|
|
3143
|
-
width: 8px; height: 8px; border-radius: 50%;
|
|
3144
|
-
background: var(--aparte-accent, #3b82f6);
|
|
3145
|
-
animation: aparte-art-card-pulse 1.2s ease-in-out infinite;
|
|
3146
|
-
}
|
|
3147
|
-
@keyframes aparte-art-card-pulse {
|
|
3148
|
-
0%, 100% { opacity: 0.35; transform: scale(0.85); }
|
|
3149
|
-
50% { opacity: 1; transform: scale(1.1); }
|
|
3150
|
-
}
|
|
3151
|
-
.aparte-art-card__actions { display: flex; gap: 4px; }
|
|
3152
|
-
.aparte-art-card__btn {
|
|
3153
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
3154
|
-
width: 28px; height: 28px;
|
|
3155
|
-
background: transparent;
|
|
3156
|
-
border: 1px solid transparent;
|
|
3157
|
-
border-radius: 6px;
|
|
3158
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
3159
|
-
cursor: pointer;
|
|
3160
|
-
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
|
3161
|
-
}
|
|
3162
|
-
.aparte-art-card__btn:hover:not(:disabled) {
|
|
3163
|
-
background: var(--aparte-surface-hover, rgba(0,0,0,0.04));
|
|
3164
|
-
border-color: var(--aparte-border, rgba(0,0,0,0.1));
|
|
3165
|
-
color: var(--aparte-text, inherit);
|
|
3166
|
-
}
|
|
3167
|
-
.aparte-art-card__btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
3168
|
-
.aparte-art-card__tabs {
|
|
3169
|
-
/* justify-content and the padding are DECLARED, not left to a default.
|
|
3170
|
-
Core is light DOM on purpose - no shadow root, no ::part(), any selector
|
|
3171
|
-
reaches in - and the corollary is that a component must state what its
|
|
3172
|
-
layout depends on, because an undeclared property has nothing to override
|
|
3173
|
-
a host rule with. A consuming page with a bare nav selector setting
|
|
3174
|
-
justify-content: space-between and padding-top (this library's own docs
|
|
3175
|
-
site had exactly that) otherwise pushes these two tabs to opposite ends of
|
|
3176
|
-
the card and pads the row out. No backticks in here: this block is a JS
|
|
3177
|
-
template literal, and one of them ended it.
|
|
3178
|
-
|
|
3179
|
-
flex-end, and Code first in the DOM. The card OPENS on Code - mounting the
|
|
3180
|
-
preview would execute model-authored code with no gesture (ratified
|
|
3181
|
-
decision #8) - and a selected tab sitting second reads backwards. Right,
|
|
3182
|
-
because the header above puts the artifact's identity on the left and its
|
|
3183
|
-
copy/download buttons on the right, so this keeps every control in one
|
|
3184
|
-
column. DOM order is also keyboard order, so the tab a reader lands on
|
|
3185
|
-
first is the one already showing. */
|
|
3186
|
-
display: flex; justify-content: flex-end; align-items: stretch; gap: 2px;
|
|
3187
|
-
padding: 4px 8px 0;
|
|
3188
|
-
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
3189
|
-
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
3190
|
-
}
|
|
3191
|
-
.aparte-art-card__tabs button {
|
|
3192
|
-
padding: 6px 12px;
|
|
3193
|
-
font-size: 0.78rem;
|
|
3194
|
-
background: transparent;
|
|
3195
|
-
border: 1px solid transparent;
|
|
3196
|
-
border-bottom: none;
|
|
3197
|
-
border-radius: 6px 6px 0 0;
|
|
3198
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.6));
|
|
3199
|
-
cursor: pointer;
|
|
3200
|
-
transition: background 0.12s, color 0.12s;
|
|
3201
|
-
}
|
|
3202
|
-
.aparte-art-card__tabs button[aria-selected="true"] {
|
|
3203
|
-
background: var(--aparte-surface, #fff);
|
|
3204
|
-
border-color: var(--aparte-border, rgba(0,0,0,0.08));
|
|
3205
|
-
color: var(--aparte-text, inherit);
|
|
3206
|
-
font-weight: 500;
|
|
3207
|
-
}
|
|
3208
|
-
.aparte-art-card__tabs button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
3209
|
-
/* The card's heights, as variables with ONE owner each.
|
|
3210
|
-
They were four hardcoded numbers in the only part of this card that did not
|
|
3211
|
-
use a variable - everything else here already reads var(--aparte-code-bg) and
|
|
3212
|
-
friends - and two of them had to agree while a third contradicted a fourth:
|
|
3213
|
-
the code pane repeated the body's 600px, and the "press Preview" placeholder
|
|
3214
|
-
was 120px tall inside a body whose min-height said 80, so that minimum applied
|
|
3215
|
-
to nothing.
|
|
3216
|
-
The frame stays a FIXED height rather than an aspect ratio, which is what
|
|
3217
|
-
embeds of arbitrary HTML actually do - CodeSandbox documents 500px, StackBlitz
|
|
3218
|
-
takes a height parameter - because a frame with an opaque origin cannot be
|
|
3219
|
-
measured and a 16/10 ratio on a wide card is enormous. The vh cap is the part
|
|
3220
|
-
that was missing: a fixed 480px should not own a phone screen.
|
|
3221
|
-
Each default lives in its read, as var(--x, literal), the way every other
|
|
3222
|
-
value in this file already does - not in a declaration block on top of the
|
|
3223
|
-
fallbacks, which would be two owners of one number again. It also means the
|
|
3224
|
-
docs' CSS-variable generator finds them: its sweep looks for reads that
|
|
3225
|
-
carry a fallback, so a read without one is a public knob nobody documents. */
|
|
3226
|
-
.aparte-art-card__body {
|
|
3227
|
-
position: relative;
|
|
3228
|
-
/* The placeholder is the tallest thing this box can hold while empty, so it
|
|
3229
|
-
IS the minimum - one number instead of two that disagreed. */
|
|
3230
|
-
min-height: var(--aparte-artifact-pending-height, 120px);
|
|
3231
|
-
max-height: var(--aparte-artifact-body-max, 600px);
|
|
3232
|
-
overflow: hidden;
|
|
3233
|
-
}
|
|
3234
|
-
.aparte-art-card__pane { display: none; height: 100%; }
|
|
3235
|
-
.segment-artifact-card[data-tab="code"] .aparte-art-card__pane[data-pane="code"] { display: block; }
|
|
3236
|
-
.segment-artifact-card[data-tab="preview"] .aparte-art-card__pane[data-pane="preview"] { display: block; }
|
|
3237
|
-
.aparte-art-card__pane[data-pane="code"] {
|
|
3238
|
-
/* The body already caps this; repeating the number was the second owner. */
|
|
3239
|
-
max-height: var(--aparte-artifact-body-max, 600px); overflow: auto;
|
|
3240
|
-
}
|
|
3241
|
-
.aparte-art-card__pane[data-pane="code"] pre {
|
|
3242
|
-
margin: 0; padding: 12px;
|
|
3243
|
-
font-size: 0.82rem;
|
|
3244
|
-
background: var(--aparte-code-bg, #fafafa);
|
|
3245
|
-
}
|
|
3246
|
-
.aparte-art-card__frame {
|
|
3247
|
-
display: block;
|
|
3248
|
-
width: 100%;
|
|
3249
|
-
height: min(var(--aparte-artifact-frame-height, 480px), var(--aparte-artifact-frame-max, 70vh));
|
|
3250
|
-
border: 0;
|
|
3251
|
-
background: #fff;
|
|
3252
|
-
}
|
|
3253
|
-
.aparte-art-card__pending {
|
|
3254
|
-
display: flex; align-items: center; justify-content: center;
|
|
3255
|
-
height: var(--aparte-artifact-pending-height, 120px);
|
|
3256
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.5));
|
|
3257
|
-
font-size: 0.85rem;
|
|
3258
|
-
font-style: italic;
|
|
3259
|
-
}
|
|
3260
|
-
/* ── Binary file artifact (xlsx/pdf/docx) ──────────────────── */
|
|
3261
|
-
.segment-artifact-file {
|
|
3262
|
-
display: flex; flex-direction: column;
|
|
3263
|
-
margin: 8px 0;
|
|
3264
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
3265
|
-
border-radius: 12px;
|
|
3266
|
-
background: var(--aparte-surface, #fff);
|
|
3267
|
-
overflow: hidden;
|
|
3268
|
-
color: var(--aparte-text, inherit);
|
|
3269
|
-
font: inherit;
|
|
3270
|
-
}
|
|
3271
|
-
.aparte-art-file__card {
|
|
3272
|
-
display: flex; align-items: center; gap: 12px;
|
|
3273
|
-
padding: 12px 14px;
|
|
3274
|
-
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
3275
|
-
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
3276
|
-
}
|
|
3277
|
-
.aparte-art-file__body {
|
|
3278
|
-
position: relative;
|
|
3279
|
-
}
|
|
3280
|
-
.aparte-art-file__code-pane {
|
|
3281
|
-
max-height: var(--aparte-artifact-file-code-max, 360px); overflow: auto;
|
|
3282
|
-
background: var(--aparte-code-bg, #fafafa);
|
|
3283
|
-
}
|
|
3284
|
-
.aparte-art-file__code-pane pre {
|
|
3285
|
-
margin: 0; padding: 12px;
|
|
3286
|
-
font-size: 0.82rem;
|
|
3287
|
-
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
|
3288
|
-
}
|
|
3289
|
-
.aparte-art-file__preview-pane {
|
|
3290
|
-
max-height: var(--aparte-artifact-file-preview-max, 460px);
|
|
3291
|
-
overflow: auto;
|
|
3292
|
-
background: #fff;
|
|
3293
|
-
/* Preview is a document view — force light scheme regardless of
|
|
3294
|
-
the app theme, with a dark text colour so cells stay readable. */
|
|
3295
|
-
color: #1f2937;
|
|
3296
|
-
}
|
|
3297
|
-
.aparte-art-file__icon {
|
|
3298
|
-
width: 40px; height: 40px;
|
|
3299
|
-
border-radius: 8px;
|
|
3300
|
-
display: flex; align-items: center; justify-content: center;
|
|
3301
|
-
background: linear-gradient(135deg, #1d6f42, #0f5132);
|
|
3302
|
-
color: #fff;
|
|
3303
|
-
font-weight: 700;
|
|
3304
|
-
font-size: 0.78rem;
|
|
3305
|
-
letter-spacing: 0.04em;
|
|
3306
|
-
flex-shrink: 0;
|
|
3307
|
-
}
|
|
3308
|
-
.aparte-art-file__icon[data-kind="pdf"] { background: linear-gradient(135deg, #c0392b, #7d1f17); }
|
|
3309
|
-
.aparte-art-file__icon[data-kind="docx"] { background: linear-gradient(135deg, #1e5288, #0f3060); }
|
|
3310
|
-
.aparte-art-file__meta { flex: 1 1 auto; min-width: 0; }
|
|
3311
|
-
.aparte-art-file__meta-name {
|
|
3312
|
-
font-weight: 600; font-size: 0.92rem;
|
|
3313
|
-
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
3314
|
-
}
|
|
3315
|
-
.aparte-art-file__meta-sub {
|
|
3316
|
-
font-size: 0.78rem;
|
|
3317
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
3318
|
-
}
|
|
3319
|
-
.aparte-art-file__actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
3320
|
-
.aparte-art-file__btn {
|
|
3321
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
3322
|
-
background: var(--aparte-surface, #fff);
|
|
3323
|
-
color: var(--aparte-text, inherit);
|
|
3324
|
-
padding: 6px 10px;
|
|
3325
|
-
border-radius: 6px;
|
|
3326
|
-
font-size: 0.8rem;
|
|
3327
|
-
cursor: pointer;
|
|
3328
|
-
transition: background 0.12s ease;
|
|
3329
|
-
}
|
|
3330
|
-
.aparte-art-file__btn:hover { background: var(--aparte-surface-2, rgba(0,0,0,0.05)); }
|
|
3331
|
-
.aparte-art-file__btn--primary {
|
|
3332
|
-
background: var(--aparte-primary, #6366f1);
|
|
3333
|
-
color: #fff;
|
|
3334
|
-
border-color: var(--aparte-primary, #6366f1);
|
|
3335
|
-
}
|
|
3336
|
-
.aparte-art-file__btn--primary:hover { filter: brightness(1.07); }
|
|
3337
|
-
/* Hide the download button until the sandbox has produced the buffer.
|
|
3338
|
-
Avoids confusing the user with a disabled-but-styled-primary button
|
|
3339
|
-
during streaming / compiling. */
|
|
3340
|
-
.segment-artifact-file:not([data-state="ready"]) .aparte-art-file__btn[data-action="download"] {
|
|
3341
|
-
display: none;
|
|
3342
|
-
}
|
|
3343
|
-
.aparte-art-file__preview-pane table {
|
|
3344
|
-
border-collapse: collapse;
|
|
3345
|
-
width: 100%;
|
|
3346
|
-
font-size: 0.82rem;
|
|
3347
|
-
font-family: inherit;
|
|
3348
|
-
}
|
|
3349
|
-
.aparte-art-file__preview-pane th,
|
|
3350
|
-
.aparte-art-file__preview-pane td {
|
|
3351
|
-
border: 1px solid rgba(0,0,0,0.10);
|
|
3352
|
-
padding: 6px 10px;
|
|
3353
|
-
text-align: left;
|
|
3354
|
-
white-space: nowrap;
|
|
3355
|
-
color: #1f2937;
|
|
3356
|
-
background: #fff;
|
|
3357
|
-
}
|
|
3358
|
-
.aparte-art-file__preview-pane tr:nth-child(odd) td { background: #f9fafb; }
|
|
3359
|
-
.aparte-art-file__preview-pane tr:first-child td {
|
|
3360
|
-
background: #f3f4f6;
|
|
3361
|
-
font-weight: 600;
|
|
3362
|
-
position: sticky; top: 0;
|
|
3363
|
-
color: #111827;
|
|
3364
|
-
}
|
|
3365
|
-
.aparte-art-file__preview-empty {
|
|
3366
|
-
padding: 20px; text-align: center;
|
|
3367
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.5));
|
|
3368
|
-
font-size: 0.85rem; font-style: italic;
|
|
3369
|
-
}
|
|
3370
|
-
.segment-artifact-file[data-state="error"] .aparte-art-file__icon {
|
|
3371
|
-
background: linear-gradient(135deg, #c0392b, #7d1f17);
|
|
3372
|
-
}
|
|
3373
|
-
.aparte-art-file__error {
|
|
3374
|
-
padding: 16px 18px;
|
|
3375
|
-
background: rgba(239, 68, 68, 0.05);
|
|
3376
|
-
border-top: 1px solid rgba(239, 68, 68, 0.2);
|
|
3377
|
-
color: var(--aparte-text, inherit);
|
|
3378
|
-
}
|
|
3379
|
-
.aparte-art-file__error-title {
|
|
3380
|
-
font-weight: 600;
|
|
3381
|
-
font-size: 0.9rem;
|
|
3382
|
-
margin-bottom: 6px;
|
|
3383
|
-
color: #b91c1c;
|
|
3384
|
-
}
|
|
3385
|
-
.aparte-art-file__error-msg {
|
|
3386
|
-
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
|
3387
|
-
font-size: 0.78rem;
|
|
3388
|
-
padding: 6px 10px;
|
|
3389
|
-
background: rgba(0,0,0,0.04);
|
|
3390
|
-
border-radius: 4px;
|
|
3391
|
-
margin-bottom: 8px;
|
|
3392
|
-
color: #7f1d1d;
|
|
3393
|
-
word-break: break-word;
|
|
3394
|
-
}
|
|
3395
|
-
.aparte-art-file__error-hint {
|
|
3396
|
-
font-size: 0.78rem;
|
|
3397
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
3398
|
-
font-style: italic;
|
|
3399
|
-
}
|
|
3400
|
-
`
|
|
3185
|
+
// CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
|
|
3186
|
+
// built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
|
|
3187
|
+
// has no other way onto the page.
|
|
3188
|
+
getStyles: () => ""
|
|
3401
3189
|
};
|
|
3402
3190
|
function languageForKind(kind) {
|
|
3403
3191
|
if (kind === "react") return "jsx";
|
|
@@ -3548,7 +3336,6 @@ const DEFAULT_RENDERERS = [
|
|
|
3548
3336
|
thinkingRenderer,
|
|
3549
3337
|
codeRenderer,
|
|
3550
3338
|
errorRenderer,
|
|
3551
|
-
progressRenderer,
|
|
3552
3339
|
toolCallRenderer,
|
|
3553
3340
|
artifactRenderer,
|
|
3554
3341
|
pipelineWaitingRenderer
|
|
@@ -3575,7 +3362,8 @@ function adoptSegment(segments, segment, messageId) {
|
|
|
3575
3362
|
...segment,
|
|
3576
3363
|
messageId,
|
|
3577
3364
|
index: segments.length,
|
|
3578
|
-
isStreaming: false
|
|
3365
|
+
isStreaming: false,
|
|
3366
|
+
...segment.type === "tool_call" && segment.status === "awaiting-approval" ? { status: "aborted" } : {}
|
|
3579
3367
|
};
|
|
3580
3368
|
}
|
|
3581
3369
|
const RESERVED = /* @__PURE__ */ new Set(["id", "type", "messageId", "index"]);
|
|
@@ -5362,344 +5150,726 @@ function readableToAsyncIterable(stream, signal) {
|
|
|
5362
5150
|
}
|
|
5363
5151
|
};
|
|
5364
5152
|
}
|
|
5365
|
-
|
|
5366
|
-
function
|
|
5367
|
-
|
|
5368
|
-
const timedOut = new Promise((_resolve, reject) => {
|
|
5369
|
-
timer = setTimeout(() => {
|
|
5370
|
-
controller.abort();
|
|
5371
|
-
reject(Object.assign(new Error(`tool handler exceeded ${timeoutMs}ms`), { name: "AbortError" }));
|
|
5372
|
-
}, timeoutMs);
|
|
5373
|
-
});
|
|
5374
|
-
return {
|
|
5375
|
-
raced: Promise.race([work(controller.signal), timedOut]),
|
|
5376
|
-
done: () => clearTimeout(timer)
|
|
5377
|
-
};
|
|
5153
|
+
let _relabels = null;
|
|
5154
|
+
function onRelabel(fn) {
|
|
5155
|
+
_relabels?.push(fn);
|
|
5378
5156
|
}
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
* layout the JSDoc documents, a single compact event made BOTH clients run —
|
|
5393
|
-
* two paid summarisation calls against whichever chat the DOM scan found
|
|
5394
|
-
* first, and a global reset that wiped the other conversation.
|
|
5395
|
-
*
|
|
5396
|
-
* A scoped client also answers only events ADDRESSED to it. The old guard let
|
|
5397
|
-
* an untargeted event through to every scoped client, which turned one
|
|
5398
|
-
* broadcast into an action on every chat on the page.
|
|
5399
|
-
*/
|
|
5400
|
-
_isForThisInstance(e) {
|
|
5401
|
-
if (!e) return true;
|
|
5402
|
-
const detail = e.detail;
|
|
5403
|
-
const scope = this.options.scopeToTargetId;
|
|
5404
|
-
if (scope) return detail?.targetId === scope;
|
|
5405
|
-
if (this._config === aparteGlobalConfig) return true;
|
|
5406
|
-
const target = this._resolveTarget(detail?.targetId);
|
|
5407
|
-
if (!target) return true;
|
|
5408
|
-
const owner = resolveConfig(target);
|
|
5409
|
-
return owner === this._config || owner === aparteGlobalConfig;
|
|
5157
|
+
function el$1(tag, className, text) {
|
|
5158
|
+
const node = document.createElement(tag);
|
|
5159
|
+
if (className) node.className = className;
|
|
5160
|
+
if (text != null) node.textContent = text;
|
|
5161
|
+
return node;
|
|
5162
|
+
}
|
|
5163
|
+
function fieldHeader(parent, field, skipTitle) {
|
|
5164
|
+
let titleId;
|
|
5165
|
+
if (field.title && field.title.trim() !== skipTitle?.trim()) {
|
|
5166
|
+
const title = el$1("p", "aparte-elic-title", field.title);
|
|
5167
|
+
titleId = `elic-title-${uuid()}`;
|
|
5168
|
+
title.id = titleId;
|
|
5169
|
+
parent.appendChild(title);
|
|
5410
5170
|
}
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5171
|
+
if (field.description) parent.appendChild(el$1("p", "aparte-elic-desc", field.description));
|
|
5172
|
+
return titleId;
|
|
5173
|
+
}
|
|
5174
|
+
function labelGroup(list, opts) {
|
|
5175
|
+
list.setAttribute("role", opts.multiple ? "group" : "radiogroup");
|
|
5176
|
+
if (opts.titleId) list.setAttribute("aria-labelledby", opts.titleId);
|
|
5177
|
+
else if (opts.fallbackLabel) list.setAttribute("aria-label", opts.fallbackLabel);
|
|
5178
|
+
}
|
|
5179
|
+
function buildEnumField(field, onChange, fallbackLabel) {
|
|
5180
|
+
const wrap = el$1("div", "aparte-elic-field aparte-elic-enum");
|
|
5181
|
+
const titleId = fieldHeader(wrap, field, fallbackLabel);
|
|
5182
|
+
const list = el$1("div", "aparte-elic-options");
|
|
5183
|
+
labelGroup(list, { multiple: field.multiple, titleId, fallbackLabel });
|
|
5184
|
+
const name = `elic-${uuid()}`;
|
|
5185
|
+
const type = field.multiple ? "checkbox" : "radio";
|
|
5186
|
+
const allowOther = field.allowOther ?? contextConfig().getElicitationOptions().allowOther;
|
|
5187
|
+
const defaults = new Set(Array.isArray(field.default) ? field.default : field.default != null ? [field.default] : []);
|
|
5188
|
+
const buildOption = (value, label, description, recommended) => {
|
|
5189
|
+
const row = el$1("label", "aparte-elic-option" + (recommended ? " aparte-elic-option--recommended" : ""));
|
|
5190
|
+
const control = el$1("input", "aparte-elic-control");
|
|
5191
|
+
control.type = type;
|
|
5192
|
+
control.name = name;
|
|
5193
|
+
control.value = value;
|
|
5194
|
+
if (defaults.has(value)) control.checked = true;
|
|
5195
|
+
const body = el$1("span", "aparte-elic-option-body");
|
|
5196
|
+
body.appendChild(el$1("span", "aparte-elic-option-title", label));
|
|
5197
|
+
if (description) body.appendChild(el$1("span", "aparte-elic-option-desc", description));
|
|
5198
|
+
row.append(control, body);
|
|
5199
|
+
return row;
|
|
5200
|
+
};
|
|
5201
|
+
for (const opt of field.options) {
|
|
5202
|
+
list.appendChild(buildOption(opt.value, opt.label ?? opt.value, opt.description, opt.recommended));
|
|
5428
5203
|
}
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5204
|
+
let otherText = null;
|
|
5205
|
+
if (allowOther) {
|
|
5206
|
+
const row = el$1("label", "aparte-elic-option aparte-elic-option--other");
|
|
5207
|
+
const control = el$1("input", "aparte-elic-control");
|
|
5208
|
+
control.type = type;
|
|
5209
|
+
control.name = name;
|
|
5210
|
+
control.value = "__other__";
|
|
5211
|
+
const body = el$1("span", "aparte-elic-option-body");
|
|
5212
|
+
const t = contextConfig();
|
|
5213
|
+
const otherTitle = el$1("span", "aparte-elic-option-title", t.t("elicitationOther"));
|
|
5214
|
+
body.appendChild(otherTitle);
|
|
5215
|
+
otherText = el$1("input", "aparte-elic-other-input");
|
|
5216
|
+
otherText.type = "text";
|
|
5217
|
+
otherText.placeholder = t.t("elicitationOtherPlaceholder");
|
|
5218
|
+
otherText.style.display = "none";
|
|
5219
|
+
otherText.setAttribute("aria-label", t.t("elicitationOtherLabel"));
|
|
5220
|
+
const otherInput = otherText;
|
|
5221
|
+
onRelabel(() => {
|
|
5222
|
+
const now = contextConfig();
|
|
5223
|
+
otherTitle.textContent = now.t("elicitationOther");
|
|
5224
|
+
otherInput.placeholder = now.t("elicitationOtherPlaceholder");
|
|
5225
|
+
otherInput.setAttribute("aria-label", now.t("elicitationOtherLabel"));
|
|
5226
|
+
});
|
|
5227
|
+
body.appendChild(otherText);
|
|
5228
|
+
row.append(control, body);
|
|
5229
|
+
list.appendChild(row);
|
|
5446
5230
|
}
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
window.addEventListener("aparte-send", this._boundHandler);
|
|
5456
|
-
}
|
|
5457
|
-
if (!this._boundAbortHandler) {
|
|
5458
|
-
this._boundAbortHandler = (e) => {
|
|
5459
|
-
if (!this._isForThisInstance(e)) return;
|
|
5460
|
-
this.abort();
|
|
5461
|
-
};
|
|
5462
|
-
}
|
|
5463
|
-
window.addEventListener("aparte-abort", this._boundAbortHandler);
|
|
5464
|
-
if (!this._boundCompactHandler) {
|
|
5465
|
-
this._boundCompactHandler = (e) => {
|
|
5466
|
-
if (!this._isForThisInstance(e)) return;
|
|
5467
|
-
const detail = e.detail;
|
|
5468
|
-
void this.compact(detail?.targetId);
|
|
5469
|
-
};
|
|
5470
|
-
}
|
|
5471
|
-
window.addEventListener("aparte-compact", this._boundCompactHandler);
|
|
5472
|
-
if (!this._boundRetryHandler) {
|
|
5473
|
-
this._boundRetryHandler = (e) => {
|
|
5474
|
-
const evt = e;
|
|
5475
|
-
if (!this._isForThisInstance(evt)) return;
|
|
5476
|
-
void this._handleRetry(evt);
|
|
5477
|
-
};
|
|
5478
|
-
}
|
|
5479
|
-
window.addEventListener("aparte-retry", this._boundRetryHandler);
|
|
5480
|
-
if (!this._boundEditHandler) {
|
|
5481
|
-
this._boundEditHandler = (e) => {
|
|
5482
|
-
const evt = e;
|
|
5483
|
-
if (!this._isForThisInstance(evt)) return;
|
|
5484
|
-
void this._handleEdit(evt);
|
|
5485
|
-
};
|
|
5486
|
-
}
|
|
5487
|
-
window.addEventListener("aparte-edit", this._boundEditHandler);
|
|
5488
|
-
}
|
|
5489
|
-
/**
|
|
5490
|
-
* Stop listening.
|
|
5491
|
-
*/
|
|
5492
|
-
stop() {
|
|
5493
|
-
this.abort();
|
|
5494
|
-
if (!this._boundHandler) return;
|
|
5495
|
-
window.removeEventListener("aparte-send", this._boundHandler);
|
|
5496
|
-
this._boundHandler = null;
|
|
5497
|
-
if (this._boundAbortHandler) {
|
|
5498
|
-
window.removeEventListener("aparte-abort", this._boundAbortHandler);
|
|
5499
|
-
this._boundAbortHandler = null;
|
|
5500
|
-
}
|
|
5501
|
-
if (this._boundCompactHandler) {
|
|
5502
|
-
window.removeEventListener("aparte-compact", this._boundCompactHandler);
|
|
5503
|
-
this._boundCompactHandler = null;
|
|
5504
|
-
}
|
|
5505
|
-
if (this._boundRetryHandler) {
|
|
5506
|
-
window.removeEventListener("aparte-retry", this._boundRetryHandler);
|
|
5507
|
-
this._boundRetryHandler = null;
|
|
5231
|
+
wrap.appendChild(list);
|
|
5232
|
+
const controls = () => Array.from(list.querySelectorAll(".aparte-elic-control"));
|
|
5233
|
+
const other = () => list.querySelector('input[value="__other__"]');
|
|
5234
|
+
list.addEventListener("change", () => {
|
|
5235
|
+
const o = other();
|
|
5236
|
+
if (o && otherText) {
|
|
5237
|
+
otherText.style.display = o.checked ? "" : "none";
|
|
5238
|
+
if (o.checked) otherText.focus();
|
|
5508
5239
|
}
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5240
|
+
onChange();
|
|
5241
|
+
});
|
|
5242
|
+
otherText?.addEventListener("input", onChange);
|
|
5243
|
+
const getValue = () => {
|
|
5244
|
+
const otherVal = other()?.checked && otherText?.value.trim() ? otherText.value.trim() : "";
|
|
5245
|
+
if (field.multiple) {
|
|
5246
|
+
const vals = controls().filter((c) => c.checked && c.value !== "__other__").map((c) => c.value);
|
|
5247
|
+
if (otherVal) vals.push(otherVal);
|
|
5248
|
+
return vals;
|
|
5512
5249
|
}
|
|
5250
|
+
const checked = controls().find((c) => c.checked);
|
|
5251
|
+
if (!checked) return "";
|
|
5252
|
+
return checked.value === "__other__" ? otherVal : checked.value;
|
|
5253
|
+
};
|
|
5254
|
+
return {
|
|
5255
|
+
el: wrap,
|
|
5256
|
+
getValue,
|
|
5257
|
+
isComplete: () => {
|
|
5258
|
+
const v = getValue();
|
|
5259
|
+
return field.multiple ? v.length > 0 : v !== "";
|
|
5260
|
+
},
|
|
5261
|
+
focus: () => controls()[0]?.focus()
|
|
5262
|
+
};
|
|
5263
|
+
}
|
|
5264
|
+
function buildBooleanField(field, onChange, fallbackLabel) {
|
|
5265
|
+
const wrap = el$1("div", "aparte-elic-field aparte-elic-boolean");
|
|
5266
|
+
const titleId = fieldHeader(wrap, field, fallbackLabel);
|
|
5267
|
+
const list = el$1("div", "aparte-elic-options");
|
|
5268
|
+
labelGroup(list, { titleId, fallbackLabel });
|
|
5269
|
+
const name = `elic-${uuid()}`;
|
|
5270
|
+
const mk = (val, label, defaulted) => {
|
|
5271
|
+
const row = el$1("label", "aparte-elic-option");
|
|
5272
|
+
const control = el$1("input", "aparte-elic-control");
|
|
5273
|
+
control.type = "radio";
|
|
5274
|
+
control.name = name;
|
|
5275
|
+
control.value = val;
|
|
5276
|
+
if (field.default != null && String(field.default) === val) control.checked = true;
|
|
5277
|
+
const body = el$1("span", "aparte-elic-option-body");
|
|
5278
|
+
const title = el$1("span", "aparte-elic-option-title", label);
|
|
5279
|
+
if (defaulted) onRelabel(() => {
|
|
5280
|
+
title.textContent = contextConfig().t(defaulted);
|
|
5281
|
+
});
|
|
5282
|
+
body.appendChild(title);
|
|
5283
|
+
row.append(control, body);
|
|
5284
|
+
list.appendChild(row);
|
|
5285
|
+
};
|
|
5286
|
+
const t = contextConfig();
|
|
5287
|
+
mk("true", field.trueLabel ?? t.t("elicitationYes"), field.trueLabel ? void 0 : "elicitationYes");
|
|
5288
|
+
mk("false", field.falseLabel ?? t.t("elicitationNo"), field.falseLabel ? void 0 : "elicitationNo");
|
|
5289
|
+
wrap.appendChild(list);
|
|
5290
|
+
list.addEventListener("change", onChange);
|
|
5291
|
+
const checked = () => list.querySelector("input:checked");
|
|
5292
|
+
return {
|
|
5293
|
+
el: wrap,
|
|
5294
|
+
getValue: () => checked()?.value === "true",
|
|
5295
|
+
isComplete: () => checked() != null,
|
|
5296
|
+
focus: () => list.querySelector("input")?.focus()
|
|
5297
|
+
};
|
|
5298
|
+
}
|
|
5299
|
+
function buildStringField(field, onChange, fallbackLabel) {
|
|
5300
|
+
const wrap = el$1("div", "aparte-elic-field aparte-elic-string");
|
|
5301
|
+
fieldHeader(wrap, field, fallbackLabel);
|
|
5302
|
+
const input = field.multiline ? el$1("textarea", "aparte-elic-text") : el$1("input", "aparte-elic-text");
|
|
5303
|
+
if (!field.multiline) input.type = "text";
|
|
5304
|
+
if (field.placeholder) input.setAttribute("placeholder", field.placeholder);
|
|
5305
|
+
if (field.default) input.value = field.default;
|
|
5306
|
+
if (field.maxLength != null) input.setAttribute("maxlength", String(field.maxLength));
|
|
5307
|
+
const ownName = field.title || field.description || fallbackLabel || void 0;
|
|
5308
|
+
input.setAttribute("aria-label", ownName ?? contextConfig().t("elicitationAnswerLabel"));
|
|
5309
|
+
if (ownName == null) {
|
|
5310
|
+
const named = input;
|
|
5311
|
+
onRelabel(() => {
|
|
5312
|
+
named.setAttribute("aria-label", contextConfig().t("elicitationAnswerLabel"));
|
|
5313
|
+
});
|
|
5513
5314
|
}
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5315
|
+
wrap.appendChild(input);
|
|
5316
|
+
input.addEventListener("input", onChange);
|
|
5317
|
+
const required = field.required ?? true;
|
|
5318
|
+
const value = () => input.value;
|
|
5319
|
+
return {
|
|
5320
|
+
el: wrap,
|
|
5321
|
+
getValue: () => value(),
|
|
5322
|
+
isComplete: () => {
|
|
5323
|
+
const v = value().trim();
|
|
5324
|
+
if (!required && v === "") return true;
|
|
5325
|
+
return v.length >= (field.minLength ?? 1);
|
|
5326
|
+
},
|
|
5327
|
+
focus: () => input.focus()
|
|
5328
|
+
};
|
|
5329
|
+
}
|
|
5330
|
+
function buildField(field, onChange, fallbackLabel, key) {
|
|
5331
|
+
const custom = contextConfig().getElicitationFieldRenderer()?.(field, { key, notifyChange: onChange });
|
|
5332
|
+
if (custom) {
|
|
5333
|
+
return {
|
|
5334
|
+
el: custom.el,
|
|
5335
|
+
getValue: () => custom.getValue(),
|
|
5336
|
+
isComplete: () => custom.isComplete(),
|
|
5337
|
+
focus: () => custom.focus?.()
|
|
5338
|
+
};
|
|
5525
5339
|
}
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
* `_activeToolControllers`) lets `abort()` cleanly resolve a pending
|
|
5534
|
-
* approval to `{ approved: false }` — there is no timeout, since a human may
|
|
5535
|
-
* take any amount of time to decide.
|
|
5536
|
-
*/
|
|
5537
|
-
/**
|
|
5538
|
-
* Await the human's Approve/Reject for one tool call.
|
|
5539
|
-
*
|
|
5540
|
-
* `target` is not decoration: it is the scope of the consent. The listener sits
|
|
5541
|
-
* on `document` and used to accept any `aparte-tool-decision` whose
|
|
5542
|
-
* `detail.toolCallId` matched — and that id is the tool-call id the MODEL chose.
|
|
5543
|
-
* The built-in buttons dispatch with `bubbles: true, composed: true`, so on a
|
|
5544
|
-
* page with two chats a click aimed at one tool could satisfy the gate awaiting
|
|
5545
|
-
* a different tool in a different conversation. The consented action and the
|
|
5546
|
-
* executed action came apart, which is the entire failure mode an approval gate
|
|
5547
|
-
* exists to prevent — and the handler behind it is arbitrary consumer code.
|
|
5548
|
-
*
|
|
5549
|
-
* The check is DOM CONTAINMENT rather than a `targetId` string comparison. A
|
|
5550
|
-
* model can choose an id; it cannot choose where in the tree the click happened.
|
|
5551
|
-
* It also needs no change on the dispatch side, so a consumer's own Approve
|
|
5552
|
-
* button keeps working as long as it fires from inside its own chat.
|
|
5553
|
-
*
|
|
5554
|
-
* The request half of this handshake was hardened with `targetId` for exactly
|
|
5555
|
-
* this hazard. This is its sibling, and it was missed.
|
|
5556
|
-
*/
|
|
5557
|
-
_awaitToolDecision(toolCallId, signal, target) {
|
|
5558
|
-
return new Promise((resolve) => {
|
|
5559
|
-
if (signal.aborted) {
|
|
5560
|
-
resolve({ approved: false });
|
|
5561
|
-
return;
|
|
5562
|
-
}
|
|
5563
|
-
const cleanup = () => {
|
|
5564
|
-
document.removeEventListener("aparte-tool-decision", onDecision);
|
|
5565
|
-
signal.removeEventListener("abort", onAbort);
|
|
5566
|
-
};
|
|
5567
|
-
const onDecision = (e) => {
|
|
5568
|
-
const detail = e.detail;
|
|
5569
|
-
if (detail?.toolCallId !== toolCallId) return;
|
|
5570
|
-
if (target && e.target instanceof Node && e.target !== document && !target.contains(e.target)) return;
|
|
5571
|
-
cleanup();
|
|
5572
|
-
resolve({ approved: detail?.approved === true, payload: detail?.payload });
|
|
5573
|
-
};
|
|
5574
|
-
const onAbort = () => {
|
|
5575
|
-
cleanup();
|
|
5576
|
-
resolve({ approved: false });
|
|
5577
|
-
};
|
|
5578
|
-
document.addEventListener("aparte-tool-decision", onDecision);
|
|
5579
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
5580
|
-
});
|
|
5340
|
+
switch (field.type) {
|
|
5341
|
+
case "enum":
|
|
5342
|
+
return buildEnumField(field, onChange, fallbackLabel);
|
|
5343
|
+
case "boolean":
|
|
5344
|
+
return buildBooleanField(field, onChange, fallbackLabel);
|
|
5345
|
+
case "string":
|
|
5346
|
+
return buildStringField(field, onChange, fallbackLabel);
|
|
5581
5347
|
}
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
}
|
|
5592
|
-
|
|
5593
|
-
|
|
5348
|
+
}
|
|
5349
|
+
function buildElicitationPanel(message, schema, onChange) {
|
|
5350
|
+
const relabels = [];
|
|
5351
|
+
const outer = _relabels;
|
|
5352
|
+
_relabels = relabels;
|
|
5353
|
+
try {
|
|
5354
|
+
const built = buildPanel(message, schema, onChange);
|
|
5355
|
+
return { ...built, relabel: () => {
|
|
5356
|
+
for (const fn of relabels) fn();
|
|
5357
|
+
} };
|
|
5358
|
+
} finally {
|
|
5359
|
+
_relabels = outer;
|
|
5594
5360
|
}
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
stream: true,
|
|
5612
|
-
tools: registeredTools.length ? registeredTools : void 0,
|
|
5613
|
-
...opts?.temperature !== void 0 ? { temperature: opts.temperature } : {}
|
|
5361
|
+
}
|
|
5362
|
+
function buildPanel(message, schema, onChange) {
|
|
5363
|
+
const panel = el$1("div", "aparte-elic-panel");
|
|
5364
|
+
const asked = typeof message === "function" ? message() : message;
|
|
5365
|
+
if (asked) panel.appendChild(el$1("p", "aparte-elic-message", asked));
|
|
5366
|
+
const body = el$1("div", "aparte-elic-body");
|
|
5367
|
+
panel.appendChild(body);
|
|
5368
|
+
const dismiss = el$1("div", "aparte-elic-dismiss");
|
|
5369
|
+
panel.appendChild(dismiss);
|
|
5370
|
+
if (schema.type === "object") {
|
|
5371
|
+
let show = function(index) {
|
|
5372
|
+
current = index;
|
|
5373
|
+
fields.forEach((f, i) => {
|
|
5374
|
+
f.field.el.hidden = i !== index;
|
|
5375
|
+
});
|
|
5376
|
+
syncNav();
|
|
5614
5377
|
};
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
if (this._isAborted) {
|
|
5629
|
-
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
5630
|
-
} else {
|
|
5631
|
-
dispatchLifecycleEvent(targetElement, "aparte-message-done", { messageId, role: "assistant", usage });
|
|
5632
|
-
}
|
|
5633
|
-
} catch (error) {
|
|
5634
|
-
if (this._isAborted) {
|
|
5635
|
-
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
5636
|
-
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
5637
|
-
return;
|
|
5378
|
+
const entries = Object.entries(schema.properties);
|
|
5379
|
+
const requiredKeys = new Set(schema.required ?? entries.map(([k]) => k));
|
|
5380
|
+
const cfg = contextConfig();
|
|
5381
|
+
let syncNav = () => {
|
|
5382
|
+
};
|
|
5383
|
+
const notify = () => {
|
|
5384
|
+
syncNav();
|
|
5385
|
+
onChange();
|
|
5386
|
+
};
|
|
5387
|
+
const fields = entries.map(([key, field2]) => {
|
|
5388
|
+
const built = buildField(field2, notify, void 0, key);
|
|
5389
|
+
if (!field2.title && !built.el.querySelector(".aparte-elic-title")) {
|
|
5390
|
+
built.el.insertBefore(el$1("p", "aparte-elic-title", key), built.el.firstChild);
|
|
5638
5391
|
}
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
}
|
|
5392
|
+
body.appendChild(built.el);
|
|
5393
|
+
return { key, field: built, required: requiredKeys.has(key), header: field2.header };
|
|
5394
|
+
});
|
|
5395
|
+
const isComplete = () => fields.every((f) => !f.required || f.field.isComplete());
|
|
5396
|
+
const api = {
|
|
5397
|
+
el: panel,
|
|
5398
|
+
dismiss,
|
|
5399
|
+
getContent: () => Object.fromEntries(fields.map((f) => [f.key, f.field.getValue()])),
|
|
5400
|
+
isComplete,
|
|
5401
|
+
focus: () => fields[0]?.field.focus(),
|
|
5402
|
+
// A form of one behaves like a single field; only a real form steps.
|
|
5403
|
+
mode: () => "submit",
|
|
5404
|
+
canProceed: isComplete,
|
|
5405
|
+
proceed: () => {
|
|
5406
|
+
}
|
|
5407
|
+
};
|
|
5408
|
+
if (fields.length < 2 || cfg.getElicitationOptions().layout !== "stepped") return api;
|
|
5409
|
+
panel.classList.add("aparte-elic-panel--stepped");
|
|
5410
|
+
let current = 0;
|
|
5411
|
+
const steps = el$1("div", "aparte-elic-steps");
|
|
5412
|
+
steps.setAttribute("role", "tablist");
|
|
5413
|
+
const chips = fields.map((f, i) => {
|
|
5414
|
+
const chip = el$1("button", "aparte-elic-step");
|
|
5415
|
+
chip.type = "button";
|
|
5416
|
+
chip.setAttribute("role", "tab");
|
|
5417
|
+
chip.textContent = f.header?.trim() || String(i + 1);
|
|
5418
|
+
chip.addEventListener("click", () => {
|
|
5419
|
+
show(i);
|
|
5420
|
+
f.field.focus();
|
|
5421
|
+
});
|
|
5422
|
+
steps.appendChild(chip);
|
|
5423
|
+
return chip;
|
|
5424
|
+
});
|
|
5425
|
+
panel.insertBefore(steps, body);
|
|
5426
|
+
syncNav = () => {
|
|
5427
|
+
chips.forEach((chip, i) => {
|
|
5428
|
+
chip.setAttribute("aria-selected", String(i === current));
|
|
5429
|
+
chip.toggleAttribute("data-answered", fields[i].field.isComplete());
|
|
5430
|
+
});
|
|
5431
|
+
};
|
|
5432
|
+
show(0);
|
|
5433
|
+
const last = () => current === fields.length - 1;
|
|
5434
|
+
return {
|
|
5435
|
+
...api,
|
|
5436
|
+
focus: () => fields[current]?.field.focus(),
|
|
5437
|
+
mode: () => last() ? "submit" : "advance",
|
|
5438
|
+
// Advancing needs only THIS question answered; submitting needs them all,
|
|
5439
|
+
// which is what stops the last question from accepting a form with a hole
|
|
5440
|
+
// in it.
|
|
5441
|
+
canProceed: () => last() ? isComplete() : !!fields[current]?.field.isComplete(),
|
|
5442
|
+
proceed: () => {
|
|
5443
|
+
if (last()) return;
|
|
5444
|
+
show(current + 1);
|
|
5445
|
+
fields[current]?.field.focus();
|
|
5446
|
+
}
|
|
5447
|
+
};
|
|
5642
5448
|
}
|
|
5449
|
+
const field = buildField(schema, onChange, asked);
|
|
5450
|
+
body.appendChild(field.el);
|
|
5451
|
+
return {
|
|
5452
|
+
el: panel,
|
|
5453
|
+
dismiss,
|
|
5454
|
+
getContent: () => field.getValue(),
|
|
5455
|
+
isComplete: () => field.isComplete(),
|
|
5456
|
+
focus: () => field.focus(),
|
|
5457
|
+
mode: () => "submit",
|
|
5458
|
+
canProceed: () => field.isComplete(),
|
|
5459
|
+
proceed: () => {
|
|
5460
|
+
}
|
|
5461
|
+
};
|
|
5462
|
+
}
|
|
5463
|
+
function el(tag, className, text) {
|
|
5464
|
+
const node = document.createElement(tag);
|
|
5465
|
+
if (className) node.className = className;
|
|
5466
|
+
if (text != null) node.textContent = text;
|
|
5467
|
+
return node;
|
|
5468
|
+
}
|
|
5469
|
+
function buildApprovalPanel(message, options, onChange) {
|
|
5470
|
+
const cfg = contextConfig();
|
|
5471
|
+
const panel = el("div", "aparte-elic-panel aparte-approval-panel");
|
|
5472
|
+
const askText = () => typeof message === "function" ? message() : message;
|
|
5473
|
+
const ask = askText() ? el("p", "aparte-elic-message", askText()) : null;
|
|
5474
|
+
if (ask) panel.appendChild(ask);
|
|
5475
|
+
const dismiss = el("div", "aparte-elic-dismiss");
|
|
5476
|
+
panel.appendChild(dismiss);
|
|
5477
|
+
let settle = null;
|
|
5478
|
+
let done = false;
|
|
5479
|
+
const fire = (answer) => {
|
|
5480
|
+
if (done) return;
|
|
5481
|
+
done = true;
|
|
5482
|
+
settle?.(answer);
|
|
5483
|
+
};
|
|
5484
|
+
const row = el("div", "aparte-approval-options");
|
|
5485
|
+
row.setAttribute("role", "group");
|
|
5486
|
+
row.setAttribute("aria-label", cfg.t("approvalOptionsLabel"));
|
|
5487
|
+
const labelled = [];
|
|
5488
|
+
const textOf = (option) => typeof option.label === "function" ? option.label() : option.label;
|
|
5489
|
+
for (const option of options) {
|
|
5490
|
+
const button = el("button", `aparte-approval-option aparte-approval-option--${option.tone ?? "affirm"}`);
|
|
5491
|
+
button.type = "button";
|
|
5492
|
+
button.textContent = textOf(option);
|
|
5493
|
+
button.addEventListener("click", () => fire({ option: option.value }));
|
|
5494
|
+
row.appendChild(button);
|
|
5495
|
+
labelled.push({ button, option });
|
|
5496
|
+
}
|
|
5497
|
+
panel.appendChild(row);
|
|
5498
|
+
const instruction = el("textarea", "aparte-approval-instruction");
|
|
5499
|
+
instruction.rows = 1;
|
|
5500
|
+
instruction.placeholder = cfg.t("approvalInstructionPlaceholder");
|
|
5501
|
+
instruction.setAttribute("aria-label", cfg.t("approvalInstructionPlaceholder"));
|
|
5502
|
+
instruction.addEventListener("input", onChange);
|
|
5503
|
+
panel.appendChild(instruction);
|
|
5504
|
+
const typed = () => instruction.value.trim();
|
|
5505
|
+
return {
|
|
5506
|
+
el: panel,
|
|
5507
|
+
dismiss,
|
|
5508
|
+
onSettle: (cb) => {
|
|
5509
|
+
settle = cb;
|
|
5510
|
+
},
|
|
5511
|
+
getContent: () => ({ instruction: typed() }),
|
|
5512
|
+
isComplete: () => typed().length > 0,
|
|
5513
|
+
focus: () => {
|
|
5514
|
+
panel.querySelector(".aparte-approval-option")?.focus();
|
|
5515
|
+
},
|
|
5516
|
+
relabel: () => {
|
|
5517
|
+
const now = contextConfig();
|
|
5518
|
+
const label = now.t("approvalInstructionPlaceholder");
|
|
5519
|
+
instruction.placeholder = label;
|
|
5520
|
+
instruction.setAttribute("aria-label", label);
|
|
5521
|
+
row.setAttribute("aria-label", now.t("approvalOptionsLabel"));
|
|
5522
|
+
if (ask) ask.textContent = askText();
|
|
5523
|
+
for (const { button, option } of labelled) button.textContent = textOf(option);
|
|
5524
|
+
}
|
|
5525
|
+
};
|
|
5526
|
+
}
|
|
5527
|
+
function requestUserInput(request) {
|
|
5528
|
+
return resolveConfig(request.target ?? null).requestUserInput(request);
|
|
5529
|
+
}
|
|
5530
|
+
const DEFAULT_TOOL_HANDLER_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
5531
|
+
function withToolTimeout(work, controller, timeoutMs) {
|
|
5532
|
+
let timer;
|
|
5533
|
+
const timedOut = new Promise((_resolve, reject) => {
|
|
5534
|
+
timer = setTimeout(() => {
|
|
5535
|
+
controller.abort();
|
|
5536
|
+
reject(Object.assign(new Error(`tool handler exceeded ${timeoutMs}ms`), { name: "AbortError" }));
|
|
5537
|
+
}, timeoutMs);
|
|
5538
|
+
});
|
|
5539
|
+
return {
|
|
5540
|
+
raced: Promise.race([work(controller.signal), timedOut]),
|
|
5541
|
+
done: () => clearTimeout(timer)
|
|
5542
|
+
};
|
|
5543
|
+
}
|
|
5544
|
+
class AparteClient {
|
|
5545
|
+
_boundHandler = null;
|
|
5546
|
+
_boundAbortHandler = null;
|
|
5547
|
+
_boundCompactHandler = null;
|
|
5548
|
+
_boundRetryHandler = null;
|
|
5549
|
+
_boundEditHandler = null;
|
|
5550
|
+
_activeToolControllers = /* @__PURE__ */ new Set();
|
|
5551
|
+
_isAborted = false;
|
|
5643
5552
|
/**
|
|
5644
|
-
*
|
|
5645
|
-
* clear the viewport, then inject the summary as a single context message.
|
|
5553
|
+
* Does this window event belong to this client?
|
|
5646
5554
|
*
|
|
5647
|
-
*
|
|
5648
|
-
*
|
|
5555
|
+
* One rule for all five handlers, because it used to be four near-copies and
|
|
5556
|
+
* one omission: `aparte-compact` had no guard at all, so in the two-client
|
|
5557
|
+
* layout the JSDoc documents, a single compact event made BOTH clients run —
|
|
5558
|
+
* two paid summarisation calls against whichever chat the DOM scan found
|
|
5559
|
+
* first, and a global reset that wiped the other conversation.
|
|
5560
|
+
*
|
|
5561
|
+
* A scoped client also answers only events ADDRESSED to it. The old guard let
|
|
5562
|
+
* an untargeted event through to every scoped client, which turned one
|
|
5563
|
+
* broadcast into an action on every chat on the page.
|
|
5649
5564
|
*/
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5565
|
+
_isForThisInstance(e) {
|
|
5566
|
+
if (!e) return true;
|
|
5567
|
+
const detail = e.detail;
|
|
5568
|
+
const scope = this.options.scopeToTargetId;
|
|
5569
|
+
if (scope) return detail?.targetId === scope;
|
|
5570
|
+
if (this._config === aparteGlobalConfig) return true;
|
|
5571
|
+
const target = this._resolveTarget(detail?.targetId);
|
|
5572
|
+
if (!target) return true;
|
|
5573
|
+
const owner = resolveConfig(target);
|
|
5574
|
+
return owner === this._config || owner === aparteGlobalConfig;
|
|
5575
|
+
}
|
|
5576
|
+
/** Aborts the in-flight vendor/transport fetch when the user stops a stream. */
|
|
5577
|
+
_streamController = null;
|
|
5578
|
+
options;
|
|
5579
|
+
/** Config read by this client — an instance config, or the global default. */
|
|
5580
|
+
_config;
|
|
5581
|
+
constructor(options = {}) {
|
|
5582
|
+
this.options = {
|
|
5583
|
+
autoRegister: true,
|
|
5584
|
+
...options
|
|
5585
|
+
};
|
|
5586
|
+
this._config = options.config ?? aparteGlobalConfig;
|
|
5587
|
+
if (this.options.autoRegister) {
|
|
5588
|
+
registerDefaultRenderers(this._config);
|
|
5589
|
+
} else {
|
|
5590
|
+
declineDefaultRenderers(this._config);
|
|
5661
5591
|
}
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5592
|
+
this._setupListeners();
|
|
5593
|
+
}
|
|
5594
|
+
/**
|
|
5595
|
+
* Sets up the event listeners.
|
|
5596
|
+
* This is called once in the constructor.
|
|
5597
|
+
*/
|
|
5598
|
+
_setupListeners() {
|
|
5599
|
+
if (this._boundHandler) return;
|
|
5600
|
+
this._boundHandler = (e) => {
|
|
5601
|
+
const event = e;
|
|
5602
|
+
if (event.type !== "aparte-send") return;
|
|
5603
|
+
if (!this._isForThisInstance(event)) return;
|
|
5604
|
+
this._isAborted = false;
|
|
5605
|
+
for (const controller of this._activeToolControllers) {
|
|
5606
|
+
controller.abort();
|
|
5607
|
+
}
|
|
5608
|
+
this._activeToolControllers.clear();
|
|
5609
|
+
void this._handleSend(event);
|
|
5610
|
+
};
|
|
5611
|
+
}
|
|
5612
|
+
/**
|
|
5613
|
+
* Start listening for aparte-send events on the window.
|
|
5614
|
+
*/
|
|
5615
|
+
start() {
|
|
5616
|
+
if (!this._boundHandler) {
|
|
5617
|
+
this._setupListeners();
|
|
5667
5618
|
}
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
if (!providerId) {
|
|
5671
|
-
window.dispatchEvent(new CustomEvent("aparte-compact-error", { detail: { error: "No provider configured" } }));
|
|
5672
|
-
return;
|
|
5619
|
+
if (this._boundHandler) {
|
|
5620
|
+
window.addEventListener("aparte-send", this._boundHandler);
|
|
5673
5621
|
}
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5622
|
+
if (!this._boundAbortHandler) {
|
|
5623
|
+
this._boundAbortHandler = (e) => {
|
|
5624
|
+
if (!this._isForThisInstance(e)) return;
|
|
5625
|
+
this.abort();
|
|
5626
|
+
};
|
|
5678
5627
|
}
|
|
5679
|
-
window.
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
})).filter((m) => contentToText(m.content).length > 0);
|
|
5686
|
-
const summarizeRequest = {
|
|
5687
|
-
messages: [
|
|
5688
|
-
{
|
|
5689
|
-
role: "system",
|
|
5690
|
-
content: "You are a conversation summarizer. Create a concise but complete summary of the conversation below. Capture: key topics, conclusions, decisions, ongoing tasks, and any context needed to continue the conversation. Write in third person. Be factual and brief. No preamble, just the summary."
|
|
5691
|
-
},
|
|
5692
|
-
...historyMessages,
|
|
5693
|
-
{
|
|
5694
|
-
role: "user",
|
|
5695
|
-
content: "Please summarize this conversation."
|
|
5696
|
-
}
|
|
5697
|
-
],
|
|
5698
|
-
modelId: config.defaultModel || "",
|
|
5699
|
-
stream: false
|
|
5628
|
+
window.addEventListener("aparte-abort", this._boundAbortHandler);
|
|
5629
|
+
if (!this._boundCompactHandler) {
|
|
5630
|
+
this._boundCompactHandler = (e) => {
|
|
5631
|
+
if (!this._isForThisInstance(e)) return;
|
|
5632
|
+
const detail = e.detail;
|
|
5633
|
+
void this.compact(detail?.targetId);
|
|
5700
5634
|
};
|
|
5701
|
-
|
|
5702
|
-
|
|
5635
|
+
}
|
|
5636
|
+
window.addEventListener("aparte-compact", this._boundCompactHandler);
|
|
5637
|
+
if (!this._boundRetryHandler) {
|
|
5638
|
+
this._boundRetryHandler = (e) => {
|
|
5639
|
+
const evt = e;
|
|
5640
|
+
if (!this._isForThisInstance(evt)) return;
|
|
5641
|
+
void this._handleRetry(evt);
|
|
5642
|
+
};
|
|
5643
|
+
}
|
|
5644
|
+
window.addEventListener("aparte-retry", this._boundRetryHandler);
|
|
5645
|
+
if (!this._boundEditHandler) {
|
|
5646
|
+
this._boundEditHandler = (e) => {
|
|
5647
|
+
const evt = e;
|
|
5648
|
+
if (!this._isForThisInstance(evt)) return;
|
|
5649
|
+
void this._handleEdit(evt);
|
|
5650
|
+
};
|
|
5651
|
+
}
|
|
5652
|
+
window.addEventListener("aparte-edit", this._boundEditHandler);
|
|
5653
|
+
}
|
|
5654
|
+
/**
|
|
5655
|
+
* Stop listening.
|
|
5656
|
+
*/
|
|
5657
|
+
stop() {
|
|
5658
|
+
this.abort();
|
|
5659
|
+
if (!this._boundHandler) return;
|
|
5660
|
+
window.removeEventListener("aparte-send", this._boundHandler);
|
|
5661
|
+
this._boundHandler = null;
|
|
5662
|
+
if (this._boundAbortHandler) {
|
|
5663
|
+
window.removeEventListener("aparte-abort", this._boundAbortHandler);
|
|
5664
|
+
this._boundAbortHandler = null;
|
|
5665
|
+
}
|
|
5666
|
+
if (this._boundCompactHandler) {
|
|
5667
|
+
window.removeEventListener("aparte-compact", this._boundCompactHandler);
|
|
5668
|
+
this._boundCompactHandler = null;
|
|
5669
|
+
}
|
|
5670
|
+
if (this._boundRetryHandler) {
|
|
5671
|
+
window.removeEventListener("aparte-retry", this._boundRetryHandler);
|
|
5672
|
+
this._boundRetryHandler = null;
|
|
5673
|
+
}
|
|
5674
|
+
if (this._boundEditHandler) {
|
|
5675
|
+
window.removeEventListener("aparte-edit", this._boundEditHandler);
|
|
5676
|
+
this._boundEditHandler = null;
|
|
5677
|
+
}
|
|
5678
|
+
}
|
|
5679
|
+
/**
|
|
5680
|
+
* Abort the current streaming response and all active tool calls.
|
|
5681
|
+
* Dispatches `aparte-message-aborted` on the target element.
|
|
5682
|
+
*/
|
|
5683
|
+
abort() {
|
|
5684
|
+
this._isAborted = true;
|
|
5685
|
+
this._streamController?.abort();
|
|
5686
|
+
for (const controller of this._activeToolControllers) {
|
|
5687
|
+
controller.abort();
|
|
5688
|
+
}
|
|
5689
|
+
this._activeToolControllers.clear();
|
|
5690
|
+
}
|
|
5691
|
+
/*
|
|
5692
|
+
* Two docblocks stood here, both describing `_awaitToolDecision` — the method
|
|
5693
|
+
* this replaced. Deleting a method and leaving its documentation is worse than
|
|
5694
|
+
* leaving the method: only the LAST comment before a declaration is its JSDoc,
|
|
5695
|
+
* so those two were floating prose, and one of them explained a DOM-containment
|
|
5696
|
+
* check as a live security property. It was the fix for a cross-chat hazard that
|
|
5697
|
+
* only existed because the decision travelled as a bubbling `document` event.
|
|
5698
|
+
* Routing through `requestUserInput` removed the hazard rather than guarding it,
|
|
5699
|
+
* so the guard is gone too — and a reader who found that comment would have
|
|
5700
|
+
* believed a containment check was still protecting them.
|
|
5701
|
+
*/
|
|
5702
|
+
/**
|
|
5703
|
+
* Ask the human at the COMPOSER, and report what they decided.
|
|
5704
|
+
*
|
|
5705
|
+
* The default channel, replacing a `document` listener for `aparte-tool-decision`
|
|
5706
|
+
* that existed only because the buttons lived in a segment renderer with no
|
|
5707
|
+
* reference to this client. Routing through `requestUserInput` puts the decision
|
|
5708
|
+
* where every other request for the user already goes, which is the whole point:
|
|
5709
|
+
* one queue, one panel slot, one teardown, and the same behaviour whether the
|
|
5710
|
+
* request came from a tool handler or from this gate.
|
|
5711
|
+
*
|
|
5712
|
+
* The tool NAME is the question; the arguments stay in the transcript, on the pill
|
|
5713
|
+
* this request is anchored to. A panel capped at 50vh cannot hold a diff or a plan,
|
|
5714
|
+
* and the transcript is already scrollable, copyable and persisted.
|
|
5715
|
+
*
|
|
5716
|
+
* `aparte-tool-decision` is GONE with the buttons that dispatched it, and so is the
|
|
5717
|
+
* `document` listener that answered it. It existed only because a segment renderer
|
|
5718
|
+
* has no reference to this client; two seams that can disagree about who answers a
|
|
5719
|
+
* decision are one seam too many, and the ratified elicitation rule already said so
|
|
5720
|
+
* — "a typed presenter registered per config instance, never window events". A host
|
|
5721
|
+
* that wants to answer programmatically registers an `approvalResolver` (headless,
|
|
5722
|
+
* no DOM) or its own presenter, both strictly more capable than an id on an event.
|
|
5723
|
+
*/
|
|
5724
|
+
async _askForApproval(event, signal, target) {
|
|
5725
|
+
const answer = await requestUserInput({
|
|
5726
|
+
kind: "approval",
|
|
5727
|
+
/*
|
|
5728
|
+
* A function, like the option labels below, and for the same reason: the
|
|
5729
|
+
* FRAME is locale text and follows a language switch while the question is
|
|
5730
|
+
* open. The tool's NAME is substituted into it and never translated — it is
|
|
5731
|
+
* wire format, the identifier the model called.
|
|
5732
|
+
*/
|
|
5733
|
+
message: () => {
|
|
5734
|
+
const loc = this._config.getLocale();
|
|
5735
|
+
return (loc.approvalAsk ?? "Run {tool}?").replace("{tool}", event.name);
|
|
5736
|
+
},
|
|
5737
|
+
// Functions, not strings, same as the question: these follow a language
|
|
5738
|
+
// switch while the request is open, which a resolved string cannot.
|
|
5739
|
+
options: [
|
|
5740
|
+
{ value: "allow", label: () => this._config.getLocale().approveTool ?? "Approve", tone: "affirm" },
|
|
5741
|
+
{ value: "deny", label: () => this._config.getLocale().rejectTool ?? "Reject", tone: "deny" }
|
|
5742
|
+
],
|
|
5743
|
+
signal,
|
|
5744
|
+
target
|
|
5745
|
+
});
|
|
5746
|
+
if (answer.action !== "accept") return { approved: false };
|
|
5747
|
+
const content = answer.content;
|
|
5748
|
+
const instruction = content?.instruction?.trim();
|
|
5749
|
+
if (instruction) return { approved: false, instruction };
|
|
5750
|
+
return { approved: content?.option === "allow" };
|
|
5751
|
+
}
|
|
5752
|
+
/**
|
|
5753
|
+
* Resolve the auth for a provider: `options.keyResolver` takes precedence,
|
|
5754
|
+
* then the aparteGlobalConfig key channel (`setKeyProvider`) so a key registered
|
|
5755
|
+
* there reaches the request. One key source on the happy path.
|
|
5756
|
+
*/
|
|
5757
|
+
async _resolveAuth(providerId) {
|
|
5758
|
+
if (this.options.keyResolver) {
|
|
5759
|
+
const resolved = await this.options.keyResolver(providerId);
|
|
5760
|
+
if (resolved) return resolved;
|
|
5761
|
+
}
|
|
5762
|
+
const key = await this._config.getKey(providerId);
|
|
5763
|
+
return key || void 0;
|
|
5764
|
+
}
|
|
5765
|
+
/**
|
|
5766
|
+
* The shared tail of send / retry / edit: attach the current tools, run the
|
|
5767
|
+
* request interceptor, honour `toolChoice: 'none'`, reset the abort flag,
|
|
5768
|
+
* dispatch `aparte-message-start`, run the agentic `_streamLoop`, then dispatch
|
|
5769
|
+
* `aparte-message-done` or route the error to the lifecycle handler.
|
|
5770
|
+
*
|
|
5771
|
+
* Callers own only their turn-specific prep (target resolution, history
|
|
5772
|
+
* building, appending the assistant placeholder) and hand the fully-built
|
|
5773
|
+
* `messages` here — one place for the provider→interceptor→toolChoice→stream
|
|
5774
|
+
* sequence, so it can't drift between the three entry points.
|
|
5775
|
+
*/
|
|
5776
|
+
async _streamTurn(targetElement, messageId, provider, messages, modelId, authConfig, opts) {
|
|
5777
|
+
const registeredTools = this._toolsForCurrentModel();
|
|
5778
|
+
let baseRequest = {
|
|
5779
|
+
messages,
|
|
5780
|
+
modelId,
|
|
5781
|
+
stream: true,
|
|
5782
|
+
tools: registeredTools.length ? registeredTools : void 0,
|
|
5783
|
+
...opts?.temperature !== void 0 ? { temperature: opts.temperature } : {}
|
|
5784
|
+
};
|
|
5785
|
+
if (this.options.requestInterceptor) {
|
|
5786
|
+
baseRequest = await this.options.requestInterceptor(baseRequest);
|
|
5787
|
+
}
|
|
5788
|
+
if (baseRequest.toolChoice === "none") {
|
|
5789
|
+
baseRequest = { ...baseRequest, tools: void 0 };
|
|
5790
|
+
}
|
|
5791
|
+
if (this._isAborted) {
|
|
5792
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
5793
|
+
return;
|
|
5794
|
+
}
|
|
5795
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-start", { messageId, role: "assistant" });
|
|
5796
|
+
try {
|
|
5797
|
+
const usage = await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig);
|
|
5798
|
+
if (this._isAborted) {
|
|
5799
|
+
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
5800
|
+
} else {
|
|
5801
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-done", { messageId, role: "assistant", usage });
|
|
5802
|
+
}
|
|
5803
|
+
} catch (error) {
|
|
5804
|
+
if (this._isAborted) {
|
|
5805
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
5806
|
+
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
5807
|
+
return;
|
|
5808
|
+
}
|
|
5809
|
+
const aparteError = AparteError.from(error, AparteErrorCode.UNKNOWN_ERROR);
|
|
5810
|
+
this._handleLifecycleError(targetElement, messageId, aparteError);
|
|
5811
|
+
}
|
|
5812
|
+
}
|
|
5813
|
+
/**
|
|
5814
|
+
* Compact the current conversation: summarize all messages via the AI,
|
|
5815
|
+
* clear the viewport, then inject the summary as a single context message.
|
|
5816
|
+
*
|
|
5817
|
+
* Triggered programmatically or by dispatching `window.dispatchEvent(new CustomEvent('aparte-compact'))`.
|
|
5818
|
+
* Dispatches `aparte-compact-done` on window when complete, or `aparte-compact-error` on failure.
|
|
5819
|
+
*/
|
|
5820
|
+
async compact(targetId) {
|
|
5821
|
+
let target = this._resolveTarget(targetId);
|
|
5822
|
+
if (target && typeof target.getMessages !== "function") target = null;
|
|
5823
|
+
if (!target) {
|
|
5824
|
+
window.dispatchEvent(new CustomEvent("aparte-compact-error", { detail: { error: "No aparte-chat target found" } }));
|
|
5825
|
+
return;
|
|
5826
|
+
}
|
|
5827
|
+
const messages = target.getMessages?.() ?? [];
|
|
5828
|
+
if (messages.length === 0) {
|
|
5829
|
+
window.dispatchEvent(new CustomEvent("aparte-compact-done", { detail: { skipped: true } }));
|
|
5830
|
+
return;
|
|
5831
|
+
}
|
|
5832
|
+
const selector = this.options.compactionSelector ?? ((m) => ({ keep: [], drop: m }));
|
|
5833
|
+
const { keep, drop } = selector(messages);
|
|
5834
|
+
if (drop.length === 0) {
|
|
5835
|
+
window.dispatchEvent(new CustomEvent("aparte-compact-done", { detail: { skipped: true } }));
|
|
5836
|
+
return;
|
|
5837
|
+
}
|
|
5838
|
+
const config = this._config.getModelConfig();
|
|
5839
|
+
const providerId = config.defaultProvider;
|
|
5840
|
+
if (!providerId) {
|
|
5841
|
+
window.dispatchEvent(new CustomEvent("aparte-compact-error", { detail: { error: "No provider configured" } }));
|
|
5842
|
+
return;
|
|
5843
|
+
}
|
|
5844
|
+
const provider = this._config.getAIProvider(providerId);
|
|
5845
|
+
if (!provider) {
|
|
5846
|
+
window.dispatchEvent(new CustomEvent("aparte-compact-error", { detail: { error: `Provider '${providerId}' not found` } }));
|
|
5847
|
+
return;
|
|
5848
|
+
}
|
|
5849
|
+
window.dispatchEvent(new CustomEvent("aparte-compact-start"));
|
|
5850
|
+
try {
|
|
5851
|
+
const authConfig = await this._resolveAuth(providerId);
|
|
5852
|
+
const historyMessages = drop.filter((m) => m.role === "user" || m.role === "assistant" && m.status === "completed").map((m) => ({
|
|
5853
|
+
role: m.role,
|
|
5854
|
+
content: this._extractText(m)
|
|
5855
|
+
})).filter((m) => contentToText(m.content).length > 0);
|
|
5856
|
+
const summarizeRequest = {
|
|
5857
|
+
messages: [
|
|
5858
|
+
{
|
|
5859
|
+
role: "system",
|
|
5860
|
+
content: "You are a conversation summarizer. Create a concise but complete summary of the conversation below. Capture: key topics, conclusions, decisions, ongoing tasks, and any context needed to continue the conversation. Write in third person. Be factual and brief. No preamble, just the summary."
|
|
5861
|
+
},
|
|
5862
|
+
...historyMessages,
|
|
5863
|
+
{
|
|
5864
|
+
role: "user",
|
|
5865
|
+
content: "Please summarize this conversation."
|
|
5866
|
+
}
|
|
5867
|
+
],
|
|
5868
|
+
modelId: config.defaultModel || "",
|
|
5869
|
+
stream: false
|
|
5870
|
+
};
|
|
5871
|
+
const compactController = new AbortController();
|
|
5872
|
+
this._streamController = compactController;
|
|
5703
5873
|
const response = await this._config.getTransport().chat(
|
|
5704
5874
|
provider,
|
|
5705
5875
|
summarizeRequest,
|
|
@@ -6236,16 +6406,27 @@ ${content}
|
|
|
6236
6406
|
this._activeToolControllers.delete(controller);
|
|
6237
6407
|
}
|
|
6238
6408
|
}
|
|
6409
|
+
/**
|
|
6410
|
+
* What the loop does after one tool call. THREE outcomes, because a boolean
|
|
6411
|
+
* conflated two questions that a refusal answers differently.
|
|
6412
|
+
*
|
|
6413
|
+
* - `'continue'` — run the next tool call of this turn.
|
|
6414
|
+
* - `'respond'` — skip this turn's remaining calls, then take another turn so the
|
|
6415
|
+
* model reads what happened. A human refusal: the model asked for
|
|
6416
|
+
* several calls and refusing one cannot license the others, but a
|
|
6417
|
+
* refusal it never reads is one the user has to retype as a message.
|
|
6418
|
+
* - `'halt'` — the run is over: an abort, a turn limit, a missing handler.
|
|
6419
|
+
*/
|
|
6239
6420
|
/**
|
|
6240
6421
|
* Handle one `tool_use` stream event from {@link _streamLoop}: the built-in
|
|
6241
6422
|
* `create_artifact`, per-tool renderer selection, the human-in-the-loop
|
|
6242
6423
|
* approval gate, and running the registered handler (timeout / abort).
|
|
6243
6424
|
* Mutates the shared `messages` / `toolCallsThisTurn` history in place and
|
|
6244
|
-
* returns
|
|
6425
|
+
* returns what the loop should do next.
|
|
6245
6426
|
*/
|
|
6246
6427
|
async _handleToolUseEvent(event, ctx) {
|
|
6247
6428
|
const { targetElement, messageId, messages, toolCallsThisTurn, precedingText, artifactProgress, turns, globalMaxTurns } = ctx;
|
|
6248
|
-
let
|
|
6429
|
+
let flow = "continue";
|
|
6249
6430
|
toolCallsThisTurn.push({ id: event.id, name: event.name, input: event.input });
|
|
6250
6431
|
if (event.name === "create_artifact") {
|
|
6251
6432
|
const input = event.input;
|
|
@@ -6271,7 +6452,7 @@ ${content}
|
|
|
6271
6452
|
content: "Artifact created successfully.",
|
|
6272
6453
|
toolCallId: event.id
|
|
6273
6454
|
});
|
|
6274
|
-
return {
|
|
6455
|
+
return { flow: "continue" };
|
|
6275
6456
|
}
|
|
6276
6457
|
const toolSeg = {
|
|
6277
6458
|
id: `tool-${event.id}`,
|
|
@@ -6305,7 +6486,7 @@ ${content}
|
|
|
6305
6486
|
if (turns >= effectiveMaxTurns) {
|
|
6306
6487
|
console.warn(`[AparteClient] Tool "${event.name}" maxTurns (${effectiveMaxTurns}) reached.`);
|
|
6307
6488
|
targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
|
|
6308
|
-
return {
|
|
6489
|
+
return { flow: "halt" };
|
|
6309
6490
|
}
|
|
6310
6491
|
const handler = this._config.getToolHandler(event.name);
|
|
6311
6492
|
if (handler) {
|
|
@@ -6320,14 +6501,24 @@ ${content}
|
|
|
6320
6501
|
input: event.input
|
|
6321
6502
|
});
|
|
6322
6503
|
let decision;
|
|
6323
|
-
const resolveApproval = this.options.approvalResolver ?? ((
|
|
6504
|
+
const resolveApproval = this.options.approvalResolver ?? ((call, sig) => this._askForApproval(call, sig, targetElement));
|
|
6324
6505
|
try {
|
|
6325
|
-
decision = await resolveApproval(event
|
|
6506
|
+
decision = await resolveApproval(event, approvalController.signal);
|
|
6507
|
+
} catch (err) {
|
|
6508
|
+
if (err?.name === "AbortError") {
|
|
6509
|
+
targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
|
|
6510
|
+
return { flow: "halt" };
|
|
6511
|
+
}
|
|
6512
|
+
throw err;
|
|
6326
6513
|
} finally {
|
|
6327
6514
|
this._activeToolControllers.delete(approvalController);
|
|
6328
6515
|
}
|
|
6516
|
+
if (approvalController.signal.aborted) {
|
|
6517
|
+
targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
|
|
6518
|
+
return { flow: "halt" };
|
|
6519
|
+
}
|
|
6329
6520
|
if (!decision.approved) {
|
|
6330
|
-
const rejection = "Tool execution was rejected by the user.";
|
|
6521
|
+
const rejection = decision.instruction ? `The user rejected this tool call and said: ${decision.instruction}` : "Tool execution was rejected by the user.";
|
|
6331
6522
|
targetElement.updateSegment?.(toolSeg.id, { status: "rejected", result: rejection });
|
|
6332
6523
|
const existingToolCallMsg = messages.find(
|
|
6333
6524
|
(m) => m.role === "tool_call" && m.toolCalls?.some((tc) => tc.id === event.id)
|
|
@@ -6341,7 +6532,7 @@ ${content}
|
|
|
6341
6532
|
});
|
|
6342
6533
|
}
|
|
6343
6534
|
messages.push({ role: "tool_result", content: rejection, toolCallId: event.id });
|
|
6344
|
-
return {
|
|
6535
|
+
return { flow: "respond" };
|
|
6345
6536
|
}
|
|
6346
6537
|
if (decision.payload && typeof decision.payload === "object" && !Array.isArray(decision.payload)) {
|
|
6347
6538
|
effectiveInput = { ...event.input, ...decision.payload };
|
|
@@ -6381,7 +6572,7 @@ ${content}
|
|
|
6381
6572
|
} catch (err) {
|
|
6382
6573
|
if (err?.name === "AbortError") {
|
|
6383
6574
|
targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
|
|
6384
|
-
|
|
6575
|
+
flow = "halt";
|
|
6385
6576
|
} else {
|
|
6386
6577
|
throw err;
|
|
6387
6578
|
}
|
|
@@ -6392,9 +6583,9 @@ ${content}
|
|
|
6392
6583
|
} else {
|
|
6393
6584
|
console.warn(`[AparteClient] No handler registered for tool "${event.name}"`);
|
|
6394
6585
|
targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
|
|
6395
|
-
|
|
6586
|
+
flow = "halt";
|
|
6396
6587
|
}
|
|
6397
|
-
return {
|
|
6588
|
+
return { flow };
|
|
6398
6589
|
}
|
|
6399
6590
|
/**
|
|
6400
6591
|
* Stream loop: runs one provider.chat() call and repeats if a tool was called.
|
|
@@ -6419,6 +6610,7 @@ ${content}
|
|
|
6419
6610
|
targetElement.addSegment?.(seg);
|
|
6420
6611
|
}
|
|
6421
6612
|
while (continueLoop) {
|
|
6613
|
+
let endTurnEarly = false;
|
|
6422
6614
|
if (this._isAborted) {
|
|
6423
6615
|
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
6424
6616
|
break;
|
|
@@ -6624,7 +6816,8 @@ ${content}
|
|
|
6624
6816
|
{ id: event.id, name: event.name, input: event.input },
|
|
6625
6817
|
{ targetElement, messageId, messages, toolCallsThisTurn, precedingText, artifactProgress, turns, globalMaxTurns }
|
|
6626
6818
|
);
|
|
6627
|
-
if (
|
|
6819
|
+
if (toolResult.flow !== "continue") endTurnEarly = true;
|
|
6820
|
+
if (toolResult.flow === "halt") continueLoop = false;
|
|
6628
6821
|
break;
|
|
6629
6822
|
}
|
|
6630
6823
|
case "error":
|
|
@@ -6639,7 +6832,7 @@ ${content}
|
|
|
6639
6832
|
break;
|
|
6640
6833
|
}
|
|
6641
6834
|
}
|
|
6642
|
-
if (
|
|
6835
|
+
if (endTurnEarly) break;
|
|
6643
6836
|
}
|
|
6644
6837
|
finalizeXmlArtifact(xmlCtx, { targetElement, messageId, artifactProgress, artifactXmlHint, textParser, streamingSegmentIds });
|
|
6645
6838
|
const finals = textParser.finalize();
|
|
@@ -6681,769 +6874,480 @@ ${content}
|
|
|
6681
6874
|
targetElement.addSegment?.(s);
|
|
6682
6875
|
} else if ("content" in s) {
|
|
6683
6876
|
targetElement.updateSegment?.(s.id, segmentContentUpdate(s));
|
|
6684
|
-
}
|
|
6685
|
-
if (s.type === "artifact") {
|
|
6686
|
-
dispatchArtifactLifecycle(targetElement, messageId, s, artifactProgress, true);
|
|
6687
|
-
}
|
|
6688
|
-
}
|
|
6689
|
-
if (toolCallsThisTurn.length === 0) {
|
|
6690
|
-
if (pipeline && pipelineIndex < pipeline.length - 1) {
|
|
6691
|
-
if (precedingText.trim()) {
|
|
6692
|
-
messages.push({ role: "assistant", content: precedingText.trim() });
|
|
6693
|
-
}
|
|
6694
|
-
pipelineIndex++;
|
|
6695
|
-
const pwId = `pw-${uuid()}`;
|
|
6696
|
-
targetElement.addSegment?.({ id: pwId, type: "pipeline-waiting" });
|
|
6697
|
-
} else {
|
|
6698
|
-
continueLoop = false;
|
|
6699
|
-
}
|
|
6700
|
-
}
|
|
6701
|
-
} finally {
|
|
6702
|
-
if (!streamDrained) {
|
|
6703
|
-
try {
|
|
6704
|
-
await reader.cancel();
|
|
6705
|
-
} catch {
|
|
6706
|
-
}
|
|
6707
|
-
}
|
|
6708
|
-
reader.releaseLock();
|
|
6709
|
-
}
|
|
6710
|
-
}
|
|
6711
|
-
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
6712
|
-
if (lastUsage) {
|
|
6713
|
-
try {
|
|
6714
|
-
targetElement.setUsage?.(messageId, lastUsage);
|
|
6715
|
-
} catch {
|
|
6716
|
-
}
|
|
6717
|
-
}
|
|
6718
|
-
return lastUsage;
|
|
6719
|
-
}
|
|
6720
|
-
/**
|
|
6721
|
-
* Delegate `_streamLoop`'s agentic loop to an injected {@link AparteStreamRunner},
|
|
6722
|
-
* rendering its DOM-free events through {@link createStreamAdapter}.
|
|
6723
|
-
* Builds the runner's dependencies from this client's config / provider /
|
|
6724
|
-
* transport; the adapter reproduces the inline loop's `targetElement.*` calls
|
|
6725
|
-
* (validated by the engine `stream-parity` suite). Leading writes (status
|
|
6726
|
-
* streaming, prefix segments) mirror the inline path; the runner's `run-start`
|
|
6727
|
-
* re-asserts `streaming` idempotently.
|
|
6728
|
-
*/
|
|
6729
|
-
async _runViaStreamRunner(streamRunner, targetElement, messageId, provider, baseRequest, authConfig, streamController) {
|
|
6730
|
-
const signal = streamController.signal;
|
|
6731
|
-
this._updateMessage(targetElement, messageId, { status: "streaming" });
|
|
6732
|
-
const prefixSegments = baseRequest._meta?.["prefixSegments"];
|
|
6733
|
-
if (prefixSegments?.length) {
|
|
6734
|
-
for (const seg of prefixSegments) targetElement.addSegment?.(seg);
|
|
6735
|
-
}
|
|
6736
|
-
const artifactHint = baseRequest._meta?.artifactHint;
|
|
6737
|
-
const emitter = createStreamAdapter({
|
|
6738
|
-
target: targetElement,
|
|
6739
|
-
config: this._config,
|
|
6740
|
-
messageId,
|
|
6741
|
-
artifactHint
|
|
6742
|
-
});
|
|
6743
|
-
const transportCall = async (request) => {
|
|
6744
|
-
const response = await this._config.getTransport().chat(provider, request, authConfig, { providerId: provider.id, signal });
|
|
6745
|
-
return typeof response === "string" ? response : readableToAsyncIterable(response, signal);
|
|
6746
|
-
};
|
|
6747
|
-
const toolLookup = (name) => {
|
|
6748
|
-
const handler = this._config.getToolHandler(name);
|
|
6749
|
-
if (!handler) return void 0;
|
|
6750
|
-
const context = { target: targetElement, config: this._config };
|
|
6751
|
-
return (call, sig) => handler(call, sig, context);
|
|
6752
|
-
};
|
|
6753
|
-
const toolConfigLookup = (name) => {
|
|
6754
|
-
const tool = this._config.getTools().find((t) => t.name === name);
|
|
6755
|
-
return tool ? { maxTurns: tool.maxTurns, needsApproval: tool.needsApproval } : void 0;
|
|
6756
|
-
};
|
|
6757
|
-
const approvalResolver = this.options.approvalResolver ?? ((id, sig) => this._awaitToolDecision(id, sig, targetElement));
|
|
6758
|
-
const usage = await streamRunner({
|
|
6759
|
-
messageId,
|
|
6760
|
-
baseRequest,
|
|
6761
|
-
transportCall,
|
|
6762
|
-
toolLookup,
|
|
6763
|
-
toolConfigLookup,
|
|
6764
|
-
approvalResolver,
|
|
6765
|
-
emitter,
|
|
6766
|
-
signal,
|
|
6767
|
-
maxTurns: this.options.maxTurns,
|
|
6768
|
-
// Forwarded, because the option's own JSDoc promises it means one thing
|
|
6769
|
-
// whichever loop runs — and the fix that introduced it only wired the
|
|
6770
|
-
// inline path, so setting it was silently ignored here. Exactly the
|
|
6771
|
-
// asymmetry the option was added to remove.
|
|
6772
|
-
toolTimeoutMs: this.options.toolTimeoutMs,
|
|
6773
|
-
// Match the inline loop's id conventions: prefixed artifact ids, but a
|
|
6774
|
-
// BARE uuid for the synthetic tool (the adapter renders `tool-<id>`).
|
|
6775
|
-
idGen: (prefix) => prefix === "synthetic-tool" ? uuid() : `${prefix}-${uuid()}`
|
|
6776
|
-
});
|
|
6777
|
-
return usage ?? void 0;
|
|
6778
|
-
}
|
|
6779
|
-
/**
|
|
6780
|
-
* Updates an existing message or appends a new error if ID lookup fails
|
|
6781
|
-
*/
|
|
6782
|
-
_updateMessage(target, messageId, updates) {
|
|
6783
|
-
if (target.updateMessage) {
|
|
6784
|
-
target.updateMessage(messageId, updates);
|
|
6785
|
-
}
|
|
6786
|
-
}
|
|
6787
|
-
/**
|
|
6788
|
-
* Gracefully handles errors by updating the current message instead of duplicating it
|
|
6789
|
-
*/
|
|
6790
|
-
_handleLifecycleError(target, messageId, error) {
|
|
6791
|
-
const errorSegment = {
|
|
6792
|
-
id: `error-${uuid()}`,
|
|
6793
|
-
type: "error",
|
|
6794
|
-
content: error.message,
|
|
6795
|
-
details: error.code
|
|
6796
|
-
};
|
|
6797
|
-
if (target.updateMessage) {
|
|
6798
|
-
const rendered = target.getMessages?.()?.find((m) => m.id === messageId)?.segments ?? [];
|
|
6799
|
-
target.updateMessage(messageId, {
|
|
6800
|
-
status: "error",
|
|
6801
|
-
segments: [...rendered, errorSegment]
|
|
6802
|
-
});
|
|
6803
|
-
} else if (target.appendMessage) {
|
|
6804
|
-
target.appendMessage({
|
|
6805
|
-
id: messageId,
|
|
6806
|
-
role: "assistant",
|
|
6807
|
-
status: "error",
|
|
6808
|
-
timestamp: Date.now(),
|
|
6809
|
-
segments: [errorSegment]
|
|
6810
|
-
});
|
|
6811
|
-
}
|
|
6812
|
-
dispatchLifecycleEvent(target, "aparte-message-error", { messageId, error });
|
|
6813
|
-
}
|
|
6814
|
-
/** Warn once per client for an unrecognised stream event, then carry on. */
|
|
6815
|
-
_warnedUnknownEvents = /* @__PURE__ */ new Set();
|
|
6816
|
-
_warnUnknownStreamEvent(event) {
|
|
6817
|
-
const type = String(event?.type ?? "undefined");
|
|
6818
|
-
if (this._warnedUnknownEvents.has(type)) return;
|
|
6819
|
-
this._warnedUnknownEvents.add(type);
|
|
6820
|
-
console.warn(
|
|
6821
|
-
`[AparteClient] Ignoring unrecognised stream event "${type}". The reply is unaffected; this usually means the provider or its SDK emits a part this version of aparté does not map yet.`
|
|
6822
|
-
);
|
|
6823
|
-
}
|
|
6824
|
-
}
|
|
6825
|
-
function findHead(node) {
|
|
6826
|
-
if (node.next) return findHead(node.next);
|
|
6827
|
-
if ("current" in node) return node;
|
|
6828
|
-
return null;
|
|
6829
|
-
}
|
|
6830
|
-
class AparteMessageRepository {
|
|
6831
|
-
_nodes = /* @__PURE__ */ new Map();
|
|
6832
|
-
_head = null;
|
|
6833
|
-
_root = { next: null, children: [] };
|
|
6834
|
-
// ─── Public read API ────────────────────────────────────────────────────
|
|
6835
|
-
/** ID of the currently active leaf message */
|
|
6836
|
-
get headId() {
|
|
6837
|
-
return this._head?.current.id ?? null;
|
|
6838
|
-
}
|
|
6839
|
-
/**
|
|
6840
|
-
* Returns the messages on the currently active path, root → head.
|
|
6841
|
-
* O(depth).
|
|
6842
|
-
*/
|
|
6843
|
-
getMessages() {
|
|
6844
|
-
const depth = this._head?.level ?? -1;
|
|
6845
|
-
if (depth < 0) return [];
|
|
6846
|
-
const messages = new Array(depth + 1);
|
|
6847
|
-
for (let cur = this._head; cur; cur = cur.prev) {
|
|
6848
|
-
messages[cur.level] = cur.current;
|
|
6849
|
-
}
|
|
6850
|
-
return messages;
|
|
6851
|
-
}
|
|
6852
|
-
/**
|
|
6853
|
-
* Returns metadata about a specific message, or `undefined` if not found.
|
|
6854
|
-
*
|
|
6855
|
-
* Read methods (`getMessage`, `getMessageById`, `getBranches`) all return
|
|
6856
|
-
* `undefined` / `[]` for unknown ids — never throw — so callers can do a
|
|
6857
|
-
* single existence check at the top of a flow rather than wrapping every
|
|
6858
|
-
* call in try/catch.
|
|
6859
|
-
*/
|
|
6860
|
-
getMessage(id) {
|
|
6861
|
-
const node = this._nodes.get(id);
|
|
6862
|
-
if (!node) return void 0;
|
|
6863
|
-
return {
|
|
6864
|
-
parentId: node.prev?.current.id ?? null,
|
|
6865
|
-
message: node.current,
|
|
6866
|
-
index: node.level
|
|
6867
|
-
};
|
|
6868
|
-
}
|
|
6869
|
-
/**
|
|
6870
|
-
* Returns the mutable message reference, or `undefined` if not found.
|
|
6871
|
-
* Mutations to the returned object are reflected in `getMessages()`.
|
|
6872
|
-
*/
|
|
6873
|
-
getMessageById(id) {
|
|
6874
|
-
return this._nodes.get(id)?.current;
|
|
6875
|
-
}
|
|
6876
|
-
/**
|
|
6877
|
-
* Returns the IDs of all siblings of `messageId`
|
|
6878
|
-
* (i.e. all children of its parent, including itself).
|
|
6879
|
-
* Use this to build branch-picker UI.
|
|
6880
|
-
*/
|
|
6881
|
-
getBranches(messageId) {
|
|
6882
|
-
const node = this._nodes.get(messageId);
|
|
6883
|
-
if (!node) return [];
|
|
6884
|
-
const parent = node.prev ?? this._root;
|
|
6885
|
-
return [...parent.children];
|
|
6886
|
-
}
|
|
6887
|
-
// ─── Public write API ───────────────────────────────────────────────────
|
|
6888
|
-
/**
|
|
6889
|
-
* Add a new message as a child of `parentId` (null = root).
|
|
6890
|
-
* If a message with the same id already exists, updates its data
|
|
6891
|
-
* and re-links it under the correct parent.
|
|
6892
|
-
*/
|
|
6893
|
-
addOrUpdateMessage(parentId, message) {
|
|
6894
|
-
const existing = this._nodes.get(message.id);
|
|
6895
|
-
const parentNode = parentId ? this._nodes.get(parentId) : void 0;
|
|
6896
|
-
if (parentId !== null && parentNode === void 0) {
|
|
6897
|
-
throw new Error(`AparteMessageRepository: parent "${parentId}" not found.`);
|
|
6898
|
-
}
|
|
6899
|
-
const parent = parentNode ?? null;
|
|
6900
|
-
if (existing) {
|
|
6901
|
-
existing.current = message;
|
|
6902
|
-
this._relink(parent, existing);
|
|
6903
|
-
return;
|
|
6904
|
-
}
|
|
6905
|
-
const node = {
|
|
6906
|
-
prev: parent,
|
|
6907
|
-
next: null,
|
|
6908
|
-
children: [],
|
|
6909
|
-
current: message,
|
|
6910
|
-
level: parent ? parent.level + 1 : 0
|
|
6911
|
-
};
|
|
6912
|
-
this._nodes.set(message.id, node);
|
|
6913
|
-
this._link(parent, node);
|
|
6914
|
-
if (this._head === parent || this._head === null && parent === null) {
|
|
6915
|
-
this._head = node;
|
|
6916
|
-
}
|
|
6917
|
-
}
|
|
6918
|
-
/**
|
|
6919
|
-
* Switch the active branch to `messageId`.
|
|
6920
|
-
* Updates `next` on the parent chain so getMessages() reflects the change.
|
|
6921
|
-
* Advances `head` to the deepest active node in the new branch.
|
|
6922
|
-
*
|
|
6923
|
-
* Silent no-op for unknown ids — mutation methods (`switchToBranch`,
|
|
6924
|
-
* `resetHead`, `updateMessage`, `clearChildren`) never throw.
|
|
6925
|
-
*/
|
|
6926
|
-
switchToBranch(messageId) {
|
|
6927
|
-
const node = this._nodes.get(messageId);
|
|
6928
|
-
if (!node) return;
|
|
6929
|
-
const parentOrRoot = node.prev ?? this._root;
|
|
6930
|
-
parentOrRoot.next = node;
|
|
6931
|
-
this._head = findHead(node);
|
|
6932
|
-
}
|
|
6933
|
-
/**
|
|
6934
|
-
* Remove all descendants of `messageId` (inclusive) and set `head` to
|
|
6935
|
-
* the parent of `messageId`. Used by edit/truncate flows.
|
|
6936
|
-
*
|
|
6937
|
-
* @param messageId The first message to remove
|
|
6938
|
-
* @param newParentId Optional explicit parent to set head to
|
|
6939
|
-
*/
|
|
6940
|
-
resetHead(messageId) {
|
|
6941
|
-
const node = this._nodes.get(messageId);
|
|
6942
|
-
if (!node) return;
|
|
6943
|
-
this._deleteDescendants(node);
|
|
6944
|
-
const parentOrRoot = node.prev ?? this._root;
|
|
6945
|
-
parentOrRoot.children = parentOrRoot.children.filter((id) => id !== messageId);
|
|
6946
|
-
if (parentOrRoot.next === node) {
|
|
6947
|
-
parentOrRoot.next = null;
|
|
6877
|
+
}
|
|
6878
|
+
if (s.type === "artifact") {
|
|
6879
|
+
dispatchArtifactLifecycle(targetElement, messageId, s, artifactProgress, true);
|
|
6880
|
+
}
|
|
6881
|
+
}
|
|
6882
|
+
if (toolCallsThisTurn.length === 0) {
|
|
6883
|
+
if (pipeline && pipelineIndex < pipeline.length - 1) {
|
|
6884
|
+
if (precedingText.trim()) {
|
|
6885
|
+
messages.push({ role: "assistant", content: precedingText.trim() });
|
|
6886
|
+
}
|
|
6887
|
+
pipelineIndex++;
|
|
6888
|
+
const pwId = `pw-${uuid()}`;
|
|
6889
|
+
targetElement.addSegment?.({ id: pwId, type: "pipeline-waiting" });
|
|
6890
|
+
} else {
|
|
6891
|
+
continueLoop = false;
|
|
6892
|
+
}
|
|
6893
|
+
}
|
|
6894
|
+
} finally {
|
|
6895
|
+
if (!streamDrained) {
|
|
6896
|
+
try {
|
|
6897
|
+
await reader.cancel();
|
|
6898
|
+
} catch {
|
|
6899
|
+
}
|
|
6900
|
+
}
|
|
6901
|
+
reader.releaseLock();
|
|
6902
|
+
}
|
|
6948
6903
|
}
|
|
6949
|
-
this.
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
if (!node) return;
|
|
6958
|
-
node.current = { ...node.current, ...updates };
|
|
6904
|
+
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
6905
|
+
if (lastUsage) {
|
|
6906
|
+
try {
|
|
6907
|
+
targetElement.setUsage?.(messageId, lastUsage);
|
|
6908
|
+
} catch {
|
|
6909
|
+
}
|
|
6910
|
+
}
|
|
6911
|
+
return lastUsage;
|
|
6959
6912
|
}
|
|
6960
6913
|
/**
|
|
6961
|
-
*
|
|
6962
|
-
*
|
|
6963
|
-
*
|
|
6914
|
+
* Delegate `_streamLoop`'s agentic loop to an injected {@link AparteStreamRunner},
|
|
6915
|
+
* rendering its DOM-free events through {@link createStreamAdapter}.
|
|
6916
|
+
* Builds the runner's dependencies from this client's config / provider /
|
|
6917
|
+
* transport; the adapter reproduces the inline loop's `targetElement.*` calls
|
|
6918
|
+
* (validated by the engine `stream-parity` suite). Leading writes (status
|
|
6919
|
+
* streaming, prefix segments) mirror the inline path; the runner's `run-start`
|
|
6920
|
+
* re-asserts `streaming` idempotently.
|
|
6964
6921
|
*/
|
|
6965
|
-
|
|
6966
|
-
const
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
this._deleteDescendants(child);
|
|
6972
|
-
this._nodes.delete(childId);
|
|
6973
|
-
}
|
|
6922
|
+
async _runViaStreamRunner(streamRunner, targetElement, messageId, provider, baseRequest, authConfig, streamController) {
|
|
6923
|
+
const signal = streamController.signal;
|
|
6924
|
+
this._updateMessage(targetElement, messageId, { status: "streaming" });
|
|
6925
|
+
const prefixSegments = baseRequest._meta?.["prefixSegments"];
|
|
6926
|
+
if (prefixSegments?.length) {
|
|
6927
|
+
for (const seg of prefixSegments) targetElement.addSegment?.(seg);
|
|
6974
6928
|
}
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6929
|
+
const artifactHint = baseRequest._meta?.artifactHint;
|
|
6930
|
+
const emitter = createStreamAdapter({
|
|
6931
|
+
target: targetElement,
|
|
6932
|
+
config: this._config,
|
|
6933
|
+
messageId,
|
|
6934
|
+
artifactHint
|
|
6935
|
+
});
|
|
6936
|
+
const transportCall = async (request) => {
|
|
6937
|
+
const response = await this._config.getTransport().chat(provider, request, authConfig, { providerId: provider.id, signal });
|
|
6938
|
+
return typeof response === "string" ? response : readableToAsyncIterable(response, signal);
|
|
6939
|
+
};
|
|
6940
|
+
const toolLookup = (name) => {
|
|
6941
|
+
const handler = this._config.getToolHandler(name);
|
|
6942
|
+
if (!handler) return void 0;
|
|
6943
|
+
const context = { target: targetElement, config: this._config };
|
|
6944
|
+
return (call, sig) => handler(call, sig, context);
|
|
6945
|
+
};
|
|
6946
|
+
const toolConfigLookup = (name) => {
|
|
6947
|
+
const tool = this._config.getTools().find((t) => t.name === name);
|
|
6948
|
+
return tool ? { maxTurns: tool.maxTurns, needsApproval: tool.needsApproval } : void 0;
|
|
6949
|
+
};
|
|
6950
|
+
const approvalResolver = this.options.approvalResolver ?? ((call, sig) => this._askForApproval(call, sig, targetElement));
|
|
6951
|
+
const usage = await streamRunner({
|
|
6952
|
+
messageId,
|
|
6953
|
+
baseRequest,
|
|
6954
|
+
transportCall,
|
|
6955
|
+
toolLookup,
|
|
6956
|
+
toolConfigLookup,
|
|
6957
|
+
approvalResolver,
|
|
6958
|
+
emitter,
|
|
6959
|
+
signal,
|
|
6960
|
+
maxTurns: this.options.maxTurns,
|
|
6961
|
+
// Forwarded, because the option's own JSDoc promises it means one thing
|
|
6962
|
+
// whichever loop runs — and the fix that introduced it only wired the
|
|
6963
|
+
// inline path, so setting it was silently ignored here. Exactly the
|
|
6964
|
+
// asymmetry the option was added to remove.
|
|
6965
|
+
toolTimeoutMs: this.options.toolTimeoutMs,
|
|
6966
|
+
// Match the inline loop's id conventions: prefixed artifact ids, but a
|
|
6967
|
+
// BARE uuid for the synthetic tool (the adapter renders `tool-<id>`).
|
|
6968
|
+
idGen: (prefix) => prefix === "synthetic-tool" ? uuid() : `${prefix}-${uuid()}`
|
|
6969
|
+
});
|
|
6970
|
+
return usage ?? void 0;
|
|
6984
6971
|
}
|
|
6985
6972
|
/**
|
|
6986
|
-
*
|
|
6987
|
-
* restoring the active head. Replaces any existing content (calls `clear()`
|
|
6988
|
-
* first). Messages must be in topological order (parent before child) —
|
|
6989
|
-
* this is guaranteed by `export()` since `_nodes` is an insertion-order
|
|
6990
|
-
* Map and parents are always inserted before their children.
|
|
6973
|
+
* Updates an existing message or appends a new error if ID lookup fails
|
|
6991
6974
|
*/
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
this.addOrUpdateMessage(parentId, message);
|
|
6996
|
-
}
|
|
6997
|
-
if (exported.headId) {
|
|
6998
|
-
this.switchToBranch(exported.headId);
|
|
6975
|
+
_updateMessage(target, messageId, updates) {
|
|
6976
|
+
if (target.updateMessage) {
|
|
6977
|
+
target.updateMessage(messageId, updates);
|
|
6999
6978
|
}
|
|
7000
6979
|
}
|
|
7001
6980
|
/**
|
|
7002
|
-
*
|
|
7003
|
-
* Compatible with the assistant-ui ExportedMessageRepository format.
|
|
6981
|
+
* Gracefully handles errors by updating the current message instead of duplicating it
|
|
7004
6982
|
*/
|
|
7005
|
-
|
|
7006
|
-
const
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
6983
|
+
_handleLifecycleError(target, messageId, error) {
|
|
6984
|
+
const errorSegment = {
|
|
6985
|
+
id: `error-${uuid()}`,
|
|
6986
|
+
type: "error",
|
|
6987
|
+
content: error.message,
|
|
6988
|
+
details: error.code
|
|
6989
|
+
};
|
|
6990
|
+
if (target.updateMessage) {
|
|
6991
|
+
const rendered = target.getMessages?.()?.find((m) => m.id === messageId)?.segments ?? [];
|
|
6992
|
+
target.updateMessage(messageId, {
|
|
6993
|
+
status: "error",
|
|
6994
|
+
segments: [...rendered, errorSegment]
|
|
7011
6995
|
});
|
|
7012
|
-
})
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
}
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
}
|
|
7033
|
-
child.prev = newParent;
|
|
7034
|
-
const newParentOrRoot = newParent ?? this._root;
|
|
7035
|
-
if (!newParentOrRoot.children.includes(child.current.id)) {
|
|
7036
|
-
newParentOrRoot.children = [...newParentOrRoot.children, child.current.id];
|
|
7037
|
-
}
|
|
7038
|
-
if (newParentOrRoot.next === null) {
|
|
7039
|
-
newParentOrRoot.next = child;
|
|
7040
|
-
}
|
|
7041
|
-
this._updateLevels(child, newParent ? newParent.level + 1 : 0);
|
|
7042
|
-
}
|
|
7043
|
-
_updateLevels(node, newLevel) {
|
|
7044
|
-
node.level = newLevel;
|
|
7045
|
-
for (const childId of node.children) {
|
|
7046
|
-
const childNode = this._nodes.get(childId);
|
|
7047
|
-
if (childNode) this._updateLevels(childNode, newLevel + 1);
|
|
7048
|
-
}
|
|
7049
|
-
}
|
|
7050
|
-
_deleteDescendants(node) {
|
|
7051
|
-
for (const childId of node.children) {
|
|
7052
|
-
const child = this._nodes.get(childId);
|
|
7053
|
-
if (child) {
|
|
7054
|
-
this._deleteDescendants(child);
|
|
7055
|
-
this._nodes.delete(childId);
|
|
7056
|
-
}
|
|
7057
|
-
}
|
|
7058
|
-
node.children = [];
|
|
7059
|
-
node.next = null;
|
|
7060
|
-
}
|
|
7061
|
-
}
|
|
7062
|
-
const APARTE_DEFAULT_UI_EVENTS = [
|
|
7063
|
-
"aparte-send",
|
|
7064
|
-
"aparte-action",
|
|
7065
|
-
"aparte-retry",
|
|
7066
|
-
"aparte-edit",
|
|
7067
|
-
"aparte-branch-navigate",
|
|
7068
|
-
"aparte-composer-change",
|
|
7069
|
-
"aparte-path-changed"
|
|
7070
|
-
];
|
|
7071
|
-
function applyElementProps(el2, props, transformValue = (value) => value) {
|
|
7072
|
-
for (const [key, value] of Object.entries(props)) {
|
|
7073
|
-
if (key.startsWith("--")) {
|
|
7074
|
-
el2.style.setProperty(key, String(value));
|
|
7075
|
-
} else if (key.startsWith("on")) {
|
|
7076
|
-
el2.removeAttribute(key);
|
|
7077
|
-
} else if (value === null || value === void 0 || value === false) {
|
|
7078
|
-
el2.removeAttribute(key);
|
|
7079
|
-
} else if (value === true) {
|
|
7080
|
-
el2.setAttribute(key, "");
|
|
7081
|
-
} else if (typeof value === "object" || typeof value === "function") {
|
|
7082
|
-
el2[key] = transformValue(value);
|
|
7083
|
-
} else {
|
|
7084
|
-
el2.setAttribute(key, String(value));
|
|
7085
|
-
}
|
|
7086
|
-
}
|
|
7087
|
-
}
|
|
7088
|
-
function isAwaitingReply(message) {
|
|
7089
|
-
if (message.status === "streaming" || message.status === "pending") return true;
|
|
7090
|
-
return message.status === void 0 && message.role === "assistant" && !message.content?.trim() && !message.segments?.length;
|
|
7091
|
-
}
|
|
7092
|
-
let _relabels = null;
|
|
7093
|
-
function onRelabel(fn) {
|
|
7094
|
-
_relabels?.push(fn);
|
|
7095
|
-
}
|
|
7096
|
-
function el(tag, className, text) {
|
|
7097
|
-
const node = document.createElement(tag);
|
|
7098
|
-
if (className) node.className = className;
|
|
7099
|
-
if (text != null) node.textContent = text;
|
|
7100
|
-
return node;
|
|
7101
|
-
}
|
|
7102
|
-
function fieldHeader(parent, field, skipTitle) {
|
|
7103
|
-
let titleId;
|
|
7104
|
-
if (field.title && field.title.trim() !== skipTitle?.trim()) {
|
|
7105
|
-
const title = el("p", "aparte-elic-title", field.title);
|
|
7106
|
-
titleId = `elic-title-${uuid()}`;
|
|
7107
|
-
title.id = titleId;
|
|
7108
|
-
parent.appendChild(title);
|
|
6996
|
+
} else if (target.appendMessage) {
|
|
6997
|
+
target.appendMessage({
|
|
6998
|
+
id: messageId,
|
|
6999
|
+
role: "assistant",
|
|
7000
|
+
status: "error",
|
|
7001
|
+
timestamp: Date.now(),
|
|
7002
|
+
segments: [errorSegment]
|
|
7003
|
+
});
|
|
7004
|
+
}
|
|
7005
|
+
dispatchLifecycleEvent(target, "aparte-message-error", { messageId, error });
|
|
7006
|
+
}
|
|
7007
|
+
/** Warn once per client for an unrecognised stream event, then carry on. */
|
|
7008
|
+
_warnedUnknownEvents = /* @__PURE__ */ new Set();
|
|
7009
|
+
_warnUnknownStreamEvent(event) {
|
|
7010
|
+
const type = String(event?.type ?? "undefined");
|
|
7011
|
+
if (this._warnedUnknownEvents.has(type)) return;
|
|
7012
|
+
this._warnedUnknownEvents.add(type);
|
|
7013
|
+
console.warn(
|
|
7014
|
+
`[AparteClient] Ignoring unrecognised stream event "${type}". The reply is unaffected; this usually means the provider or its SDK emits a part this version of aparté does not map yet.`
|
|
7015
|
+
);
|
|
7109
7016
|
}
|
|
7110
|
-
if (field.description) parent.appendChild(el("p", "aparte-elic-desc", field.description));
|
|
7111
|
-
return titleId;
|
|
7112
7017
|
}
|
|
7113
|
-
function
|
|
7114
|
-
|
|
7115
|
-
if (
|
|
7116
|
-
|
|
7018
|
+
function findHead(node) {
|
|
7019
|
+
if (node.next) return findHead(node.next);
|
|
7020
|
+
if ("current" in node) return node;
|
|
7021
|
+
return null;
|
|
7117
7022
|
}
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
const defaults = new Set(Array.isArray(field.default) ? field.default : field.default != null ? [field.default] : []);
|
|
7127
|
-
const buildOption = (value, label, description, recommended) => {
|
|
7128
|
-
const row = el("label", "aparte-elic-option" + (recommended ? " aparte-elic-option--recommended" : ""));
|
|
7129
|
-
const control = el("input", "aparte-elic-control");
|
|
7130
|
-
control.type = type;
|
|
7131
|
-
control.name = name;
|
|
7132
|
-
control.value = value;
|
|
7133
|
-
if (defaults.has(value)) control.checked = true;
|
|
7134
|
-
const body = el("span", "aparte-elic-option-body");
|
|
7135
|
-
body.appendChild(el("span", "aparte-elic-option-title", label));
|
|
7136
|
-
if (description) body.appendChild(el("span", "aparte-elic-option-desc", description));
|
|
7137
|
-
row.append(control, body);
|
|
7138
|
-
return row;
|
|
7139
|
-
};
|
|
7140
|
-
for (const opt of field.options) {
|
|
7141
|
-
list.appendChild(buildOption(opt.value, opt.label ?? opt.value, opt.description, opt.recommended));
|
|
7023
|
+
class AparteMessageRepository {
|
|
7024
|
+
_nodes = /* @__PURE__ */ new Map();
|
|
7025
|
+
_head = null;
|
|
7026
|
+
_root = { next: null, children: [] };
|
|
7027
|
+
// ─── Public read API ────────────────────────────────────────────────────
|
|
7028
|
+
/** ID of the currently active leaf message */
|
|
7029
|
+
get headId() {
|
|
7030
|
+
return this._head?.current.id ?? null;
|
|
7142
7031
|
}
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
const
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
otherText.type = "text";
|
|
7156
|
-
otherText.placeholder = t.t("elicitationOtherPlaceholder");
|
|
7157
|
-
otherText.style.display = "none";
|
|
7158
|
-
otherText.setAttribute("aria-label", t.t("elicitationOtherLabel"));
|
|
7159
|
-
const otherInput = otherText;
|
|
7160
|
-
onRelabel(() => {
|
|
7161
|
-
const now = contextConfig();
|
|
7162
|
-
otherTitle.textContent = now.t("elicitationOther");
|
|
7163
|
-
otherInput.placeholder = now.t("elicitationOtherPlaceholder");
|
|
7164
|
-
otherInput.setAttribute("aria-label", now.t("elicitationOtherLabel"));
|
|
7165
|
-
});
|
|
7166
|
-
body.appendChild(otherText);
|
|
7167
|
-
row.append(control, body);
|
|
7168
|
-
list.appendChild(row);
|
|
7032
|
+
/**
|
|
7033
|
+
* Returns the messages on the currently active path, root → head.
|
|
7034
|
+
* O(depth).
|
|
7035
|
+
*/
|
|
7036
|
+
getMessages() {
|
|
7037
|
+
const depth = this._head?.level ?? -1;
|
|
7038
|
+
if (depth < 0) return [];
|
|
7039
|
+
const messages = new Array(depth + 1);
|
|
7040
|
+
for (let cur = this._head; cur; cur = cur.prev) {
|
|
7041
|
+
messages[cur.level] = cur.current;
|
|
7042
|
+
}
|
|
7043
|
+
return messages;
|
|
7169
7044
|
}
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7045
|
+
/**
|
|
7046
|
+
* Returns metadata about a specific message, or `undefined` if not found.
|
|
7047
|
+
*
|
|
7048
|
+
* Read methods (`getMessage`, `getMessageById`, `getBranches`) all return
|
|
7049
|
+
* `undefined` / `[]` for unknown ids — never throw — so callers can do a
|
|
7050
|
+
* single existence check at the top of a flow rather than wrapping every
|
|
7051
|
+
* call in try/catch.
|
|
7052
|
+
*/
|
|
7053
|
+
getMessage(id) {
|
|
7054
|
+
const node = this._nodes.get(id);
|
|
7055
|
+
if (!node) return void 0;
|
|
7056
|
+
return {
|
|
7057
|
+
parentId: node.prev?.current.id ?? null,
|
|
7058
|
+
message: node.current,
|
|
7059
|
+
index: node.level
|
|
7060
|
+
};
|
|
7061
|
+
}
|
|
7062
|
+
/**
|
|
7063
|
+
* Returns the mutable message reference, or `undefined` if not found.
|
|
7064
|
+
* Mutations to the returned object are reflected in `getMessages()`.
|
|
7065
|
+
*/
|
|
7066
|
+
getMessageById(id) {
|
|
7067
|
+
return this._nodes.get(id)?.current;
|
|
7068
|
+
}
|
|
7069
|
+
/**
|
|
7070
|
+
* Returns the IDs of all siblings of `messageId`
|
|
7071
|
+
* (i.e. all children of its parent, including itself).
|
|
7072
|
+
* Use this to build branch-picker UI.
|
|
7073
|
+
*/
|
|
7074
|
+
getBranches(messageId) {
|
|
7075
|
+
const node = this._nodes.get(messageId);
|
|
7076
|
+
if (!node) return [];
|
|
7077
|
+
const parent = node.prev ?? this._root;
|
|
7078
|
+
return [...parent.children];
|
|
7079
|
+
}
|
|
7080
|
+
// ─── Public write API ───────────────────────────────────────────────────
|
|
7081
|
+
/**
|
|
7082
|
+
* Add a new message as a child of `parentId` (null = root).
|
|
7083
|
+
* If a message with the same id already exists, updates its data
|
|
7084
|
+
* and re-links it under the correct parent.
|
|
7085
|
+
*/
|
|
7086
|
+
addOrUpdateMessage(parentId, message) {
|
|
7087
|
+
const existing = this._nodes.get(message.id);
|
|
7088
|
+
const parentNode = parentId ? this._nodes.get(parentId) : void 0;
|
|
7089
|
+
if (parentId !== null && parentNode === void 0) {
|
|
7090
|
+
throw new Error(`AparteMessageRepository: parent "${parentId}" not found.`);
|
|
7178
7091
|
}
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
if (field.multiple) {
|
|
7185
|
-
const vals = controls().filter((c) => c.checked && c.value !== "__other__").map((c) => c.value);
|
|
7186
|
-
if (otherVal) vals.push(otherVal);
|
|
7187
|
-
return vals;
|
|
7092
|
+
const parent = parentNode ?? null;
|
|
7093
|
+
if (existing) {
|
|
7094
|
+
existing.current = message;
|
|
7095
|
+
this._relink(parent, existing);
|
|
7096
|
+
return;
|
|
7188
7097
|
}
|
|
7189
|
-
const
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
}
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
const
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
if (field.placeholder) input.setAttribute("placeholder", field.placeholder);
|
|
7244
|
-
if (field.default) input.value = field.default;
|
|
7245
|
-
if (field.maxLength != null) input.setAttribute("maxlength", String(field.maxLength));
|
|
7246
|
-
const ownName = field.title || field.description || fallbackLabel || void 0;
|
|
7247
|
-
input.setAttribute("aria-label", ownName ?? contextConfig().t("elicitationAnswerLabel"));
|
|
7248
|
-
if (ownName == null) {
|
|
7249
|
-
const named = input;
|
|
7250
|
-
onRelabel(() => {
|
|
7251
|
-
named.setAttribute("aria-label", contextConfig().t("elicitationAnswerLabel"));
|
|
7252
|
-
});
|
|
7098
|
+
const node = {
|
|
7099
|
+
prev: parent,
|
|
7100
|
+
next: null,
|
|
7101
|
+
children: [],
|
|
7102
|
+
current: message,
|
|
7103
|
+
level: parent ? parent.level + 1 : 0
|
|
7104
|
+
};
|
|
7105
|
+
this._nodes.set(message.id, node);
|
|
7106
|
+
this._link(parent, node);
|
|
7107
|
+
if (this._head === parent || this._head === null && parent === null) {
|
|
7108
|
+
this._head = node;
|
|
7109
|
+
}
|
|
7110
|
+
}
|
|
7111
|
+
/**
|
|
7112
|
+
* Switch the active branch to `messageId`.
|
|
7113
|
+
* Updates `next` on the parent chain so getMessages() reflects the change.
|
|
7114
|
+
* Advances `head` to the deepest active node in the new branch.
|
|
7115
|
+
*
|
|
7116
|
+
* Silent no-op for unknown ids — mutation methods (`switchToBranch`,
|
|
7117
|
+
* `resetHead`, `updateMessage`, `clearChildren`) never throw.
|
|
7118
|
+
*/
|
|
7119
|
+
switchToBranch(messageId) {
|
|
7120
|
+
const node = this._nodes.get(messageId);
|
|
7121
|
+
if (!node) return;
|
|
7122
|
+
const parentOrRoot = node.prev ?? this._root;
|
|
7123
|
+
parentOrRoot.next = node;
|
|
7124
|
+
this._head = findHead(node);
|
|
7125
|
+
}
|
|
7126
|
+
/**
|
|
7127
|
+
* Remove all descendants of `messageId` (inclusive) and set `head` to
|
|
7128
|
+
* the parent of `messageId`. Used by edit/truncate flows.
|
|
7129
|
+
*
|
|
7130
|
+
* @param messageId The first message to remove
|
|
7131
|
+
* @param newParentId Optional explicit parent to set head to
|
|
7132
|
+
*/
|
|
7133
|
+
resetHead(messageId) {
|
|
7134
|
+
const node = this._nodes.get(messageId);
|
|
7135
|
+
if (!node) return;
|
|
7136
|
+
this._deleteDescendants(node);
|
|
7137
|
+
const parentOrRoot = node.prev ?? this._root;
|
|
7138
|
+
parentOrRoot.children = parentOrRoot.children.filter((id) => id !== messageId);
|
|
7139
|
+
if (parentOrRoot.next === node) {
|
|
7140
|
+
parentOrRoot.next = null;
|
|
7141
|
+
}
|
|
7142
|
+
this._nodes.delete(messageId);
|
|
7143
|
+
this._head = node.prev;
|
|
7144
|
+
}
|
|
7145
|
+
/**
|
|
7146
|
+
* Update a message's data without changing its position in the tree.
|
|
7147
|
+
*/
|
|
7148
|
+
updateMessage(messageId, updates) {
|
|
7149
|
+
const node = this._nodes.get(messageId);
|
|
7150
|
+
if (!node) return;
|
|
7151
|
+
node.current = { ...node.current, ...updates };
|
|
7253
7152
|
}
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
}
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
return {
|
|
7273
|
-
el: custom.el,
|
|
7274
|
-
getValue: () => custom.getValue(),
|
|
7275
|
-
isComplete: () => custom.isComplete(),
|
|
7276
|
-
focus: () => custom.focus?.()
|
|
7277
|
-
};
|
|
7153
|
+
/**
|
|
7154
|
+
* Remove ALL children of `parentId` (and their descendants) and set head
|
|
7155
|
+
* to `parentId`. Used by edit to discard every previous response before
|
|
7156
|
+
* re-generating — ensures the new response starts with sibling count = 1.
|
|
7157
|
+
*/
|
|
7158
|
+
clearChildren(parentId) {
|
|
7159
|
+
const parent = this._nodes.get(parentId);
|
|
7160
|
+
if (!parent) return;
|
|
7161
|
+
for (const childId of [...parent.children]) {
|
|
7162
|
+
const child = this._nodes.get(childId);
|
|
7163
|
+
if (child) {
|
|
7164
|
+
this._deleteDescendants(child);
|
|
7165
|
+
this._nodes.delete(childId);
|
|
7166
|
+
}
|
|
7167
|
+
}
|
|
7168
|
+
parent.children = [];
|
|
7169
|
+
parent.next = null;
|
|
7170
|
+
this._head = parent;
|
|
7278
7171
|
}
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
case "string":
|
|
7285
|
-
return buildStringField(field, onChange, fallbackLabel);
|
|
7172
|
+
/** Remove all messages and reset to empty state. */
|
|
7173
|
+
clear() {
|
|
7174
|
+
this._nodes.clear();
|
|
7175
|
+
this._head = null;
|
|
7176
|
+
this._root = { next: null, children: [] };
|
|
7286
7177
|
}
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
}
|
|
7297
|
-
|
|
7298
|
-
|
|
7178
|
+
/**
|
|
7179
|
+
* Import a previously-exported snapshot, rebuilding the full tree and
|
|
7180
|
+
* restoring the active head. Replaces any existing content (calls `clear()`
|
|
7181
|
+
* first). Messages must be in topological order (parent before child) —
|
|
7182
|
+
* this is guaranteed by `export()` since `_nodes` is an insertion-order
|
|
7183
|
+
* Map and parents are always inserted before their children.
|
|
7184
|
+
*/
|
|
7185
|
+
import(exported) {
|
|
7186
|
+
this.clear();
|
|
7187
|
+
for (const { message, parentId } of exported.messages) {
|
|
7188
|
+
this.addOrUpdateMessage(parentId, message);
|
|
7189
|
+
}
|
|
7190
|
+
if (exported.headId) {
|
|
7191
|
+
this.switchToBranch(exported.headId);
|
|
7192
|
+
}
|
|
7299
7193
|
}
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
current = index;
|
|
7311
|
-
fields.forEach((f, i) => {
|
|
7312
|
-
f.field.el.hidden = i !== index;
|
|
7194
|
+
/**
|
|
7195
|
+
* Export the full tree for serialisation or debugging.
|
|
7196
|
+
* Compatible with the assistant-ui ExportedMessageRepository format.
|
|
7197
|
+
*/
|
|
7198
|
+
export() {
|
|
7199
|
+
const messages = [];
|
|
7200
|
+
this._nodes.forEach((node) => {
|
|
7201
|
+
messages.push({
|
|
7202
|
+
message: node.current,
|
|
7203
|
+
parentId: node.prev?.current.id ?? null
|
|
7313
7204
|
});
|
|
7314
|
-
syncNav();
|
|
7315
|
-
};
|
|
7316
|
-
const entries = Object.entries(schema.properties);
|
|
7317
|
-
const requiredKeys = new Set(schema.required ?? entries.map(([k]) => k));
|
|
7318
|
-
const cfg = contextConfig();
|
|
7319
|
-
let syncNav = () => {
|
|
7320
|
-
};
|
|
7321
|
-
const notify = () => {
|
|
7322
|
-
syncNav();
|
|
7323
|
-
onChange();
|
|
7324
|
-
};
|
|
7325
|
-
const fields = entries.map(([key, field2]) => {
|
|
7326
|
-
const built = buildField(field2, notify, void 0, key);
|
|
7327
|
-
if (!field2.title && !built.el.querySelector(".aparte-elic-title")) {
|
|
7328
|
-
built.el.insertBefore(el("p", "aparte-elic-title", key), built.el.firstChild);
|
|
7329
|
-
}
|
|
7330
|
-
body.appendChild(built.el);
|
|
7331
|
-
return { key, field: built, required: requiredKeys.has(key), header: field2.header };
|
|
7332
7205
|
});
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7206
|
+
return { headId: this.headId, messages };
|
|
7207
|
+
}
|
|
7208
|
+
// ─── Private helpers ────────────────────────────────────────────────────
|
|
7209
|
+
_link(parent, child) {
|
|
7210
|
+
const parentOrRoot = parent ?? this._root;
|
|
7211
|
+
parentOrRoot.children = [...parentOrRoot.children, child.current.id];
|
|
7212
|
+
if (parentOrRoot.next === null) {
|
|
7213
|
+
parentOrRoot.next = child;
|
|
7214
|
+
}
|
|
7215
|
+
child.prev = parent;
|
|
7216
|
+
}
|
|
7217
|
+
_relink(newParent, child) {
|
|
7218
|
+
const oldParentOrRoot = child.prev ?? this._root;
|
|
7219
|
+
if (oldParentOrRoot !== (newParent ?? this._root)) {
|
|
7220
|
+
oldParentOrRoot.children = oldParentOrRoot.children.filter((id) => id !== child.current.id);
|
|
7221
|
+
if (oldParentOrRoot.next === child) {
|
|
7222
|
+
const nextId = oldParentOrRoot.children[0];
|
|
7223
|
+
oldParentOrRoot.next = nextId ? this._nodes.get(nextId) ?? null : null;
|
|
7344
7224
|
}
|
|
7345
|
-
}
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
};
|
|
7370
|
-
show(0);
|
|
7371
|
-
const last = () => current === fields.length - 1;
|
|
7372
|
-
return {
|
|
7373
|
-
...api,
|
|
7374
|
-
focus: () => fields[current]?.field.focus(),
|
|
7375
|
-
mode: () => last() ? "submit" : "advance",
|
|
7376
|
-
// Advancing needs only THIS question answered; submitting needs them all,
|
|
7377
|
-
// which is what stops the last question from accepting a form with a hole
|
|
7378
|
-
// in it.
|
|
7379
|
-
canProceed: () => last() ? isComplete() : !!fields[current]?.field.isComplete(),
|
|
7380
|
-
proceed: () => {
|
|
7381
|
-
if (last()) return;
|
|
7382
|
-
show(current + 1);
|
|
7383
|
-
fields[current]?.field.focus();
|
|
7225
|
+
}
|
|
7226
|
+
child.prev = newParent;
|
|
7227
|
+
const newParentOrRoot = newParent ?? this._root;
|
|
7228
|
+
if (!newParentOrRoot.children.includes(child.current.id)) {
|
|
7229
|
+
newParentOrRoot.children = [...newParentOrRoot.children, child.current.id];
|
|
7230
|
+
}
|
|
7231
|
+
if (newParentOrRoot.next === null) {
|
|
7232
|
+
newParentOrRoot.next = child;
|
|
7233
|
+
}
|
|
7234
|
+
this._updateLevels(child, newParent ? newParent.level + 1 : 0);
|
|
7235
|
+
}
|
|
7236
|
+
_updateLevels(node, newLevel) {
|
|
7237
|
+
node.level = newLevel;
|
|
7238
|
+
for (const childId of node.children) {
|
|
7239
|
+
const childNode = this._nodes.get(childId);
|
|
7240
|
+
if (childNode) this._updateLevels(childNode, newLevel + 1);
|
|
7241
|
+
}
|
|
7242
|
+
}
|
|
7243
|
+
_deleteDescendants(node) {
|
|
7244
|
+
for (const childId of node.children) {
|
|
7245
|
+
const child = this._nodes.get(childId);
|
|
7246
|
+
if (child) {
|
|
7247
|
+
this._deleteDescendants(child);
|
|
7248
|
+
this._nodes.delete(childId);
|
|
7384
7249
|
}
|
|
7385
|
-
}
|
|
7250
|
+
}
|
|
7251
|
+
node.children = [];
|
|
7252
|
+
node.next = null;
|
|
7386
7253
|
}
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7254
|
+
}
|
|
7255
|
+
const APARTE_DEFAULT_UI_EVENTS = [
|
|
7256
|
+
// <aparte-composer> and its parts
|
|
7257
|
+
"aparte-send",
|
|
7258
|
+
"aparte-cancel",
|
|
7259
|
+
"aparte-composer-change",
|
|
7260
|
+
"aparte-composer-submit",
|
|
7261
|
+
"aparte-action-click",
|
|
7262
|
+
"aparte-attachment-preview",
|
|
7263
|
+
// <aparte-chat-bubble>
|
|
7264
|
+
"aparte-action",
|
|
7265
|
+
"aparte-retry",
|
|
7266
|
+
"aparte-edit",
|
|
7267
|
+
"aparte-feedback",
|
|
7268
|
+
"aparte-message-info",
|
|
7269
|
+
"aparte-branch-navigate",
|
|
7270
|
+
// <aparte-chat-viewport>
|
|
7271
|
+
"aparte-segment-update",
|
|
7272
|
+
"aparte-reset-done",
|
|
7273
|
+
"aparte-path-changed",
|
|
7274
|
+
// <aparte-conversation-list>
|
|
7275
|
+
"aparte-select-conversation",
|
|
7276
|
+
"aparte-delete-conversation",
|
|
7277
|
+
"aparte-archive-conversation",
|
|
7278
|
+
"aparte-unarchive-conversation",
|
|
7279
|
+
// the select primitives
|
|
7280
|
+
"aparte-select-change",
|
|
7281
|
+
"aparte-select-open",
|
|
7282
|
+
"aparte-select-close",
|
|
7283
|
+
"aparte-optgroup-toggle"
|
|
7284
|
+
];
|
|
7285
|
+
function applyElementProps(el2, props, transformValue = (value) => value) {
|
|
7286
|
+
for (const [key, value] of Object.entries(props)) {
|
|
7287
|
+
if (key.startsWith("--")) {
|
|
7288
|
+
el2.style.setProperty(key, String(value));
|
|
7289
|
+
} else if (key.startsWith("on")) {
|
|
7290
|
+
el2.removeAttribute(key);
|
|
7291
|
+
} else if (value === null || value === void 0 || value === false) {
|
|
7292
|
+
el2.removeAttribute(key);
|
|
7293
|
+
} else if (value === true) {
|
|
7294
|
+
el2.setAttribute(key, "");
|
|
7295
|
+
} else if (typeof value === "object" || typeof value === "function") {
|
|
7296
|
+
el2[key] = transformValue(value);
|
|
7297
|
+
} else {
|
|
7298
|
+
el2.setAttribute(key, String(value));
|
|
7398
7299
|
}
|
|
7399
|
-
}
|
|
7300
|
+
}
|
|
7400
7301
|
}
|
|
7401
|
-
function
|
|
7402
|
-
|
|
7302
|
+
function isAwaitingReply(message) {
|
|
7303
|
+
if (message.status === "streaming" || message.status === "pending") return true;
|
|
7304
|
+
return message.status === void 0 && message.role === "assistant" && !message.content?.trim() && !message.segments?.length;
|
|
7403
7305
|
}
|
|
7404
7306
|
export {
|
|
7405
|
-
|
|
7307
|
+
resolveConfig as $,
|
|
7406
7308
|
APARTE_CONFIG_CHANGE as A,
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7309
|
+
contentToText as B,
|
|
7310
|
+
contextConfig as C,
|
|
7311
|
+
createAparteChatHandler as D,
|
|
7312
|
+
createStreamAdapter as E,
|
|
7313
|
+
cssEscape as F,
|
|
7314
|
+
declineDefaultRenderers as G,
|
|
7315
|
+
defaultSanitizer as H,
|
|
7316
|
+
deriveArtifactKind as I,
|
|
7317
|
+
detachConfig as J,
|
|
7318
|
+
escapeAttr as K,
|
|
7319
|
+
escapeHtml as L,
|
|
7320
|
+
filesToAttachments as M,
|
|
7321
|
+
getAllRenderers as N,
|
|
7322
|
+
getSegmentRenderer as O,
|
|
7323
|
+
installDefaultRenderersOnce as P,
|
|
7324
|
+
isAwaitingReply as Q,
|
|
7325
|
+
isFormatAdapter as R,
|
|
7326
|
+
isSafeUrl as S,
|
|
7327
|
+
isSegmentSettled as T,
|
|
7328
|
+
parseAparteEventStream as U,
|
|
7329
|
+
parseMarkdownToSegments as V,
|
|
7330
|
+
populateBubbleFromMessage as W,
|
|
7331
|
+
readableToAsyncIterable as X,
|
|
7332
|
+
registerDefaultRenderers as Y,
|
|
7333
|
+
registerSegmentRenderer as Z,
|
|
7334
|
+
requestUserInput as _,
|
|
7433
7335
|
APARTE_CONVERSATION_SCHEMA_VERSION as a,
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7336
|
+
revokeAttachmentUrls as a0,
|
|
7337
|
+
runWithConfig as a1,
|
|
7338
|
+
segmentDuration as a2,
|
|
7339
|
+
segmentTiming as a3,
|
|
7340
|
+
subscribeConfigChange as a4,
|
|
7341
|
+
unregisterSegmentRenderer as a5,
|
|
7342
|
+
uuid as a6,
|
|
7343
|
+
mergeSegmentUpdate as a7,
|
|
7344
|
+
stampSegmentActivity as a8,
|
|
7345
|
+
stampSegmentOnInsert as a9,
|
|
7346
|
+
stampSegmentOnUpdate as aa,
|
|
7347
|
+
renumberSegments as ab,
|
|
7348
|
+
openSegmentIds as ac,
|
|
7349
|
+
isTerminalStatus as ad,
|
|
7350
|
+
adoptMessageSegments as ae,
|
|
7447
7351
|
APARTE_DEFAULT_BUBBLE_ACTIONS as b,
|
|
7448
7352
|
APARTE_DEFAULT_HOST_HANDLERS as c,
|
|
7449
7353
|
APARTE_DEFAULT_ICON_FALLBACKS as d,
|
|
@@ -7458,16 +7362,16 @@ export {
|
|
|
7458
7362
|
AparteConversationController as m,
|
|
7459
7363
|
AparteConversationManager as n,
|
|
7460
7364
|
AparteDirectTransport as o,
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7365
|
+
AparteElicitationAbortError as p,
|
|
7366
|
+
AparteError as q,
|
|
7367
|
+
AparteErrorCode as r,
|
|
7368
|
+
AparteMessageRepository as s,
|
|
7369
|
+
AparteStreamParser as t,
|
|
7370
|
+
aparteGlobalConfig as u,
|
|
7371
|
+
applyElementProps as v,
|
|
7372
|
+
attachConfig as w,
|
|
7373
|
+
buildApprovalPanel as x,
|
|
7374
|
+
buildElicitationPanel as y,
|
|
7375
|
+
collectRendererStyles as z
|
|
7472
7376
|
};
|
|
7473
|
-
//# sourceMappingURL=
|
|
7377
|
+
//# sourceMappingURL=is-awaiting-reply-Cb7H3YfN.js.map
|