@aparte/core 0.9.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 +49 -7
- 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-action.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-add-attachment.d.ts +16 -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 +10 -2
- package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
- 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 +34 -2
- package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
- 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 +22 -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 +18 -5
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +54 -2
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/config-subscribe.d.ts +4 -0
- package/dist/config/config-subscribe.d.ts.map +1 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/locale.d.ts +141 -0
- package/dist/config/locale.d.ts.map +1 -1
- package/dist/config/sanitize.d.ts.map +1 -1
- package/dist/custom-elements.json +5905 -4694
- 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 +14 -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/host/aparte-chat-host.d.ts +3 -1
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/index.css +752 -392
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +538 -290
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +5 -4
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +62 -57
- 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-hQQljwfX.js → is-awaiting-reply-Cb7H3YfN.js} +1159 -972
- package/dist/is-awaiting-reply-Cb7H3YfN.js.map +1 -0
- package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
- 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/highlight-stream.d.ts +17 -0
- package/dist/renderers/highlight-stream.d.ts.map +1 -0
- package/dist/renderers/segment-renderers.d.ts.map +1 -1
- package/dist/renderers/segments/artifact/binary-file.d.ts.map +1 -1
- package/dist/renderers/segments/artifact/card.d.ts.map +1 -1
- package/dist/renderers/segments/artifact/shared.d.ts +19 -2
- package/dist/renderers/segments/artifact/shared.d.ts.map +1 -1
- package/dist/renderers/segments/code.d.ts.map +1 -1
- package/dist/renderers/segments/error.d.ts.map +1 -1
- package/dist/renderers/segments/pipeline-waiting.d.ts.map +1 -1
- package/dist/renderers/segments/thinking.d.ts.map +1 -1
- package/dist/renderers/segments/tool-call.d.ts.map +1 -1
- package/dist/types/element-attributes.d.ts +42 -0
- package/dist/types/element-attributes.d.ts.map +1 -0
- package/dist/types/event-map.d.ts +2 -4
- package/dist/types/event-map.d.ts.map +1 -1
- package/dist/types/events.d.ts +0 -22
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/models.d.ts +1 -6
- package/dist/types/models.d.ts.map +1 -1
- package/dist/types/segments.d.ts +87 -74
- 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 +83 -5
- package/dist/utils/segments.d.ts.map +1 -1
- package/package.json +3 -2
- package/dist/index-hQQljwfX.js.map +0 -1
- package/dist/renderers/segments/file-tree.d.ts +0 -3
- package/dist/renderers/segments/file-tree.d.ts.map +0 -1
- package/dist/renderers/segments/progress.d.ts +0 -3
- package/dist/renderers/segments/progress.d.ts.map +0 -1
- package/dist/renderers/segments/terminal.d.ts +0 -3
- package/dist/renderers/segments/terminal.d.ts.map +0 -1
|
@@ -69,11 +69,31 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
69
69
|
elicitationOtherPlaceholder: "Type your answer…",
|
|
70
70
|
elicitationOtherLabel: "Custom answer",
|
|
71
71
|
submitButton: "Submit",
|
|
72
|
+
stopButton: "Stop",
|
|
72
73
|
elicitationNext: "Next",
|
|
73
74
|
elicitationSkip: "Skip",
|
|
74
75
|
elicitationYes: "Yes",
|
|
75
76
|
elicitationNo: "No",
|
|
76
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",
|
|
88
|
+
actionUpload: "Attach file",
|
|
89
|
+
download: "Download",
|
|
90
|
+
previewPending: "Press Preview to run this artifact.",
|
|
91
|
+
sandboxError: "The sandbox failed during generation.",
|
|
92
|
+
sandboxErrorHint: "Common cause: the model produced invalid code (undefined variable, wrong argument type). Retry the request — the model may produce different code.",
|
|
93
|
+
preview: "Preview",
|
|
94
|
+
code: "Code",
|
|
95
|
+
generating: "Generating…",
|
|
96
|
+
rebuildingPreview: "Rebuilding preview…",
|
|
77
97
|
typing: "Typing...",
|
|
78
98
|
error: "Error",
|
|
79
99
|
running: "Running...",
|
|
@@ -551,7 +571,6 @@ class AparteStreamParser {
|
|
|
551
571
|
id: this._options.autoGenerateIds ? this._generateId() : "",
|
|
552
572
|
type: "thinking",
|
|
553
573
|
content,
|
|
554
|
-
collapsed: false,
|
|
555
574
|
label: "Thinking"
|
|
556
575
|
};
|
|
557
576
|
}
|
|
@@ -939,6 +958,9 @@ const SAFE_STYLE_PROPS = /* @__PURE__ */ new Set([
|
|
|
939
958
|
"text-decoration-style",
|
|
940
959
|
"white-space"
|
|
941
960
|
]);
|
|
961
|
+
function isSafeCustomProperty(prop) {
|
|
962
|
+
return prop.startsWith("--") && !prop.startsWith("--aparte-");
|
|
963
|
+
}
|
|
942
964
|
function scrubStyle(value) {
|
|
943
965
|
const kept = [];
|
|
944
966
|
for (const decl of value.split(";")) {
|
|
@@ -947,7 +969,8 @@ function scrubStyle(value) {
|
|
|
947
969
|
if (idx === -1) continue;
|
|
948
970
|
const prop = trimmed.slice(0, idx).trim().toLowerCase();
|
|
949
971
|
const val = trimmed.slice(idx + 1);
|
|
950
|
-
if (!val.trim()
|
|
972
|
+
if (!val.trim()) continue;
|
|
973
|
+
if (!SAFE_STYLE_PROPS.has(prop) && !isSafeCustomProperty(prop)) continue;
|
|
951
974
|
if (val.includes("\\")) continue;
|
|
952
975
|
if (/url\s*\(|expression\s*\(|javascript:|vbscript:|[<>]/i.test(val)) continue;
|
|
953
976
|
kept.push(trimmed);
|
|
@@ -1011,6 +1034,23 @@ const defaultSanitizer = (html) => {
|
|
|
1011
1034
|
sanitizeChildren(doc.body, container, doc);
|
|
1012
1035
|
return container.innerHTML;
|
|
1013
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
|
+
}
|
|
1014
1054
|
function escapeHtml(value) {
|
|
1015
1055
|
let out = "";
|
|
1016
1056
|
for (let i = 0; i < value.length; i++) {
|
|
@@ -1034,10 +1074,11 @@ const APARTE_DEFAULT_BUBBLE_ACTIONS = {
|
|
|
1034
1074
|
};
|
|
1035
1075
|
const APARTE_DEFAULT_HOST_HANDLERS = {
|
|
1036
1076
|
attachmentPreview: false,
|
|
1037
|
-
terminalRun: false,
|
|
1038
1077
|
artifactRedownload: false,
|
|
1039
1078
|
artifactRehydrate: false
|
|
1040
1079
|
};
|
|
1080
|
+
const NOOP = () => {
|
|
1081
|
+
};
|
|
1041
1082
|
class AparteConfig {
|
|
1042
1083
|
_markdownProvider;
|
|
1043
1084
|
_streamingMarkdownProvider;
|
|
@@ -1094,9 +1135,27 @@ class AparteConfig {
|
|
|
1094
1135
|
_elicitationPresenter;
|
|
1095
1136
|
/** Warn once per config, not once per request — a tool loop can ask repeatedly. */
|
|
1096
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;
|
|
1097
1155
|
// Tool Registry
|
|
1098
1156
|
_tools = /* @__PURE__ */ new Map();
|
|
1099
1157
|
_toolRenderers = /* @__PURE__ */ new Map();
|
|
1158
|
+
_segmentDefaults = /* @__PURE__ */ new Map();
|
|
1100
1159
|
// Host handlers — what the app declares it can actually complete.
|
|
1101
1160
|
_hostHandlers = { ...APARTE_DEFAULT_HOST_HANDLERS };
|
|
1102
1161
|
// Bubble Actions — APARTE_DEFAULT_BUBBLE_ACTIONS is the single source of truth
|
|
@@ -1177,7 +1236,6 @@ class AparteConfig {
|
|
|
1177
1236
|
getHostHandlers() {
|
|
1178
1237
|
return {
|
|
1179
1238
|
attachmentPreview: this._hostHandlers.attachmentPreview ?? APARTE_DEFAULT_HOST_HANDLERS.attachmentPreview,
|
|
1180
|
-
terminalRun: this._hostHandlers.terminalRun ?? APARTE_DEFAULT_HOST_HANDLERS.terminalRun,
|
|
1181
1239
|
artifactRedownload: this._hostHandlers.artifactRedownload ?? APARTE_DEFAULT_HOST_HANDLERS.artifactRedownload,
|
|
1182
1240
|
artifactRehydrate: this._hostHandlers.artifactRehydrate ?? APARTE_DEFAULT_HOST_HANDLERS.artifactRehydrate
|
|
1183
1241
|
};
|
|
@@ -1799,11 +1857,49 @@ class AparteConfig {
|
|
|
1799
1857
|
*
|
|
1800
1858
|
* @example
|
|
1801
1859
|
* // Custom pill for a web-search tool
|
|
1802
|
-
* aparteGlobalConfig.registerToolRenderer('web_search', { render: (seg) => `<div class="tool-
|
|
1860
|
+
* aparteGlobalConfig.registerToolRenderer('web_search', { render: (seg) => `<div class="aparte-tool-label">Searching...</div>` });
|
|
1803
1861
|
*/
|
|
1804
1862
|
registerToolRenderer(toolName, renderer) {
|
|
1805
1863
|
this._toolRenderers.set(toolName, renderer);
|
|
1806
1864
|
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Field defaults for a segment TYPE, filled in when a segment enters a message.
|
|
1867
|
+
*
|
|
1868
|
+
* One function for every type rather than one per field: a `setThinkingOpen()`
|
|
1869
|
+
* would need a sibling the next time any type wanted a default, and the type key
|
|
1870
|
+
* is a string, so a consumer's own segment type is covered by the same call.
|
|
1871
|
+
*
|
|
1872
|
+
* The problem it solves: a consumer streaming a reply does not CONSTRUCT its
|
|
1873
|
+
* segments — the parser does — so there was no hook at all for "reasoning blocks
|
|
1874
|
+
* open in my app". Per-segment fields only reach segments you build yourself.
|
|
1875
|
+
*
|
|
1876
|
+
* Applied where identity is stamped (`utils/segments.ts`), so it covers every
|
|
1877
|
+
* path a segment can arrive by — the parser, `addSegment`, and the segments
|
|
1878
|
+
* seeded on an `appendMessage` — and no renderer has to look anything up.
|
|
1879
|
+
*
|
|
1880
|
+
* A field the producer set always wins, and identity is never defaulted. Read at
|
|
1881
|
+
* insertion and baked in: changing a default later does not reach segments already
|
|
1882
|
+
* on screen, because a block the reader opened has state the data does not.
|
|
1883
|
+
*
|
|
1884
|
+
* @example
|
|
1885
|
+
* // Reasoning blocks arrive open in this app, streaming or settled.
|
|
1886
|
+
* aparteGlobalConfig.setSegmentDefaults('thinking', { collapsed: false });
|
|
1887
|
+
*
|
|
1888
|
+
* @example
|
|
1889
|
+
* // A consumer's own type, same call.
|
|
1890
|
+
* aparteGlobalConfig.setSegmentDefaults('my-chart', { theme: 'dark' });
|
|
1891
|
+
*/
|
|
1892
|
+
setSegmentDefaults(type, defaults) {
|
|
1893
|
+
this._segmentDefaults.set(type, { ...defaults });
|
|
1894
|
+
}
|
|
1895
|
+
/** The defaults registered for a type, or undefined. */
|
|
1896
|
+
getSegmentDefaults(type) {
|
|
1897
|
+
return this._segmentDefaults.get(type);
|
|
1898
|
+
}
|
|
1899
|
+
/** Drop the defaults for a type. */
|
|
1900
|
+
clearSegmentDefaults(type) {
|
|
1901
|
+
this._segmentDefaults.delete(type);
|
|
1902
|
+
}
|
|
1807
1903
|
/** Unregister a per-tool renderer */
|
|
1808
1904
|
unregisterToolRenderer(toolName) {
|
|
1809
1905
|
this._toolRenderers.delete(toolName);
|
|
@@ -1858,15 +1954,29 @@ class AparteConfig {
|
|
|
1858
1954
|
* itself — nothing to register", which is how this failure mode was found.
|
|
1859
1955
|
*/
|
|
1860
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) {
|
|
1861
1970
|
if (!this._elicitationPresenter) {
|
|
1862
1971
|
if (!this._warnedNoPresenter) {
|
|
1863
1972
|
this._warnedNoPresenter = true;
|
|
1864
1973
|
console.warn(
|
|
1865
|
-
"[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."
|
|
1866
1975
|
);
|
|
1867
1976
|
}
|
|
1868
|
-
return Promise.
|
|
1977
|
+
return Promise.reject(new AparteElicitationAbortError("no-presenter"));
|
|
1869
1978
|
}
|
|
1979
|
+
if (request.signal?.aborted) return Promise.reject(new AparteElicitationAbortError("aborted"));
|
|
1870
1980
|
return this._elicitationPresenter(request);
|
|
1871
1981
|
}
|
|
1872
1982
|
/**
|
|
@@ -1888,11 +1998,13 @@ class AparteConfig {
|
|
|
1888
1998
|
this._siblingNavRenderer = void 0;
|
|
1889
1999
|
this._bubbleShellRenderer = void 0;
|
|
1890
2000
|
this._iconProvider = void 0;
|
|
2001
|
+
this._segmentDefaults.clear();
|
|
1891
2002
|
this._avatarProvider = void 0;
|
|
1892
2003
|
this._artifactPreviewBuilder = void 0;
|
|
1893
2004
|
this._keyProvider = void 0;
|
|
1894
2005
|
this._conversationManager = void 0;
|
|
1895
2006
|
this._elicitationPresenter = void 0;
|
|
2007
|
+
this._elicitationQueue = null;
|
|
1896
2008
|
this._locale = APARTE_DEFAULT_LOCALE;
|
|
1897
2009
|
this._actions = [];
|
|
1898
2010
|
this._sanitizer = defaultSanitizer;
|
|
@@ -1983,6 +2095,16 @@ function runWithConfig(config, fn) {
|
|
|
1983
2095
|
function contextConfig(el2) {
|
|
1984
2096
|
return _renderConfig ?? resolveConfig(el2);
|
|
1985
2097
|
}
|
|
2098
|
+
const APARTE_CONFIG_CHANGE = "aparte-config-change";
|
|
2099
|
+
function subscribeConfigChange(el2, handler) {
|
|
2100
|
+
const listener = (e) => {
|
|
2101
|
+
const detail = e.detail;
|
|
2102
|
+
if (detail?.config && detail.config !== resolveConfig(el2)) return;
|
|
2103
|
+
handler();
|
|
2104
|
+
};
|
|
2105
|
+
window.addEventListener(APARTE_CONFIG_CHANGE, listener);
|
|
2106
|
+
return () => window.removeEventListener(APARTE_CONFIG_CHANGE, listener);
|
|
2107
|
+
}
|
|
1986
2108
|
const APARTE_DEFAULT_SKELETON_FALLBACKS = {
|
|
1987
2109
|
message: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading...</div>',
|
|
1988
2110
|
code: '<div class="aparte-skeleton-fallback" style="padding:16px;background:#1e293b;color:#64748b;border-radius:8px;">Loading code...</div>',
|
|
@@ -2033,9 +2155,9 @@ function highlightMarkdownFences(contentEl) {
|
|
|
2033
2155
|
}
|
|
2034
2156
|
const textRenderer = {
|
|
2035
2157
|
type: "text",
|
|
2036
|
-
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>`,
|
|
2037
2159
|
update: (el2, segment) => {
|
|
2038
|
-
const contentEl = el2.querySelector(".segment-content");
|
|
2160
|
+
const contentEl = el2.querySelector(".aparte-segment-content");
|
|
2039
2161
|
if (!contentEl) return;
|
|
2040
2162
|
writeStreamedMarkdown(
|
|
2041
2163
|
el2,
|
|
@@ -2063,9 +2185,17 @@ const thinkingRenderer = {
|
|
|
2063
2185
|
* new class of risk. With no Markdown provider registered it degrades to the
|
|
2064
2186
|
* zero-dependency default (escape + `<br>`), which is what it used to be.
|
|
2065
2187
|
*/
|
|
2066
|
-
render: (segment) => `<details class="segment segment-thinking" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed ? "" : "
|
|
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>`,
|
|
2189
|
+
/**
|
|
2190
|
+
* The default label is the only config-derived text here — and `segment.label`
|
|
2191
|
+
* still wins, exactly as in `render`, because that string is the app's.
|
|
2192
|
+
*/
|
|
2193
|
+
relabel: (el2, segment) => {
|
|
2194
|
+
const label = el2.querySelector(".aparte-thinking-label");
|
|
2195
|
+
if (label) label.textContent = segment.label || contextConfig().t("thinking");
|
|
2196
|
+
},
|
|
2067
2197
|
update: (el2, segment) => {
|
|
2068
|
-
const contentEl = el2.querySelector(".thinking-content");
|
|
2198
|
+
const contentEl = el2.querySelector(".aparte-thinking-content");
|
|
2069
2199
|
if (!contentEl) return;
|
|
2070
2200
|
const wasAtBottom = contentEl.scrollHeight - contentEl.scrollTop - contentEl.clientHeight <= THINKING_ANCHOR_SLACK;
|
|
2071
2201
|
const streaming = segment.isStreaming !== false;
|
|
@@ -2075,56 +2205,106 @@ const thinkingRenderer = {
|
|
|
2075
2205
|
},
|
|
2076
2206
|
getStyles: () => ``
|
|
2077
2207
|
};
|
|
2208
|
+
const HIGHLIGHT_DEBOUNCE_MS = 400;
|
|
2209
|
+
const MAX_THROTTLE_ENTRIES = 256;
|
|
2210
|
+
const _lastHighlightAt = /* @__PURE__ */ new Map();
|
|
2211
|
+
function markThrottle(map, id, at) {
|
|
2212
|
+
map.delete(id);
|
|
2213
|
+
if (map.size >= MAX_THROTTLE_ENTRIES) {
|
|
2214
|
+
const oldest = map.keys().next().value;
|
|
2215
|
+
if (oldest !== void 0) map.delete(oldest);
|
|
2216
|
+
}
|
|
2217
|
+
map.set(id, at);
|
|
2218
|
+
}
|
|
2219
|
+
function highlightedLen(pane) {
|
|
2220
|
+
const n = Number(pane.dataset.aparteHlLen);
|
|
2221
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
2222
|
+
}
|
|
2223
|
+
function streamHighlight(element, paneSelector, content, lang, segId) {
|
|
2224
|
+
const pane = element.querySelector(paneSelector);
|
|
2225
|
+
if (!pane) return;
|
|
2226
|
+
const tail = pane.querySelector("[data-aparte-tail]");
|
|
2227
|
+
const hlLen = highlightedLen(pane);
|
|
2228
|
+
if (tail && hlLen <= content.length) {
|
|
2229
|
+
tail.textContent = content.slice(hlLen);
|
|
2230
|
+
} else {
|
|
2231
|
+
delete pane.dataset.aparteHlLen;
|
|
2232
|
+
const codeEl = pane.querySelector("code");
|
|
2233
|
+
if (codeEl) codeEl.textContent = content;
|
|
2234
|
+
else pane.innerHTML = `<pre><code class="language-${escapeHtml(lang || "text")}">${escapeHtml(content)}</code></pre>`;
|
|
2235
|
+
}
|
|
2236
|
+
const cut = content.lastIndexOf("\n") + 1;
|
|
2237
|
+
if (cut <= hlLen) return;
|
|
2238
|
+
const now = Date.now();
|
|
2239
|
+
if (now - (_lastHighlightAt.get(segId) ?? 0) < HIGHLIGHT_DEBOUNCE_MS) return;
|
|
2240
|
+
markThrottle(_lastHighlightAt, segId, now);
|
|
2241
|
+
void contextConfig(element).highlightCode(content.slice(0, cut), lang).then((html) => {
|
|
2242
|
+
const live = element.querySelector(paneSelector);
|
|
2243
|
+
if (!live) return;
|
|
2244
|
+
if (cut <= highlightedLen(live)) return;
|
|
2245
|
+
live.innerHTML = html;
|
|
2246
|
+
const codeEl = live.querySelector("code") ?? live;
|
|
2247
|
+
const span = document.createElement("span");
|
|
2248
|
+
span.dataset.aparteTail = "";
|
|
2249
|
+
span.textContent = content.slice(cut);
|
|
2250
|
+
codeEl.appendChild(span);
|
|
2251
|
+
live.dataset.aparteHlLen = String(cut);
|
|
2252
|
+
}).catch(() => {
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2078
2255
|
const codeRenderer = {
|
|
2079
2256
|
type: "code",
|
|
2080
2257
|
render: (segment) => `
|
|
2081
|
-
<div class="segment segment-code" data-segment-id="${escapeHtml(segment.id)}">
|
|
2082
|
-
<div class="code-header">
|
|
2083
|
-
${segment.filename ? `<span class="code-filename">${escapeHtml(segment.filename)}</span>` : `<span class="code-header-filler"></span>`}
|
|
2084
|
-
<span class="code-language">${escapeHtml(segment.language || "")}</span>
|
|
2085
|
-
<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"))}">
|
|
2086
2263
|
${contextConfig().getIcon("copy")}
|
|
2087
2264
|
</button>
|
|
2088
2265
|
</div>
|
|
2089
|
-
<div class="code-content-wrapper">
|
|
2266
|
+
<div class="aparte-code-content-wrapper">
|
|
2090
2267
|
<pre><code class="language-${escapeHtml(segment.language || "text")}">${escapeHtml(segment.content)}</code></pre>
|
|
2091
2268
|
</div>
|
|
2092
2269
|
</div>
|
|
2093
2270
|
`,
|
|
2271
|
+
/** The copy button's icon and tooltip — nothing else here comes from config. */
|
|
2272
|
+
relabel: (element) => {
|
|
2273
|
+
const copyBtn = element.querySelector(".aparte-code-copy");
|
|
2274
|
+
if (!copyBtn || copyBtn.dataset.copied) return;
|
|
2275
|
+
copyBtn.setAttribute("title", contextConfig().t("copy"));
|
|
2276
|
+
copyBtn.innerHTML = contextConfig().getIcon("copy");
|
|
2277
|
+
},
|
|
2094
2278
|
setup: (element, segment) => {
|
|
2095
|
-
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2279
|
+
const wrapper = element.querySelector(".aparte-code-content-wrapper");
|
|
2096
2280
|
if (wrapper) {
|
|
2097
2281
|
void contextConfig().highlightCode(segment.content, segment.language || "").then((html) => {
|
|
2098
2282
|
wrapper.innerHTML = html;
|
|
2099
2283
|
}).catch(() => {
|
|
2100
2284
|
});
|
|
2101
2285
|
}
|
|
2102
|
-
const copyBtn = element.querySelector(".code-copy");
|
|
2286
|
+
const copyBtn = element.querySelector(".aparte-code-copy");
|
|
2103
2287
|
if (copyBtn) {
|
|
2104
2288
|
copyBtn.addEventListener("click", () => {
|
|
2105
|
-
const source = element.querySelector(".code-content-wrapper")?.textContent ?? "";
|
|
2289
|
+
const source = element.querySelector(".aparte-code-content-wrapper")?.textContent ?? "";
|
|
2106
2290
|
void navigator.clipboard.writeText(source).catch(() => {
|
|
2107
2291
|
});
|
|
2108
2292
|
copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
|
|
2109
2293
|
copyBtn.setAttribute("title", contextConfig(copyBtn).t("copied"));
|
|
2294
|
+
copyBtn.dataset.copied = "1";
|
|
2110
2295
|
setTimeout(() => {
|
|
2111
2296
|
copyBtn.innerHTML = contextConfig(copyBtn).getIcon("copy");
|
|
2112
2297
|
copyBtn.setAttribute("title", contextConfig(copyBtn).t("copy"));
|
|
2298
|
+
delete copyBtn.dataset.copied;
|
|
2113
2299
|
}, 1500);
|
|
2114
2300
|
});
|
|
2115
2301
|
}
|
|
2116
2302
|
},
|
|
2117
2303
|
update: (element, segment) => {
|
|
2118
2304
|
if (segment.isStreaming) {
|
|
2119
|
-
|
|
2120
|
-
if (codeEl) {
|
|
2121
|
-
codeEl.textContent = segment.content;
|
|
2122
|
-
} else {
|
|
2123
|
-
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2124
|
-
if (wrapper) wrapper.innerHTML = `<pre><code class="language-${escapeHtml(segment.language || "text")}">${escapeHtml(segment.content)}</code></pre>`;
|
|
2125
|
-
}
|
|
2305
|
+
streamHighlight(element, ".aparte-code-content-wrapper", segment.content, segment.language || "", segment.id);
|
|
2126
2306
|
} else {
|
|
2127
|
-
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2307
|
+
const wrapper = element.querySelector(".aparte-code-content-wrapper");
|
|
2128
2308
|
if (wrapper) {
|
|
2129
2309
|
void contextConfig().highlightCode(segment.content, segment.language || "").then((html) => {
|
|
2130
2310
|
wrapper.innerHTML = html;
|
|
@@ -2135,58 +2315,6 @@ const codeRenderer = {
|
|
|
2135
2315
|
},
|
|
2136
2316
|
getStyles: () => ``
|
|
2137
2317
|
};
|
|
2138
|
-
const terminalRenderer = {
|
|
2139
|
-
type: "terminal",
|
|
2140
|
-
render: (segment) => `
|
|
2141
|
-
<div class="segment segment-terminal" data-segment-id="${escapeHtml(segment.id)}">
|
|
2142
|
-
<div class="terminal-command-block">
|
|
2143
|
-
<div class="terminal-icon">
|
|
2144
|
-
${contextConfig().getIcon("terminal")}
|
|
2145
|
-
</div>
|
|
2146
|
-
<code class="terminal-command">${escapeHtml(segment.command || "")}</code>
|
|
2147
|
-
<div class="terminal-actions">
|
|
2148
|
-
${segment.isRunning ? `<span class="terminal-running"><span class="spinner"></span>${contextConfig().t("running")}</span>` : contextConfig().getHostHandlers().terminalRun ? `<button class="terminal-run-btn" data-action="run" aria-label="${escapeAttr(contextConfig().t("run"))}" title="${escapeAttr(contextConfig().t("run"))}">${contextConfig().t("run")}</button>` : ""}
|
|
2149
|
-
<button class="terminal-copy-btn" data-action="copy" aria-label="${escapeAttr(contextConfig().t("copy"))}" title="${escapeAttr(contextConfig().t("copy"))}">
|
|
2150
|
-
${contextConfig().getIcon("copy")}
|
|
2151
|
-
</button>
|
|
2152
|
-
</div>
|
|
2153
|
-
</div>
|
|
2154
|
-
${segment.output ? `<div class="terminal-output">${escapeHtml(segment.output)}</div>` : ""}
|
|
2155
|
-
${segment.exitCode !== void 0 && segment.exitCode !== 0 ? `<div class="terminal-error">Command failed with exit code ${segment.exitCode}</div>` : ""}
|
|
2156
|
-
</div>
|
|
2157
|
-
`,
|
|
2158
|
-
setup: (element, segment) => {
|
|
2159
|
-
const copyBtn = element.querySelector(".terminal-copy-btn");
|
|
2160
|
-
const command = element.querySelector(".terminal-command");
|
|
2161
|
-
if (copyBtn && command) {
|
|
2162
|
-
copyBtn.addEventListener("click", () => {
|
|
2163
|
-
void navigator.clipboard.writeText(command.textContent || "").catch(() => {
|
|
2164
|
-
});
|
|
2165
|
-
copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
|
|
2166
|
-
copyBtn.setAttribute("title", contextConfig(copyBtn).t("copied"));
|
|
2167
|
-
setTimeout(() => {
|
|
2168
|
-
copyBtn.innerHTML = contextConfig(copyBtn).getIcon("copy");
|
|
2169
|
-
copyBtn.setAttribute("title", contextConfig(copyBtn).t("copy"));
|
|
2170
|
-
}, 1500);
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
2173
|
-
const runBtn = element.querySelector(".terminal-run-btn");
|
|
2174
|
-
if (runBtn) {
|
|
2175
|
-
runBtn.addEventListener("click", () => {
|
|
2176
|
-
element.dispatchEvent(new CustomEvent("aparte-terminal-run", {
|
|
2177
|
-
bubbles: true,
|
|
2178
|
-
composed: true,
|
|
2179
|
-
detail: {
|
|
2180
|
-
segmentId: segment.id,
|
|
2181
|
-
messageId: segment.messageId,
|
|
2182
|
-
command: command?.textContent || ""
|
|
2183
|
-
}
|
|
2184
|
-
}));
|
|
2185
|
-
});
|
|
2186
|
-
}
|
|
2187
|
-
},
|
|
2188
|
-
getStyles: () => ``
|
|
2189
|
-
};
|
|
2190
2318
|
const errorRenderer = {
|
|
2191
2319
|
type: "error",
|
|
2192
2320
|
render: (segment) => {
|
|
@@ -2200,173 +2328,239 @@ const errorRenderer = {
|
|
|
2200
2328
|
return out;
|
|
2201
2329
|
}
|
|
2202
2330
|
return `
|
|
2203
|
-
<div class="segment segment-error" data-segment-id="${escapeHtml(segment.id)}">
|
|
2204
|
-
<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">
|
|
2205
2333
|
${contextConfig().getIcon("error") || "⚠"}
|
|
2206
2334
|
</div>
|
|
2207
|
-
<div class="error-content">
|
|
2208
|
-
<div class="error-title"
|
|
2209
|
-
<div class="error-message">${escapeHtml(segment.content)}</div>
|
|
2210
|
-
${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>` : ""}
|
|
2211
2339
|
</div>
|
|
2212
2340
|
</div>
|
|
2213
2341
|
`;
|
|
2214
2342
|
},
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2343
|
+
/**
|
|
2344
|
+
* The icon and the heading. The heading used to be the hardcoded literal `Error`
|
|
2345
|
+
* while `locale.error` — a REQUIRED key, documented, and already translated to
|
|
2346
|
+
* "Erreur" — was read by nothing at all. A translated string with no consumer and
|
|
2347
|
+
* a literal with no translation, in the same card.
|
|
2348
|
+
*
|
|
2349
|
+
* Not the message: that is the model's or the transport's text, in whatever
|
|
2350
|
+
* language it arrived in. Relabelling it would be inventing content.
|
|
2351
|
+
*/
|
|
2352
|
+
relabel: (element) => {
|
|
2353
|
+
const wrap = element.querySelector(".aparte-error-icon-wrapper");
|
|
2354
|
+
if (wrap) wrap.innerHTML = contextConfig().getIcon("error") || "⚠";
|
|
2355
|
+
const title = element.querySelector(".aparte-error-title");
|
|
2356
|
+
if (title) title.textContent = contextConfig().t("error");
|
|
2223
2357
|
},
|
|
2224
2358
|
getStyles: () => ``
|
|
2225
2359
|
};
|
|
2226
|
-
function
|
|
2227
|
-
const
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
html += renderFileNode(child, depth + 1);
|
|
2234
|
-
}
|
|
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 "";
|
|
2235
2367
|
}
|
|
2236
|
-
return html;
|
|
2237
2368
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
};
|
|
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
|
+
}
|
|
2251
2433
|
const toolCallRenderer = {
|
|
2252
2434
|
type: "tool_call",
|
|
2253
2435
|
render: (segment) => {
|
|
2254
2436
|
const name = segment.toolCall?.name ?? "tool";
|
|
2255
2437
|
const status = segment.status ?? "pending";
|
|
2256
2438
|
const toolCallId = segment.toolCall?.id ?? "";
|
|
2257
|
-
if (status === "awaiting-approval") {
|
|
2258
|
-
const loc = contextConfig().getLocale();
|
|
2259
|
-
const approve = loc.approveTool ?? "Approve";
|
|
2260
|
-
const reject = loc.rejectTool ?? "Reject";
|
|
2261
|
-
return `
|
|
2262
|
-
<div class="segment segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="awaiting-approval" data-tool-call-id="${escapeAttr(toolCallId)}">
|
|
2263
|
-
<span class="tool-pill">
|
|
2264
|
-
<span class="tool-pill-icon">${contextConfig().getIcon("tool")}</span>
|
|
2265
|
-
<span class="tool-pill-name">${escapeHtml(name)}</span>
|
|
2266
|
-
</span>
|
|
2267
|
-
<span class="tool-approval" role="group" aria-label="${escapeAttr(name)}">
|
|
2268
|
-
<button type="button" class="tool-approve-btn" data-tool-decision="approve" aria-label="${escapeAttr(approve)}">${escapeHtml(approve)}</button>
|
|
2269
|
-
<button type="button" class="tool-reject-btn" data-tool-decision="reject" aria-label="${escapeAttr(reject)}">${escapeHtml(reject)}</button>
|
|
2270
|
-
</span>
|
|
2271
|
-
</div>
|
|
2272
|
-
`;
|
|
2273
|
-
}
|
|
2274
2439
|
const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
|
|
2275
2440
|
if (customRenderer) {
|
|
2276
2441
|
const out = customRenderer.render(segment);
|
|
2277
2442
|
if (out) return out;
|
|
2278
2443
|
}
|
|
2279
|
-
const
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
<span class="tool-pill-name">${escapeHtml(name)}</span>
|
|
2286
|
-
${spinner}
|
|
2287
|
-
${statusIcon ? `<span class="tool-pill-status">${statusIcon}</span>` : ""}
|
|
2288
|
-
</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}
|
|
2289
2450
|
</div>
|
|
2290
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
|
+
`;
|
|
2291
2471
|
},
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2472
|
+
/**
|
|
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.
|
|
2479
|
+
*
|
|
2480
|
+
* No child node is added or removed, per the relabel contract.
|
|
2481
|
+
*/
|
|
2482
|
+
relabel: (element, segment) => {
|
|
2483
|
+
const cfg = contextConfig();
|
|
2484
|
+
const icon = element.querySelector(".aparte-tool-icon");
|
|
2485
|
+
if (icon) icon.innerHTML = cfg.getIcon("tool");
|
|
2486
|
+
const statusEl = element.querySelector(".aparte-tool-state");
|
|
2487
|
+
if (statusEl) {
|
|
2488
|
+
const s = segment.status;
|
|
2489
|
+
statusEl.innerHTML = s === "resolved" ? cfg.getIcon("check") : s === "aborted" || s === "rejected" ? cfg.getIcon("close") : "";
|
|
2490
|
+
}
|
|
2491
|
+
if (statusEl && segment.status === "awaiting-approval") {
|
|
2492
|
+
statusEl.textContent = cfg.t("approvalWaiting");
|
|
2493
|
+
}
|
|
2494
|
+
},
|
|
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
|
+
}
|
|
2304
2522
|
return;
|
|
2305
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);
|
|
2306
2534
|
const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
|
|
2307
2535
|
customRenderer?.setup?.(element, segment);
|
|
2308
2536
|
},
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
align-items: center;
|
|
2314
|
-
gap: 5px;
|
|
2315
|
-
padding: 3px 10px 3px 7px;
|
|
2316
|
-
border-radius: 99px;
|
|
2317
|
-
font-size: 0.78rem;
|
|
2318
|
-
font-weight: 500;
|
|
2319
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
2320
|
-
background: var(--aparte-surface, #f8f8f8);
|
|
2321
|
-
color: var(--aparte-text-secondary, rgba(0,0,0,0.55));
|
|
2322
|
-
user-select: none;
|
|
2323
|
-
}
|
|
2324
|
-
[data-status="resolved"] .tool-pill {
|
|
2325
|
-
border-color: var(--aparte-success-border, rgba(34,197,94,0.3));
|
|
2326
|
-
background: var(--aparte-success-surface, rgba(34,197,94,0.06));
|
|
2327
|
-
color: var(--aparte-success, rgb(21,128,61));
|
|
2328
|
-
}
|
|
2329
|
-
[data-status="aborted"] .tool-pill,
|
|
2330
|
-
[data-status="rejected"] .tool-pill {
|
|
2331
|
-
border-color: var(--aparte-error-border, rgba(239,68,68,0.3));
|
|
2332
|
-
background: var(--aparte-error-surface, rgba(239,68,68,0.06));
|
|
2333
|
-
color: var(--aparte-error, rgb(185,28,28));
|
|
2334
|
-
}
|
|
2335
|
-
.tool-approval { display: inline-flex; gap: 6px; margin-left: 8px; vertical-align: middle; }
|
|
2336
|
-
.tool-approve-btn, .tool-reject-btn {
|
|
2337
|
-
font: inherit; font-size: 0.78rem; font-weight: 600; line-height: 1;
|
|
2338
|
-
padding: 4px 12px; border-radius: 99px; cursor: pointer;
|
|
2339
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
2340
|
-
background: var(--aparte-surface, #f8f8f8);
|
|
2341
|
-
}
|
|
2342
|
-
.tool-approve-btn { color: var(--aparte-success, rgb(21,128,61)); border-color: var(--aparte-success-border, rgba(34,197,94,0.4)); }
|
|
2343
|
-
.tool-reject-btn { color: var(--aparte-error, rgb(185,28,28)); border-color: var(--aparte-error-border, rgba(239,68,68,0.4)); }
|
|
2344
|
-
.tool-approve-btn:hover { background: var(--aparte-success-surface, rgba(34,197,94,0.1)); }
|
|
2345
|
-
.tool-reject-btn:hover { background: var(--aparte-error-surface, rgba(239,68,68,0.1)); }
|
|
2346
|
-
.tool-pill-spinner {
|
|
2347
|
-
width: 10px; height: 10px;
|
|
2348
|
-
border: 1.5px solid currentColor;
|
|
2349
|
-
border-top-color: transparent;
|
|
2350
|
-
border-radius: 50%;
|
|
2351
|
-
display: inline-block;
|
|
2352
|
-
animation: tool-spin 0.7s linear infinite;
|
|
2353
|
-
}
|
|
2354
|
-
.tool-pill-status { font-size: 0.75rem; }
|
|
2355
|
-
@keyframes tool-spin { to { transform: rotate(360deg); } }
|
|
2356
|
-
`
|
|
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: () => ""
|
|
2357
2541
|
};
|
|
2542
|
+
const waitingLabel = () => contextConfig().t("generating");
|
|
2358
2543
|
const pipelineWaitingRenderer = {
|
|
2359
2544
|
type: "pipeline-waiting",
|
|
2360
2545
|
render: (segment) => {
|
|
2361
2546
|
return `
|
|
2362
|
-
<div class="segment segment-pipeline-waiting" data-segment-id="${escapeHtml(segment.id)}" aria-label="
|
|
2363
|
-
<span class="pw-dot"></span>
|
|
2364
|
-
<span class="pw-dot"></span>
|
|
2365
|
-
<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>
|
|
2366
2551
|
</div>`;
|
|
2367
2552
|
},
|
|
2368
2553
|
update: () => {
|
|
2369
2554
|
},
|
|
2555
|
+
/**
|
|
2556
|
+
* Three dots and an accessible name. The dots are CSS; the name is the whole
|
|
2557
|
+
* content as far as a screen reader is concerned, and it was hardcoded English in
|
|
2558
|
+
* every locale — the one string in this library where being untranslated is
|
|
2559
|
+
* invisible to everyone who can see the screen.
|
|
2560
|
+
*/
|
|
2561
|
+
relabel: (element) => {
|
|
2562
|
+
element.setAttribute("aria-label", waitingLabel());
|
|
2563
|
+
},
|
|
2370
2564
|
setup: (el2) => {
|
|
2371
2565
|
const parent = el2.parentElement;
|
|
2372
2566
|
if (!parent) return;
|
|
@@ -2378,30 +2572,10 @@ const pipelineWaitingRenderer = {
|
|
|
2378
2572
|
});
|
|
2379
2573
|
observer.observe(parent, { childList: true });
|
|
2380
2574
|
},
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
gap: 5px;
|
|
2386
|
-
padding: 6px 2px;
|
|
2387
|
-
min-height: 28px;
|
|
2388
|
-
}
|
|
2389
|
-
.pw-dot {
|
|
2390
|
-
display: inline-block;
|
|
2391
|
-
width: 8px;
|
|
2392
|
-
height: 8px;
|
|
2393
|
-
border-radius: 50%;
|
|
2394
|
-
background: var(--aparte-text-muted, #6b7280);
|
|
2395
|
-
opacity: 0.3;
|
|
2396
|
-
animation: pw-pulse 1.2s ease-in-out infinite;
|
|
2397
|
-
}
|
|
2398
|
-
.pw-dot:nth-child(2) { animation-delay: 0.2s; }
|
|
2399
|
-
.pw-dot:nth-child(3) { animation-delay: 0.4s; }
|
|
2400
|
-
@keyframes pw-pulse {
|
|
2401
|
-
0%, 80%, 100% { opacity: 0.3; transform: scale(0.85); }
|
|
2402
|
-
40% { opacity: 1; transform: scale(1.1); }
|
|
2403
|
-
}
|
|
2404
|
-
`
|
|
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: () => ""
|
|
2405
2579
|
};
|
|
2406
2580
|
function stripCodeFences(content) {
|
|
2407
2581
|
let s = content;
|
|
@@ -2451,26 +2625,6 @@ function findClosingFence(s) {
|
|
|
2451
2625
|
}
|
|
2452
2626
|
return -1;
|
|
2453
2627
|
}
|
|
2454
|
-
const _lastHighlightAt = /* @__PURE__ */ new Map();
|
|
2455
|
-
function markThrottle(map, id, at) {
|
|
2456
|
-
map.delete(id);
|
|
2457
|
-
if (map.size >= MAX_THROTTLE_ENTRIES) {
|
|
2458
|
-
const oldest = map.keys().next().value;
|
|
2459
|
-
if (oldest !== void 0) map.delete(oldest);
|
|
2460
|
-
}
|
|
2461
|
-
map.set(id, at);
|
|
2462
|
-
}
|
|
2463
|
-
function debounceHighlight(element, paneSelector, content, lang, segId) {
|
|
2464
|
-
const now = Date.now();
|
|
2465
|
-
const last = _lastHighlightAt.get(segId) ?? 0;
|
|
2466
|
-
if (now - last < HIGHLIGHT_DEBOUNCE_MS) return;
|
|
2467
|
-
markThrottle(_lastHighlightAt, segId, now);
|
|
2468
|
-
void contextConfig(element).highlightCode(content, lang).then((html) => {
|
|
2469
|
-
const wrapper = element.querySelector(paneSelector);
|
|
2470
|
-
if (wrapper) wrapper.innerHTML = html;
|
|
2471
|
-
}).catch(() => {
|
|
2472
|
-
});
|
|
2473
|
-
}
|
|
2474
2628
|
function labelForKind(kind) {
|
|
2475
2629
|
switch (kind) {
|
|
2476
2630
|
case "react":
|
|
@@ -2509,8 +2663,6 @@ function labelForKind(kind) {
|
|
|
2509
2663
|
return "Artifact";
|
|
2510
2664
|
}
|
|
2511
2665
|
}
|
|
2512
|
-
const MAX_THROTTLE_ENTRIES = 256;
|
|
2513
|
-
const HIGHLIGHT_DEBOUNCE_MS = 400;
|
|
2514
2666
|
const PREVIEW_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:";
|
|
2515
2667
|
function withMetaCsp(doc) {
|
|
2516
2668
|
const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttr(PREVIEW_CSP)}">`;
|
|
@@ -2527,9 +2679,18 @@ function buildPreviewBody(kind, body, title) {
|
|
|
2527
2679
|
if (startsWithDoctype(body)) return body;
|
|
2528
2680
|
return `<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>${escapeAttr(title)}</title></head><body>${body}</body></html>`;
|
|
2529
2681
|
}
|
|
2682
|
+
// An SVG with only a `viewBox` — the recommended, responsive form, and the one
|
|
2683
|
+
// a model writes most often — has NO intrinsic size. As a flex item its cross
|
|
2684
|
+
// size then collapses to zero and the frame is blank: the preview worked for an
|
|
2685
|
+
// SVG that declared `width`/`height` and silently showed nothing for the
|
|
2686
|
+
// idiomatic one. Reported from the landing the moment its demo payload became a
|
|
2687
|
+
// real file instead of a hand-written chart with dimensions on it.
|
|
2688
|
+
//
|
|
2689
|
+
// The rule is narrowed by attribute selector rather than applied to every SVG,
|
|
2690
|
+
// so an SVG that DOES state its size keeps the size it asked for.
|
|
2530
2691
|
case "svg":
|
|
2531
2692
|
return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
|
|
2532
|
-
<style>html,body{margin:0;height:100%;display:flex;align-items:center;justify-content:center;background:#fff}svg{max-width:90%;max-height:90%}</style>
|
|
2693
|
+
<style>html,body{margin:0;height:100%;display:flex;align-items:center;justify-content:center;background:#fff}svg{max-width:90%;max-height:90%}svg:not([width]):not([height]){width:90%;height:90%}</style>
|
|
2533
2694
|
</head><body>${body}</body></html>`;
|
|
2534
2695
|
case "js": {
|
|
2535
2696
|
const safeBody = escapeClosingScriptTag(body);
|
|
@@ -2612,11 +2773,12 @@ function renderBinaryFileArtifact(segment, kind) {
|
|
|
2612
2773
|
const iconLabel = FILE_ICON_LABEL[kind] ?? kind.toUpperCase();
|
|
2613
2774
|
const isStreaming = !!segment.isStreaming;
|
|
2614
2775
|
const canRedownload = contextConfig().getHostHandlers().artifactRedownload;
|
|
2776
|
+
const downloadLabel = escapeHtml(contextConfig().t("download"));
|
|
2615
2777
|
const cached = _binaryArtifactCache.get(segment.id);
|
|
2616
2778
|
if (cached && !isStreaming) {
|
|
2617
2779
|
const previewBody = cached.previewHtml ? contextConfig().sanitizeHtml(cached.previewHtml) : `<div class="aparte-art-file__preview-empty">Preview not available for ${escapeHtml(kind)} yet</div>`;
|
|
2618
2780
|
return `
|
|
2619
|
-
<div class="segment segment-artifact-file"
|
|
2781
|
+
<div class="aparte-segment aparte-segment-artifact-file"
|
|
2620
2782
|
data-segment-id="${escapeHtml(segment.id)}"
|
|
2621
2783
|
data-artifact-type="${escapeHtml(kind)}"
|
|
2622
2784
|
data-state="ready">
|
|
@@ -2627,7 +2789,7 @@ function renderBinaryFileArtifact(segment, kind) {
|
|
|
2627
2789
|
<div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(formatBytes(cached.bytes))} · ${escapeHtml(kind.toUpperCase())}</div>
|
|
2628
2790
|
</div>
|
|
2629
2791
|
<div class="aparte-art-file__actions">
|
|
2630
|
-
${canRedownload ?
|
|
2792
|
+
${canRedownload ? `<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download">${downloadLabel}</button>` : ""}
|
|
2631
2793
|
</div>
|
|
2632
2794
|
</div>
|
|
2633
2795
|
<div class="aparte-art-file__body">
|
|
@@ -2640,9 +2802,9 @@ function renderBinaryFileArtifact(segment, kind) {
|
|
|
2640
2802
|
`;
|
|
2641
2803
|
}
|
|
2642
2804
|
const cleanContent = stripCodeFences(segment.content || "");
|
|
2643
|
-
const subText = isStreaming ? "
|
|
2805
|
+
const subText = isStreaming ? contextConfig().t("generating") : contextConfig().t("rebuildingPreview");
|
|
2644
2806
|
return `
|
|
2645
|
-
<div class="segment segment-artifact-file"
|
|
2807
|
+
<div class="aparte-segment aparte-segment-artifact-file"
|
|
2646
2808
|
data-segment-id="${escapeHtml(segment.id)}"
|
|
2647
2809
|
data-artifact-type="${escapeHtml(kind)}"
|
|
2648
2810
|
data-state="${isStreaming ? "streaming" : "compiling"}">
|
|
@@ -2653,7 +2815,7 @@ function renderBinaryFileArtifact(segment, kind) {
|
|
|
2653
2815
|
<div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(subText)}</div>
|
|
2654
2816
|
</div>
|
|
2655
2817
|
<div class="aparte-art-file__actions">
|
|
2656
|
-
${canRedownload ?
|
|
2818
|
+
${canRedownload ? `<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download" disabled>${downloadLabel}</button>` : ""}
|
|
2657
2819
|
</div>
|
|
2658
2820
|
</div>
|
|
2659
2821
|
<div class="aparte-art-file__body">
|
|
@@ -2759,12 +2921,11 @@ function updateBinaryFileArtifact(element, segment, isStreaming) {
|
|
|
2759
2921
|
const state = element.getAttribute("data-state");
|
|
2760
2922
|
if (state === "ready") return;
|
|
2761
2923
|
const cleanContent = stripCodeFences(segment.content || "");
|
|
2762
|
-
const codeEl = element.querySelector('[data-role="code-pane"] code');
|
|
2763
|
-
if (codeEl) {
|
|
2764
|
-
codeEl.textContent = cleanContent;
|
|
2765
|
-
}
|
|
2766
2924
|
if (isStreaming) {
|
|
2767
|
-
|
|
2925
|
+
streamHighlight(element, '[data-role="code-pane"]', cleanContent, "js", segment.id);
|
|
2926
|
+
} else {
|
|
2927
|
+
const codeEl = element.querySelector('[data-role="code-pane"] code');
|
|
2928
|
+
if (codeEl) codeEl.textContent = cleanContent;
|
|
2768
2929
|
}
|
|
2769
2930
|
if (!isStreaming && state === "streaming") {
|
|
2770
2931
|
element.setAttribute("data-state", "compiling");
|
|
@@ -2811,9 +2972,9 @@ function showSandboxError(element, phase, errorMsg) {
|
|
|
2811
2972
|
const short = (errorMsg.split("\n")[0] ?? "").slice(0, 240);
|
|
2812
2973
|
body.innerHTML = `
|
|
2813
2974
|
<div class="aparte-art-file__error">
|
|
2814
|
-
<div class="aparte-art-file__error-title"
|
|
2975
|
+
<div class="aparte-art-file__error-title">${escapeHtml(contextConfig().t("sandboxError"))}</div>
|
|
2815
2976
|
<div class="aparte-art-file__error-msg">${escapeHtml(short)}</div>
|
|
2816
|
-
<div class="aparte-art-file__error-hint"
|
|
2977
|
+
<div class="aparte-art-file__error-hint">${escapeHtml(contextConfig().t("sandboxErrorHint"))}</div>
|
|
2817
2978
|
</div>
|
|
2818
2979
|
`;
|
|
2819
2980
|
}
|
|
@@ -2831,10 +2992,14 @@ const artifactRenderer = {
|
|
|
2831
2992
|
const displayLang = languageForKind(kind);
|
|
2832
2993
|
const isStreaming = !!segment.isStreaming;
|
|
2833
2994
|
const previewable = PREVIEWABLE_KINDS.has(kind);
|
|
2995
|
+
const cfg = contextConfig();
|
|
2996
|
+
const downloadLabel = cfg.t("download");
|
|
2997
|
+
const previewLabel = cfg.t("preview");
|
|
2998
|
+
const codeLabel = cfg.t("code");
|
|
2834
2999
|
const isBinary = BINARY_FILE_KINDS.has(kind);
|
|
2835
3000
|
const cleanContent = stripCodeFences(segment.content || "");
|
|
2836
3001
|
return `
|
|
2837
|
-
<div class="segment segment-artifact-card"
|
|
3002
|
+
<div class="aparte-segment aparte-segment-artifact-card"
|
|
2838
3003
|
data-segment-id="${escapeHtml(segment.id)}"
|
|
2839
3004
|
data-artifact-type="${escapeHtml(kind)}"
|
|
2840
3005
|
data-streaming="${isStreaming ? "true" : "false"}"
|
|
@@ -2848,33 +3013,64 @@ const artifactRenderer = {
|
|
|
2848
3013
|
${isStreaming ? '<span class="aparte-art-card__pulse" aria-label="Streaming"></span>' : ""}
|
|
2849
3014
|
</div>
|
|
2850
3015
|
<div class="aparte-art-card__actions">
|
|
2851
|
-
<button type="button" class="aparte-art-card__btn" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}" aria-label="
|
|
3016
|
+
<button type="button" class="aparte-art-card__btn" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}" aria-label="${escapeAttr(contextConfig().t("copy"))}">
|
|
2852
3017
|
${contextConfig().getIcon("copy")}
|
|
2853
3018
|
</button>
|
|
2854
|
-
<button type="button" class="aparte-art-card__btn" data-action="download" title="
|
|
3019
|
+
<button type="button" class="aparte-art-card__btn" data-action="download" title="${escapeAttr(downloadLabel)}" aria-label="${escapeAttr(downloadLabel)}" ${isStreaming ? "disabled" : ""}>
|
|
2855
3020
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v9m0 0l-3-3m3 3l3-3M2 13h12"/></svg>
|
|
2856
3021
|
</button>
|
|
2857
3022
|
</div>
|
|
2858
3023
|
</header>
|
|
2859
3024
|
<nav class="aparte-art-card__tabs" role="tablist">
|
|
2860
|
-
|
|
2861
|
-
|
|
3025
|
+
<button type="button" role="tab" data-tab-target="code" aria-selected="true">${escapeHtml(codeLabel)}</button>
|
|
3026
|
+
${previewable ? `<button type="button" role="tab" data-tab-target="preview" aria-selected="false" ${isStreaming ? "disabled" : ""}>${escapeHtml(previewLabel)}</button>` : ""}
|
|
2862
3027
|
</nav>
|
|
2863
3028
|
<div class="aparte-art-card__body">
|
|
2864
3029
|
<div class="aparte-art-card__pane" data-pane="code">
|
|
2865
|
-
<div class="code-content-wrapper">
|
|
3030
|
+
<div class="aparte-code-content-wrapper">
|
|
2866
3031
|
<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>
|
|
2867
3032
|
</div>
|
|
2868
3033
|
</div>
|
|
2869
3034
|
${previewable ? `
|
|
2870
3035
|
<div class="aparte-art-card__pane" data-pane="preview">
|
|
2871
|
-
<div class="aparte-art-card__pending"
|
|
3036
|
+
<div class="aparte-art-card__pending">${escapeHtml(cfg.t("previewPending"))}</div>
|
|
2872
3037
|
</div>
|
|
2873
3038
|
` : ""}
|
|
2874
3039
|
</div>
|
|
2875
3040
|
</div>
|
|
2876
3041
|
`;
|
|
2877
3042
|
},
|
|
3043
|
+
/**
|
|
3044
|
+
* Every string this card shows: the copy button's tooltip, glyph and accessible
|
|
3045
|
+
* name, the download button's title and label, and the two tab names. They were
|
|
3046
|
+
* all hardcoded literals until they got locale keys — including the copy button's
|
|
3047
|
+
* `aria-label`, which said "Copy" while its own `title` one attribute away already
|
|
3048
|
+
* went through `t('copy')`, so a French reader got a French tooltip and an English
|
|
3049
|
+
* announcement.
|
|
3050
|
+
*
|
|
3051
|
+
* Nothing here touches the tab state or the preview pane: a mounted iframe is
|
|
3052
|
+
* running model-authored code, and re-rendering this card is exactly what the
|
|
3053
|
+
* hook exists to avoid.
|
|
3054
|
+
*/
|
|
3055
|
+
relabel: (element) => {
|
|
3056
|
+
const cfg = contextConfig();
|
|
3057
|
+
const copyBtn = element.querySelector('.aparte-art-card__btn[data-action="copy"]');
|
|
3058
|
+
if (copyBtn) {
|
|
3059
|
+
copyBtn.setAttribute("title", cfg.t("copy"));
|
|
3060
|
+
copyBtn.setAttribute("aria-label", cfg.t("copy"));
|
|
3061
|
+
copyBtn.innerHTML = cfg.getIcon("copy");
|
|
3062
|
+
}
|
|
3063
|
+
const dl = element.querySelector('.aparte-art-card__btn[data-action="download"]');
|
|
3064
|
+
if (dl) {
|
|
3065
|
+
const label = cfg.t("download");
|
|
3066
|
+
dl.setAttribute("title", label);
|
|
3067
|
+
dl.setAttribute("aria-label", label);
|
|
3068
|
+
}
|
|
3069
|
+
const previewTab = element.querySelector('[data-tab-target="preview"]');
|
|
3070
|
+
if (previewTab) previewTab.textContent = cfg.t("preview");
|
|
3071
|
+
const codeTab = element.querySelector('[data-tab-target="code"]');
|
|
3072
|
+
if (codeTab) codeTab.textContent = cfg.t("code");
|
|
3073
|
+
},
|
|
2878
3074
|
setup: (element, segment) => {
|
|
2879
3075
|
latestSegment.set(element, segment);
|
|
2880
3076
|
const kind = (segment.artifactType || "").toLowerCase();
|
|
@@ -2882,7 +3078,7 @@ const artifactRenderer = {
|
|
|
2882
3078
|
setupBinaryFileArtifact(element, segment, kind);
|
|
2883
3079
|
return;
|
|
2884
3080
|
}
|
|
2885
|
-
const wrapper = element.querySelector(".code-content-wrapper");
|
|
3081
|
+
const wrapper = element.querySelector(".aparte-code-content-wrapper");
|
|
2886
3082
|
if (wrapper) {
|
|
2887
3083
|
const displayLang = languageForKind(kind);
|
|
2888
3084
|
const cleanContent = stripCodeFences(segment.content || "");
|
|
@@ -2955,23 +3151,25 @@ const artifactRenderer = {
|
|
|
2955
3151
|
}
|
|
2956
3152
|
const wasStreaming = element.getAttribute("data-streaming") === "true";
|
|
2957
3153
|
const cleanContent = stripCodeFences(segment.content || "");
|
|
2958
|
-
const codeEl = element.querySelector(".code-content-wrapper code");
|
|
2959
|
-
if (codeEl) {
|
|
2960
|
-
codeEl.textContent = cleanContent;
|
|
2961
|
-
} else {
|
|
2962
|
-
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2963
|
-
if (wrapper) {
|
|
2964
|
-
const displayLang = languageForKind(kind);
|
|
2965
|
-
wrapper.innerHTML = `<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>`;
|
|
2966
|
-
}
|
|
2967
|
-
}
|
|
2968
3154
|
if (isStreaming) {
|
|
2969
3155
|
const segId = element.getAttribute("data-segment-id") ?? segment.id;
|
|
2970
|
-
|
|
3156
|
+
streamHighlight(element, ".aparte-code-content-wrapper", cleanContent, languageForKind(kind), segId);
|
|
3157
|
+
} else {
|
|
3158
|
+
const codeEl = element.querySelector(".aparte-code-content-wrapper code");
|
|
3159
|
+
if (codeEl) {
|
|
3160
|
+
codeEl.textContent = cleanContent;
|
|
3161
|
+
} else {
|
|
3162
|
+
const wrapper = element.querySelector(".aparte-code-content-wrapper");
|
|
3163
|
+
if (wrapper) {
|
|
3164
|
+
const displayLang = languageForKind(kind);
|
|
3165
|
+
wrapper.innerHTML = `<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>`;
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
2971
3168
|
}
|
|
2972
3169
|
if (wasStreaming && !isStreaming) {
|
|
2973
3170
|
element.setAttribute("data-streaming", "false");
|
|
2974
|
-
|
|
3171
|
+
element.querySelector(".aparte-art-card__pulse")?.remove();
|
|
3172
|
+
const wrapper = element.querySelector(".aparte-code-content-wrapper");
|
|
2975
3173
|
if (wrapper) {
|
|
2976
3174
|
const displayLang = languageForKind(kind);
|
|
2977
3175
|
void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
|
|
@@ -2984,258 +3182,10 @@ const artifactRenderer = {
|
|
|
2984
3182
|
});
|
|
2985
3183
|
}
|
|
2986
3184
|
},
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
2992
|
-
border-radius: 12px;
|
|
2993
|
-
background: var(--aparte-surface, #fff);
|
|
2994
|
-
overflow: hidden;
|
|
2995
|
-
font: inherit;
|
|
2996
|
-
color: var(--aparte-text, inherit);
|
|
2997
|
-
}
|
|
2998
|
-
.aparte-art-card__header {
|
|
2999
|
-
display: flex; align-items: center; justify-content: space-between;
|
|
3000
|
-
padding: 8px 10px;
|
|
3001
|
-
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
3002
|
-
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
3003
|
-
min-height: 36px;
|
|
3004
|
-
}
|
|
3005
|
-
.aparte-art-card__title-block { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
3006
|
-
.aparte-art-card__kind {
|
|
3007
|
-
font-size: 0.7rem; font-weight: 600;
|
|
3008
|
-
text-transform: uppercase; letter-spacing: 0.04em;
|
|
3009
|
-
padding: 2px 6px; border-radius: 4px;
|
|
3010
|
-
background: var(--aparte-surface, #fff);
|
|
3011
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.1));
|
|
3012
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.6));
|
|
3013
|
-
}
|
|
3014
|
-
.aparte-art-card__title {
|
|
3015
|
-
font-size: 0.85rem; font-weight: 500;
|
|
3016
|
-
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
3017
|
-
}
|
|
3018
|
-
.aparte-art-card__pulse {
|
|
3019
|
-
width: 8px; height: 8px; border-radius: 50%;
|
|
3020
|
-
background: var(--aparte-accent, #3b82f6);
|
|
3021
|
-
animation: aparte-art-card-pulse 1.2s ease-in-out infinite;
|
|
3022
|
-
}
|
|
3023
|
-
@keyframes aparte-art-card-pulse {
|
|
3024
|
-
0%, 100% { opacity: 0.35; transform: scale(0.85); }
|
|
3025
|
-
50% { opacity: 1; transform: scale(1.1); }
|
|
3026
|
-
}
|
|
3027
|
-
.aparte-art-card__actions { display: flex; gap: 4px; }
|
|
3028
|
-
.aparte-art-card__btn {
|
|
3029
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
3030
|
-
width: 28px; height: 28px;
|
|
3031
|
-
background: transparent;
|
|
3032
|
-
border: 1px solid transparent;
|
|
3033
|
-
border-radius: 6px;
|
|
3034
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
3035
|
-
cursor: pointer;
|
|
3036
|
-
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
|
3037
|
-
}
|
|
3038
|
-
.aparte-art-card__btn:hover:not(:disabled) {
|
|
3039
|
-
background: var(--aparte-surface-hover, rgba(0,0,0,0.04));
|
|
3040
|
-
border-color: var(--aparte-border, rgba(0,0,0,0.1));
|
|
3041
|
-
color: var(--aparte-text, inherit);
|
|
3042
|
-
}
|
|
3043
|
-
.aparte-art-card__btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
3044
|
-
.aparte-art-card__tabs {
|
|
3045
|
-
display: flex; gap: 2px;
|
|
3046
|
-
padding: 4px 8px 0;
|
|
3047
|
-
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
3048
|
-
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
3049
|
-
}
|
|
3050
|
-
.aparte-art-card__tabs button {
|
|
3051
|
-
padding: 6px 12px;
|
|
3052
|
-
font-size: 0.78rem;
|
|
3053
|
-
background: transparent;
|
|
3054
|
-
border: 1px solid transparent;
|
|
3055
|
-
border-bottom: none;
|
|
3056
|
-
border-radius: 6px 6px 0 0;
|
|
3057
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.6));
|
|
3058
|
-
cursor: pointer;
|
|
3059
|
-
transition: background 0.12s, color 0.12s;
|
|
3060
|
-
}
|
|
3061
|
-
.aparte-art-card__tabs button[aria-selected="true"] {
|
|
3062
|
-
background: var(--aparte-surface, #fff);
|
|
3063
|
-
border-color: var(--aparte-border, rgba(0,0,0,0.08));
|
|
3064
|
-
color: var(--aparte-text, inherit);
|
|
3065
|
-
font-weight: 500;
|
|
3066
|
-
}
|
|
3067
|
-
.aparte-art-card__tabs button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
3068
|
-
.aparte-art-card__body {
|
|
3069
|
-
position: relative;
|
|
3070
|
-
min-height: 80px;
|
|
3071
|
-
max-height: 600px;
|
|
3072
|
-
overflow: hidden;
|
|
3073
|
-
}
|
|
3074
|
-
.aparte-art-card__pane { display: none; height: 100%; }
|
|
3075
|
-
.segment-artifact-card[data-tab="code"] .aparte-art-card__pane[data-pane="code"] { display: block; }
|
|
3076
|
-
.segment-artifact-card[data-tab="preview"] .aparte-art-card__pane[data-pane="preview"] { display: block; }
|
|
3077
|
-
.aparte-art-card__pane[data-pane="code"] {
|
|
3078
|
-
max-height: 600px; overflow: auto;
|
|
3079
|
-
}
|
|
3080
|
-
.aparte-art-card__pane[data-pane="code"] pre {
|
|
3081
|
-
margin: 0; padding: 12px;
|
|
3082
|
-
font-size: 0.82rem;
|
|
3083
|
-
background: var(--aparte-code-bg, #fafafa);
|
|
3084
|
-
}
|
|
3085
|
-
.aparte-art-card__frame {
|
|
3086
|
-
display: block;
|
|
3087
|
-
width: 100%; height: 480px;
|
|
3088
|
-
border: 0;
|
|
3089
|
-
background: #fff;
|
|
3090
|
-
}
|
|
3091
|
-
.aparte-art-card__pending {
|
|
3092
|
-
display: flex; align-items: center; justify-content: center;
|
|
3093
|
-
height: 120px;
|
|
3094
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.5));
|
|
3095
|
-
font-size: 0.85rem;
|
|
3096
|
-
font-style: italic;
|
|
3097
|
-
}
|
|
3098
|
-
/* ── Binary file artifact (xlsx/pdf/docx) ──────────────────── */
|
|
3099
|
-
.segment-artifact-file {
|
|
3100
|
-
display: flex; flex-direction: column;
|
|
3101
|
-
margin: 8px 0;
|
|
3102
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
3103
|
-
border-radius: 12px;
|
|
3104
|
-
background: var(--aparte-surface, #fff);
|
|
3105
|
-
overflow: hidden;
|
|
3106
|
-
color: var(--aparte-text, inherit);
|
|
3107
|
-
font: inherit;
|
|
3108
|
-
}
|
|
3109
|
-
.aparte-art-file__card {
|
|
3110
|
-
display: flex; align-items: center; gap: 12px;
|
|
3111
|
-
padding: 12px 14px;
|
|
3112
|
-
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
3113
|
-
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
3114
|
-
}
|
|
3115
|
-
.aparte-art-file__body {
|
|
3116
|
-
position: relative;
|
|
3117
|
-
}
|
|
3118
|
-
.aparte-art-file__code-pane {
|
|
3119
|
-
max-height: 360px; overflow: auto;
|
|
3120
|
-
background: var(--aparte-code-bg, #fafafa);
|
|
3121
|
-
}
|
|
3122
|
-
.aparte-art-file__code-pane pre {
|
|
3123
|
-
margin: 0; padding: 12px;
|
|
3124
|
-
font-size: 0.82rem;
|
|
3125
|
-
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
|
3126
|
-
}
|
|
3127
|
-
.aparte-art-file__preview-pane {
|
|
3128
|
-
max-height: 460px;
|
|
3129
|
-
overflow: auto;
|
|
3130
|
-
background: #fff;
|
|
3131
|
-
/* Preview is a document view — force light scheme regardless of
|
|
3132
|
-
the app theme, with a dark text colour so cells stay readable. */
|
|
3133
|
-
color: #1f2937;
|
|
3134
|
-
}
|
|
3135
|
-
.aparte-art-file__icon {
|
|
3136
|
-
width: 40px; height: 40px;
|
|
3137
|
-
border-radius: 8px;
|
|
3138
|
-
display: flex; align-items: center; justify-content: center;
|
|
3139
|
-
background: linear-gradient(135deg, #1d6f42, #0f5132);
|
|
3140
|
-
color: #fff;
|
|
3141
|
-
font-weight: 700;
|
|
3142
|
-
font-size: 0.78rem;
|
|
3143
|
-
letter-spacing: 0.04em;
|
|
3144
|
-
flex-shrink: 0;
|
|
3145
|
-
}
|
|
3146
|
-
.aparte-art-file__icon[data-kind="pdf"] { background: linear-gradient(135deg, #c0392b, #7d1f17); }
|
|
3147
|
-
.aparte-art-file__icon[data-kind="docx"] { background: linear-gradient(135deg, #1e5288, #0f3060); }
|
|
3148
|
-
.aparte-art-file__meta { flex: 1 1 auto; min-width: 0; }
|
|
3149
|
-
.aparte-art-file__meta-name {
|
|
3150
|
-
font-weight: 600; font-size: 0.92rem;
|
|
3151
|
-
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
3152
|
-
}
|
|
3153
|
-
.aparte-art-file__meta-sub {
|
|
3154
|
-
font-size: 0.78rem;
|
|
3155
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
3156
|
-
}
|
|
3157
|
-
.aparte-art-file__actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
3158
|
-
.aparte-art-file__btn {
|
|
3159
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
3160
|
-
background: var(--aparte-surface, #fff);
|
|
3161
|
-
color: var(--aparte-text, inherit);
|
|
3162
|
-
padding: 6px 10px;
|
|
3163
|
-
border-radius: 6px;
|
|
3164
|
-
font-size: 0.8rem;
|
|
3165
|
-
cursor: pointer;
|
|
3166
|
-
transition: background 0.12s ease;
|
|
3167
|
-
}
|
|
3168
|
-
.aparte-art-file__btn:hover { background: var(--aparte-surface-2, rgba(0,0,0,0.05)); }
|
|
3169
|
-
.aparte-art-file__btn--primary {
|
|
3170
|
-
background: var(--aparte-primary, #6366f1);
|
|
3171
|
-
color: #fff;
|
|
3172
|
-
border-color: var(--aparte-primary, #6366f1);
|
|
3173
|
-
}
|
|
3174
|
-
.aparte-art-file__btn--primary:hover { filter: brightness(1.07); }
|
|
3175
|
-
/* Hide the download button until the sandbox has produced the buffer.
|
|
3176
|
-
Avoids confusing the user with a disabled-but-styled-primary button
|
|
3177
|
-
during streaming / compiling. */
|
|
3178
|
-
.segment-artifact-file:not([data-state="ready"]) .aparte-art-file__btn[data-action="download"] {
|
|
3179
|
-
display: none;
|
|
3180
|
-
}
|
|
3181
|
-
.aparte-art-file__preview-pane table {
|
|
3182
|
-
border-collapse: collapse;
|
|
3183
|
-
width: 100%;
|
|
3184
|
-
font-size: 0.82rem;
|
|
3185
|
-
font-family: inherit;
|
|
3186
|
-
}
|
|
3187
|
-
.aparte-art-file__preview-pane th,
|
|
3188
|
-
.aparte-art-file__preview-pane td {
|
|
3189
|
-
border: 1px solid rgba(0,0,0,0.10);
|
|
3190
|
-
padding: 6px 10px;
|
|
3191
|
-
text-align: left;
|
|
3192
|
-
white-space: nowrap;
|
|
3193
|
-
color: #1f2937;
|
|
3194
|
-
background: #fff;
|
|
3195
|
-
}
|
|
3196
|
-
.aparte-art-file__preview-pane tr:nth-child(odd) td { background: #f9fafb; }
|
|
3197
|
-
.aparte-art-file__preview-pane tr:first-child td {
|
|
3198
|
-
background: #f3f4f6;
|
|
3199
|
-
font-weight: 600;
|
|
3200
|
-
position: sticky; top: 0;
|
|
3201
|
-
color: #111827;
|
|
3202
|
-
}
|
|
3203
|
-
.aparte-art-file__preview-empty {
|
|
3204
|
-
padding: 20px; text-align: center;
|
|
3205
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.5));
|
|
3206
|
-
font-size: 0.85rem; font-style: italic;
|
|
3207
|
-
}
|
|
3208
|
-
.segment-artifact-file[data-state="error"] .aparte-art-file__icon {
|
|
3209
|
-
background: linear-gradient(135deg, #c0392b, #7d1f17);
|
|
3210
|
-
}
|
|
3211
|
-
.aparte-art-file__error {
|
|
3212
|
-
padding: 16px 18px;
|
|
3213
|
-
background: rgba(239, 68, 68, 0.05);
|
|
3214
|
-
border-top: 1px solid rgba(239, 68, 68, 0.2);
|
|
3215
|
-
color: var(--aparte-text, inherit);
|
|
3216
|
-
}
|
|
3217
|
-
.aparte-art-file__error-title {
|
|
3218
|
-
font-weight: 600;
|
|
3219
|
-
font-size: 0.9rem;
|
|
3220
|
-
margin-bottom: 6px;
|
|
3221
|
-
color: #b91c1c;
|
|
3222
|
-
}
|
|
3223
|
-
.aparte-art-file__error-msg {
|
|
3224
|
-
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
|
3225
|
-
font-size: 0.78rem;
|
|
3226
|
-
padding: 6px 10px;
|
|
3227
|
-
background: rgba(0,0,0,0.04);
|
|
3228
|
-
border-radius: 4px;
|
|
3229
|
-
margin-bottom: 8px;
|
|
3230
|
-
color: #7f1d1d;
|
|
3231
|
-
word-break: break-word;
|
|
3232
|
-
}
|
|
3233
|
-
.aparte-art-file__error-hint {
|
|
3234
|
-
font-size: 0.78rem;
|
|
3235
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
3236
|
-
font-style: italic;
|
|
3237
|
-
}
|
|
3238
|
-
`
|
|
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: () => ""
|
|
3239
3189
|
};
|
|
3240
3190
|
function languageForKind(kind) {
|
|
3241
3191
|
if (kind === "react") return "jsx";
|
|
@@ -3385,10 +3335,7 @@ const DEFAULT_RENDERERS = [
|
|
|
3385
3335
|
textRenderer,
|
|
3386
3336
|
thinkingRenderer,
|
|
3387
3337
|
codeRenderer,
|
|
3388
|
-
terminalRenderer,
|
|
3389
3338
|
errorRenderer,
|
|
3390
|
-
progressRenderer,
|
|
3391
|
-
fileTreeRenderer,
|
|
3392
3339
|
toolCallRenderer,
|
|
3393
3340
|
artifactRenderer,
|
|
3394
3341
|
pipelineWaitingRenderer
|
|
@@ -3398,14 +3345,48 @@ const ACTIVITY_KEYS = ["content", "output", "result"];
|
|
|
3398
3345
|
function isActivity(updates) {
|
|
3399
3346
|
return ACTIVITY_KEYS.some((key) => key in updates);
|
|
3400
3347
|
}
|
|
3401
|
-
function stampSegmentOnInsert(segments, segment, messageId) {
|
|
3348
|
+
function stampSegmentOnInsert(segments, segment, messageId, defaults) {
|
|
3349
|
+
const base = applyDefaults(segment, defaults);
|
|
3402
3350
|
return {
|
|
3403
|
-
...
|
|
3351
|
+
...base,
|
|
3404
3352
|
messageId: segment.messageId ?? messageId,
|
|
3405
3353
|
index: segment.index ?? segments.length,
|
|
3406
|
-
|
|
3354
|
+
// A start already present is kept — a segment a consumer rebuilt from their own
|
|
3355
|
+
// storage keeps the number it was persisted with. `adoptSegment` is the path
|
|
3356
|
+
// that never writes one at all.
|
|
3357
|
+
...timingPatch(base, { startedAt: segmentTiming(segment)?.startedAt ?? Date.now() })
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
function adoptSegment(segments, segment, messageId) {
|
|
3361
|
+
return {
|
|
3362
|
+
...segment,
|
|
3363
|
+
messageId,
|
|
3364
|
+
index: segments.length,
|
|
3365
|
+
isStreaming: false,
|
|
3366
|
+
...segment.type === "tool_call" && segment.status === "awaiting-approval" ? { status: "aborted" } : {}
|
|
3407
3367
|
};
|
|
3408
3368
|
}
|
|
3369
|
+
const RESERVED = /* @__PURE__ */ new Set(["id", "type", "messageId", "index"]);
|
|
3370
|
+
function segmentTiming(segment) {
|
|
3371
|
+
return segment.meta?.aparte;
|
|
3372
|
+
}
|
|
3373
|
+
function timingPatch(segment, timing) {
|
|
3374
|
+
return { meta: { ...segment.meta, aparte: { ...segment.meta?.aparte, ...timing } } };
|
|
3375
|
+
}
|
|
3376
|
+
function applyDefaults(segment, defaults) {
|
|
3377
|
+
if (!defaults) return segment;
|
|
3378
|
+
const out = { ...segment };
|
|
3379
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
3380
|
+
if (RESERVED.has(key) || key in segment) continue;
|
|
3381
|
+
if (key === "meta") {
|
|
3382
|
+
const { aparte: _forged, ...rest } = value ?? {};
|
|
3383
|
+
out.meta = rest;
|
|
3384
|
+
continue;
|
|
3385
|
+
}
|
|
3386
|
+
out[key] = value;
|
|
3387
|
+
}
|
|
3388
|
+
return out;
|
|
3389
|
+
}
|
|
3409
3390
|
function isSegmentSettled(segment) {
|
|
3410
3391
|
if (segment.type === "tool_call") return !OPEN_TOOL_STATUSES.has(segment.status);
|
|
3411
3392
|
return segment.isStreaming === false;
|
|
@@ -3414,17 +3395,32 @@ function stampSegmentOnUpdate(segment, updates) {
|
|
|
3414
3395
|
if (isSegmentSettled(segment)) return updates;
|
|
3415
3396
|
const merged = { ...segment, ...updates };
|
|
3416
3397
|
if (isSegmentSettled(merged)) {
|
|
3417
|
-
|
|
3398
|
+
const ended = segmentTiming(segment)?.endedAt;
|
|
3399
|
+
return { ...updates, ...timingPatch(segment, { endedAt: ended ?? Date.now() }) };
|
|
3418
3400
|
}
|
|
3419
|
-
return isActivity(updates) ? { ...updates, endedAt: Date.now() } : updates;
|
|
3401
|
+
return isActivity(updates) ? { ...updates, ...timingPatch(segment, { endedAt: Date.now() }) } : updates;
|
|
3402
|
+
}
|
|
3403
|
+
function mergeSegmentUpdate(segment, updates) {
|
|
3404
|
+
const merged = { ...segment, ...updates };
|
|
3405
|
+
if (!("meta" in updates)) return merged;
|
|
3406
|
+
return {
|
|
3407
|
+
...merged,
|
|
3408
|
+
meta: {
|
|
3409
|
+
...segment.meta,
|
|
3410
|
+
...updates.meta,
|
|
3411
|
+
// Neither side's timing wins by accident: whichever one carries it, wins,
|
|
3412
|
+
// and a consumer writing `meta` without an `aparte` key keeps ours.
|
|
3413
|
+
...segment.meta?.aparte || updates.meta?.aparte ? { aparte: { ...segment.meta?.aparte, ...updates.meta?.aparte } } : {}
|
|
3414
|
+
}
|
|
3415
|
+
};
|
|
3420
3416
|
}
|
|
3421
3417
|
function segmentDuration(segment) {
|
|
3422
|
-
const { startedAt, endedAt } = segment;
|
|
3418
|
+
const { startedAt, endedAt } = segmentTiming(segment) ?? {};
|
|
3423
3419
|
if (startedAt == null || endedAt == null) return void 0;
|
|
3424
3420
|
return endedAt - startedAt;
|
|
3425
3421
|
}
|
|
3426
3422
|
function stampSegmentActivity(segment) {
|
|
3427
|
-
return isSegmentSettled(segment) ? {} : { endedAt: Date.now() };
|
|
3423
|
+
return isSegmentSettled(segment) ? {} : timingPatch(segment, { endedAt: Date.now() });
|
|
3428
3424
|
}
|
|
3429
3425
|
function segmentContentUpdate(segment) {
|
|
3430
3426
|
const content = segment.content;
|
|
@@ -3439,6 +3435,16 @@ function isTerminalStatus(status) {
|
|
|
3439
3435
|
function openSegmentIds(segments) {
|
|
3440
3436
|
return segments.filter((segment) => !isSegmentSettled(segment)).map((segment) => segment.id);
|
|
3441
3437
|
}
|
|
3438
|
+
function adoptMessageSegments(message) {
|
|
3439
|
+
if (!message.segments?.length) return { ...message };
|
|
3440
|
+
return {
|
|
3441
|
+
...message,
|
|
3442
|
+
segments: message.segments.reduce((acc, segment) => {
|
|
3443
|
+
acc.push(adoptSegment(acc, segment, message.id));
|
|
3444
|
+
return acc;
|
|
3445
|
+
}, [])
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3442
3448
|
function renumberSegments(segments) {
|
|
3443
3449
|
for (let i = 0; i < segments.length; i++) {
|
|
3444
3450
|
const segment = segments[i];
|
|
@@ -4139,10 +4145,14 @@ class AparteChatHost {
|
|
|
4139
4145
|
// Controller-driven flushes mirror AparteConversationManager state, not
|
|
4140
4146
|
// user intent — deliberately NOT emitted as `messagesChange` to
|
|
4141
4147
|
// avoid a feedback loop with hosts using a two-way `messages` input.
|
|
4148
|
+
// Adopted here rather than passed through: this is the conversation
|
|
4149
|
+
// controller handing back a stored list, and it goes STRAIGHT to the
|
|
4150
|
+
// framework's own reactive setter — so in every wrapper it never met the
|
|
4151
|
+
// stamping seam at all, tree or no tree.
|
|
4142
4152
|
setMessages: (msgs) => {
|
|
4143
4153
|
this._beginConversationSwap();
|
|
4144
4154
|
this._vp()?.resetSpacer?.();
|
|
4145
|
-
this.binding.setMessages(
|
|
4155
|
+
this.binding.setMessages(msgs.map(adoptMessageSegments));
|
|
4146
4156
|
},
|
|
4147
4157
|
appendMessage: (msg) => this.appendMessage(msg),
|
|
4148
4158
|
getMessages: () => this.binding.getMessages(),
|
|
@@ -4193,11 +4203,23 @@ class AparteChatHost {
|
|
|
4193
4203
|
* spec guards ("optimistic-append + parent-push race"). `onMessageAppended`
|
|
4194
4204
|
* is the append-specific signal instead.
|
|
4195
4205
|
*/
|
|
4196
|
-
appendMessage(message) {
|
|
4206
|
+
appendMessage(message, options) {
|
|
4197
4207
|
this._flushStreamState();
|
|
4198
4208
|
if (message.role === "user") this._vp()?.setAutoScroll?.(true);
|
|
4199
|
-
|
|
4200
|
-
|
|
4209
|
+
const stored = options?.historical ? adoptMessageSegments(message) : message.segments?.length ? {
|
|
4210
|
+
...message,
|
|
4211
|
+
segments: message.segments.reduce((acc, segment) => {
|
|
4212
|
+
acc.push(stampSegmentOnInsert(
|
|
4213
|
+
acc,
|
|
4214
|
+
segment,
|
|
4215
|
+
message.id,
|
|
4216
|
+
resolveConfig(this.binding.host).getSegmentDefaults(segment.type)
|
|
4217
|
+
));
|
|
4218
|
+
return acc;
|
|
4219
|
+
}, [])
|
|
4220
|
+
} : message;
|
|
4221
|
+
this.binding.setMessages([...this.binding.getMessages(), stored]);
|
|
4222
|
+
this.binding.onMessageAppended?.(stored);
|
|
4201
4223
|
}
|
|
4202
4224
|
/** Atomic partial update of a message by id. */
|
|
4203
4225
|
updateMessage(messageId, updates) {
|
|
@@ -4240,7 +4262,15 @@ class AparteChatHost {
|
|
|
4240
4262
|
this.binding.onTypingChange?.(false);
|
|
4241
4263
|
const msgs = before;
|
|
4242
4264
|
const last = target;
|
|
4243
|
-
const stamped = last ? stampSegmentOnInsert(
|
|
4265
|
+
const stamped = last ? stampSegmentOnInsert(
|
|
4266
|
+
last.segments || [],
|
|
4267
|
+
segment,
|
|
4268
|
+
last.id,
|
|
4269
|
+
// The host attaches its config to `binding.host`, so resolving from
|
|
4270
|
+
// that element is what gives a framework-managed chat its OWN
|
|
4271
|
+
// defaults rather than the page's.
|
|
4272
|
+
resolveConfig(this.binding.host).getSegmentDefaults(segment.type)
|
|
4273
|
+
) : segment;
|
|
4244
4274
|
this._lastBubble()?.addSegment?.(stamped);
|
|
4245
4275
|
if (!last) return;
|
|
4246
4276
|
const segments = [...last.segments || [], stamped];
|
|
@@ -4259,7 +4289,7 @@ class AparteChatHost {
|
|
|
4259
4289
|
this._lastBubble()?.updateSegment?.(segmentId, stamped);
|
|
4260
4290
|
if (!last || !last.segments) return;
|
|
4261
4291
|
const segments = last.segments.map(
|
|
4262
|
-
(s) => s.id === segmentId ?
|
|
4292
|
+
(s) => s.id === segmentId ? mergeSegmentUpdate(s, stamped) : s
|
|
4263
4293
|
);
|
|
4264
4294
|
const next = [...msgs.slice(0, -1), { ...last, segments }];
|
|
4265
4295
|
this.binding.setMessages(next);
|
|
@@ -5120,21 +5150,398 @@ function readableToAsyncIterable(stream, signal) {
|
|
|
5120
5150
|
}
|
|
5121
5151
|
};
|
|
5122
5152
|
}
|
|
5123
|
-
|
|
5124
|
-
function
|
|
5125
|
-
|
|
5126
|
-
const timedOut = new Promise((_resolve, reject) => {
|
|
5127
|
-
timer = setTimeout(() => {
|
|
5128
|
-
controller.abort();
|
|
5129
|
-
reject(Object.assign(new Error(`tool handler exceeded ${timeoutMs}ms`), { name: "AbortError" }));
|
|
5130
|
-
}, timeoutMs);
|
|
5131
|
-
});
|
|
5132
|
-
return {
|
|
5133
|
-
raced: Promise.race([work(controller.signal), timedOut]),
|
|
5134
|
-
done: () => clearTimeout(timer)
|
|
5135
|
-
};
|
|
5153
|
+
let _relabels = null;
|
|
5154
|
+
function onRelabel(fn) {
|
|
5155
|
+
_relabels?.push(fn);
|
|
5136
5156
|
}
|
|
5137
|
-
|
|
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);
|
|
5170
|
+
}
|
|
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));
|
|
5203
|
+
}
|
|
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);
|
|
5230
|
+
}
|
|
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();
|
|
5239
|
+
}
|
|
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;
|
|
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
|
+
});
|
|
5314
|
+
}
|
|
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
|
+
};
|
|
5339
|
+
}
|
|
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);
|
|
5347
|
+
}
|
|
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;
|
|
5360
|
+
}
|
|
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();
|
|
5377
|
+
};
|
|
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);
|
|
5391
|
+
}
|
|
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
|
+
};
|
|
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 {
|
|
5138
5545
|
_boundHandler = null;
|
|
5139
5546
|
_boundAbortHandler = null;
|
|
5140
5547
|
_boundCompactHandler = null;
|
|
@@ -5281,61 +5688,66 @@ class AparteClient {
|
|
|
5281
5688
|
}
|
|
5282
5689
|
this._activeToolControllers.clear();
|
|
5283
5690
|
}
|
|
5284
|
-
|
|
5285
|
-
*
|
|
5286
|
-
*
|
|
5287
|
-
*
|
|
5288
|
-
*
|
|
5289
|
-
*
|
|
5290
|
-
*
|
|
5291
|
-
*
|
|
5292
|
-
*
|
|
5293
|
-
*
|
|
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.
|
|
5294
5701
|
*/
|
|
5295
5702
|
/**
|
|
5296
|
-
*
|
|
5703
|
+
* Ask the human at the COMPOSER, and report what they decided.
|
|
5297
5704
|
*
|
|
5298
|
-
*
|
|
5299
|
-
*
|
|
5300
|
-
*
|
|
5301
|
-
*
|
|
5302
|
-
*
|
|
5303
|
-
*
|
|
5304
|
-
* executed action came apart, which is the entire failure mode an approval gate
|
|
5305
|
-
* exists to prevent — and the handler behind it is arbitrary consumer code.
|
|
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.
|
|
5306
5711
|
*
|
|
5307
|
-
* The
|
|
5308
|
-
*
|
|
5309
|
-
*
|
|
5310
|
-
* button keeps working as long as it fires from inside its own chat.
|
|
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.
|
|
5311
5715
|
*
|
|
5312
|
-
*
|
|
5313
|
-
*
|
|
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.
|
|
5314
5723
|
*/
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
document.addEventListener("aparte-tool-decision", onDecision);
|
|
5337
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
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
|
|
5338
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" };
|
|
5339
5751
|
}
|
|
5340
5752
|
/**
|
|
5341
5753
|
* Resolve the auth for a provider: `options.keyResolver` takes precedence,
|
|
@@ -5994,16 +6406,27 @@ ${content}
|
|
|
5994
6406
|
this._activeToolControllers.delete(controller);
|
|
5995
6407
|
}
|
|
5996
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
|
+
*/
|
|
5997
6420
|
/**
|
|
5998
6421
|
* Handle one `tool_use` stream event from {@link _streamLoop}: the built-in
|
|
5999
6422
|
* `create_artifact`, per-tool renderer selection, the human-in-the-loop
|
|
6000
6423
|
* approval gate, and running the registered handler (timeout / abort).
|
|
6001
6424
|
* Mutates the shared `messages` / `toolCallsThisTurn` history in place and
|
|
6002
|
-
* returns
|
|
6425
|
+
* returns what the loop should do next.
|
|
6003
6426
|
*/
|
|
6004
6427
|
async _handleToolUseEvent(event, ctx) {
|
|
6005
6428
|
const { targetElement, messageId, messages, toolCallsThisTurn, precedingText, artifactProgress, turns, globalMaxTurns } = ctx;
|
|
6006
|
-
let
|
|
6429
|
+
let flow = "continue";
|
|
6007
6430
|
toolCallsThisTurn.push({ id: event.id, name: event.name, input: event.input });
|
|
6008
6431
|
if (event.name === "create_artifact") {
|
|
6009
6432
|
const input = event.input;
|
|
@@ -6029,7 +6452,7 @@ ${content}
|
|
|
6029
6452
|
content: "Artifact created successfully.",
|
|
6030
6453
|
toolCallId: event.id
|
|
6031
6454
|
});
|
|
6032
|
-
return {
|
|
6455
|
+
return { flow: "continue" };
|
|
6033
6456
|
}
|
|
6034
6457
|
const toolSeg = {
|
|
6035
6458
|
id: `tool-${event.id}`,
|
|
@@ -6063,7 +6486,7 @@ ${content}
|
|
|
6063
6486
|
if (turns >= effectiveMaxTurns) {
|
|
6064
6487
|
console.warn(`[AparteClient] Tool "${event.name}" maxTurns (${effectiveMaxTurns}) reached.`);
|
|
6065
6488
|
targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
|
|
6066
|
-
return {
|
|
6489
|
+
return { flow: "halt" };
|
|
6067
6490
|
}
|
|
6068
6491
|
const handler = this._config.getToolHandler(event.name);
|
|
6069
6492
|
if (handler) {
|
|
@@ -6078,14 +6501,24 @@ ${content}
|
|
|
6078
6501
|
input: event.input
|
|
6079
6502
|
});
|
|
6080
6503
|
let decision;
|
|
6081
|
-
const resolveApproval = this.options.approvalResolver ?? ((
|
|
6504
|
+
const resolveApproval = this.options.approvalResolver ?? ((call, sig) => this._askForApproval(call, sig, targetElement));
|
|
6082
6505
|
try {
|
|
6083
|
-
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;
|
|
6084
6513
|
} finally {
|
|
6085
6514
|
this._activeToolControllers.delete(approvalController);
|
|
6086
6515
|
}
|
|
6516
|
+
if (approvalController.signal.aborted) {
|
|
6517
|
+
targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
|
|
6518
|
+
return { flow: "halt" };
|
|
6519
|
+
}
|
|
6087
6520
|
if (!decision.approved) {
|
|
6088
|
-
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.";
|
|
6089
6522
|
targetElement.updateSegment?.(toolSeg.id, { status: "rejected", result: rejection });
|
|
6090
6523
|
const existingToolCallMsg = messages.find(
|
|
6091
6524
|
(m) => m.role === "tool_call" && m.toolCalls?.some((tc) => tc.id === event.id)
|
|
@@ -6099,7 +6532,7 @@ ${content}
|
|
|
6099
6532
|
});
|
|
6100
6533
|
}
|
|
6101
6534
|
messages.push({ role: "tool_result", content: rejection, toolCallId: event.id });
|
|
6102
|
-
return {
|
|
6535
|
+
return { flow: "respond" };
|
|
6103
6536
|
}
|
|
6104
6537
|
if (decision.payload && typeof decision.payload === "object" && !Array.isArray(decision.payload)) {
|
|
6105
6538
|
effectiveInput = { ...event.input, ...decision.payload };
|
|
@@ -6139,7 +6572,7 @@ ${content}
|
|
|
6139
6572
|
} catch (err) {
|
|
6140
6573
|
if (err?.name === "AbortError") {
|
|
6141
6574
|
targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
|
|
6142
|
-
|
|
6575
|
+
flow = "halt";
|
|
6143
6576
|
} else {
|
|
6144
6577
|
throw err;
|
|
6145
6578
|
}
|
|
@@ -6150,9 +6583,9 @@ ${content}
|
|
|
6150
6583
|
} else {
|
|
6151
6584
|
console.warn(`[AparteClient] No handler registered for tool "${event.name}"`);
|
|
6152
6585
|
targetElement.updateSegment?.(toolSeg.id, { status: "aborted" });
|
|
6153
|
-
|
|
6586
|
+
flow = "halt";
|
|
6154
6587
|
}
|
|
6155
|
-
return {
|
|
6588
|
+
return { flow };
|
|
6156
6589
|
}
|
|
6157
6590
|
/**
|
|
6158
6591
|
* Stream loop: runs one provider.chat() call and repeats if a tool was called.
|
|
@@ -6177,6 +6610,7 @@ ${content}
|
|
|
6177
6610
|
targetElement.addSegment?.(seg);
|
|
6178
6611
|
}
|
|
6179
6612
|
while (continueLoop) {
|
|
6613
|
+
let endTurnEarly = false;
|
|
6180
6614
|
if (this._isAborted) {
|
|
6181
6615
|
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
6182
6616
|
break;
|
|
@@ -6382,7 +6816,8 @@ ${content}
|
|
|
6382
6816
|
{ id: event.id, name: event.name, input: event.input },
|
|
6383
6817
|
{ targetElement, messageId, messages, toolCallsThisTurn, precedingText, artifactProgress, turns, globalMaxTurns }
|
|
6384
6818
|
);
|
|
6385
|
-
if (
|
|
6819
|
+
if (toolResult.flow !== "continue") endTurnEarly = true;
|
|
6820
|
+
if (toolResult.flow === "halt") continueLoop = false;
|
|
6386
6821
|
break;
|
|
6387
6822
|
}
|
|
6388
6823
|
case "error":
|
|
@@ -6397,7 +6832,7 @@ ${content}
|
|
|
6397
6832
|
break;
|
|
6398
6833
|
}
|
|
6399
6834
|
}
|
|
6400
|
-
if (
|
|
6835
|
+
if (endTurnEarly) break;
|
|
6401
6836
|
}
|
|
6402
6837
|
finalizeXmlArtifact(xmlCtx, { targetElement, messageId, artifactProgress, artifactXmlHint, textParser, streamingSegmentIds });
|
|
6403
6838
|
const finals = textParser.finalize();
|
|
@@ -6512,7 +6947,7 @@ ${content}
|
|
|
6512
6947
|
const tool = this._config.getTools().find((t) => t.name === name);
|
|
6513
6948
|
return tool ? { maxTurns: tool.maxTurns, needsApproval: tool.needsApproval } : void 0;
|
|
6514
6949
|
};
|
|
6515
|
-
const approvalResolver = this.options.approvalResolver ?? ((
|
|
6950
|
+
const approvalResolver = this.options.approvalResolver ?? ((call, sig) => this._askForApproval(call, sig, targetElement));
|
|
6516
6951
|
const usage = await streamRunner({
|
|
6517
6952
|
messageId,
|
|
6518
6953
|
baseRequest,
|
|
@@ -6818,13 +7253,34 @@ class AparteMessageRepository {
|
|
|
6818
7253
|
}
|
|
6819
7254
|
}
|
|
6820
7255
|
const APARTE_DEFAULT_UI_EVENTS = [
|
|
7256
|
+
// <aparte-composer> and its parts
|
|
6821
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>
|
|
6822
7264
|
"aparte-action",
|
|
6823
7265
|
"aparte-retry",
|
|
6824
7266
|
"aparte-edit",
|
|
7267
|
+
"aparte-feedback",
|
|
7268
|
+
"aparte-message-info",
|
|
6825
7269
|
"aparte-branch-navigate",
|
|
6826
|
-
|
|
6827
|
-
"aparte-
|
|
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"
|
|
6828
7284
|
];
|
|
6829
7285
|
function applyElementProps(el2, props, transformValue = (value) => value) {
|
|
6830
7286
|
for (const [key, value] of Object.entries(props)) {
|
|
@@ -6847,344 +7303,75 @@ function isAwaitingReply(message) {
|
|
|
6847
7303
|
if (message.status === "streaming" || message.status === "pending") return true;
|
|
6848
7304
|
return message.status === void 0 && message.role === "assistant" && !message.content?.trim() && !message.segments?.length;
|
|
6849
7305
|
}
|
|
6850
|
-
function el(tag, className, text) {
|
|
6851
|
-
const node = document.createElement(tag);
|
|
6852
|
-
if (className) node.className = className;
|
|
6853
|
-
if (text != null) node.textContent = text;
|
|
6854
|
-
return node;
|
|
6855
|
-
}
|
|
6856
|
-
function fieldHeader(parent, field, skipTitle) {
|
|
6857
|
-
let titleId;
|
|
6858
|
-
if (field.title && field.title.trim() !== skipTitle?.trim()) {
|
|
6859
|
-
const title = el("p", "aparte-elic-title", field.title);
|
|
6860
|
-
titleId = `elic-title-${uuid()}`;
|
|
6861
|
-
title.id = titleId;
|
|
6862
|
-
parent.appendChild(title);
|
|
6863
|
-
}
|
|
6864
|
-
if (field.description) parent.appendChild(el("p", "aparte-elic-desc", field.description));
|
|
6865
|
-
return titleId;
|
|
6866
|
-
}
|
|
6867
|
-
function labelGroup(list, opts) {
|
|
6868
|
-
list.setAttribute("role", opts.multiple ? "group" : "radiogroup");
|
|
6869
|
-
if (opts.titleId) list.setAttribute("aria-labelledby", opts.titleId);
|
|
6870
|
-
else if (opts.fallbackLabel) list.setAttribute("aria-label", opts.fallbackLabel);
|
|
6871
|
-
}
|
|
6872
|
-
function buildEnumField(field, onChange, fallbackLabel) {
|
|
6873
|
-
const wrap = el("div", "aparte-elic-field aparte-elic-enum");
|
|
6874
|
-
const titleId = fieldHeader(wrap, field, fallbackLabel);
|
|
6875
|
-
const list = el("div", "aparte-elic-options");
|
|
6876
|
-
labelGroup(list, { multiple: field.multiple, titleId, fallbackLabel });
|
|
6877
|
-
const name = `elic-${uuid()}`;
|
|
6878
|
-
const type = field.multiple ? "checkbox" : "radio";
|
|
6879
|
-
const allowOther = field.allowOther ?? contextConfig().getElicitationOptions().allowOther;
|
|
6880
|
-
const defaults = new Set(Array.isArray(field.default) ? field.default : field.default != null ? [field.default] : []);
|
|
6881
|
-
const buildOption = (value, label, description, recommended) => {
|
|
6882
|
-
const row = el("label", "aparte-elic-option" + (recommended ? " aparte-elic-option--recommended" : ""));
|
|
6883
|
-
const control = el("input", "aparte-elic-control");
|
|
6884
|
-
control.type = type;
|
|
6885
|
-
control.name = name;
|
|
6886
|
-
control.value = value;
|
|
6887
|
-
if (defaults.has(value)) control.checked = true;
|
|
6888
|
-
const body = el("span", "aparte-elic-option-body");
|
|
6889
|
-
body.appendChild(el("span", "aparte-elic-option-title", label));
|
|
6890
|
-
if (description) body.appendChild(el("span", "aparte-elic-option-desc", description));
|
|
6891
|
-
row.append(control, body);
|
|
6892
|
-
return row;
|
|
6893
|
-
};
|
|
6894
|
-
for (const opt of field.options) {
|
|
6895
|
-
list.appendChild(buildOption(opt.value, opt.label ?? opt.value, opt.description, opt.recommended));
|
|
6896
|
-
}
|
|
6897
|
-
let otherText = null;
|
|
6898
|
-
if (allowOther) {
|
|
6899
|
-
const row = el("label", "aparte-elic-option aparte-elic-option--other");
|
|
6900
|
-
const control = el("input", "aparte-elic-control");
|
|
6901
|
-
control.type = type;
|
|
6902
|
-
control.name = name;
|
|
6903
|
-
control.value = "__other__";
|
|
6904
|
-
const body = el("span", "aparte-elic-option-body");
|
|
6905
|
-
const t = contextConfig();
|
|
6906
|
-
body.appendChild(el("span", "aparte-elic-option-title", t.t("elicitationOther")));
|
|
6907
|
-
otherText = el("input", "aparte-elic-other-input");
|
|
6908
|
-
otherText.type = "text";
|
|
6909
|
-
otherText.placeholder = t.t("elicitationOtherPlaceholder");
|
|
6910
|
-
otherText.style.display = "none";
|
|
6911
|
-
otherText.setAttribute("aria-label", t.t("elicitationOtherLabel"));
|
|
6912
|
-
body.appendChild(otherText);
|
|
6913
|
-
row.append(control, body);
|
|
6914
|
-
list.appendChild(row);
|
|
6915
|
-
}
|
|
6916
|
-
wrap.appendChild(list);
|
|
6917
|
-
const controls = () => Array.from(list.querySelectorAll(".aparte-elic-control"));
|
|
6918
|
-
const other = () => list.querySelector('input[value="__other__"]');
|
|
6919
|
-
list.addEventListener("change", () => {
|
|
6920
|
-
const o = other();
|
|
6921
|
-
if (o && otherText) {
|
|
6922
|
-
otherText.style.display = o.checked ? "" : "none";
|
|
6923
|
-
if (o.checked) otherText.focus();
|
|
6924
|
-
}
|
|
6925
|
-
onChange();
|
|
6926
|
-
});
|
|
6927
|
-
otherText?.addEventListener("input", onChange);
|
|
6928
|
-
const getValue = () => {
|
|
6929
|
-
const otherVal = other()?.checked && otherText?.value.trim() ? otherText.value.trim() : "";
|
|
6930
|
-
if (field.multiple) {
|
|
6931
|
-
const vals = controls().filter((c) => c.checked && c.value !== "__other__").map((c) => c.value);
|
|
6932
|
-
if (otherVal) vals.push(otherVal);
|
|
6933
|
-
return vals;
|
|
6934
|
-
}
|
|
6935
|
-
const checked = controls().find((c) => c.checked);
|
|
6936
|
-
if (!checked) return "";
|
|
6937
|
-
return checked.value === "__other__" ? otherVal : checked.value;
|
|
6938
|
-
};
|
|
6939
|
-
return {
|
|
6940
|
-
el: wrap,
|
|
6941
|
-
getValue,
|
|
6942
|
-
isComplete: () => {
|
|
6943
|
-
const v = getValue();
|
|
6944
|
-
return field.multiple ? v.length > 0 : v !== "";
|
|
6945
|
-
},
|
|
6946
|
-
focus: () => controls()[0]?.focus()
|
|
6947
|
-
};
|
|
6948
|
-
}
|
|
6949
|
-
function buildBooleanField(field, onChange, fallbackLabel) {
|
|
6950
|
-
const wrap = el("div", "aparte-elic-field aparte-elic-boolean");
|
|
6951
|
-
const titleId = fieldHeader(wrap, field, fallbackLabel);
|
|
6952
|
-
const list = el("div", "aparte-elic-options");
|
|
6953
|
-
labelGroup(list, { titleId, fallbackLabel });
|
|
6954
|
-
const name = `elic-${uuid()}`;
|
|
6955
|
-
const mk = (val, label) => {
|
|
6956
|
-
const row = el("label", "aparte-elic-option");
|
|
6957
|
-
const control = el("input", "aparte-elic-control");
|
|
6958
|
-
control.type = "radio";
|
|
6959
|
-
control.name = name;
|
|
6960
|
-
control.value = val;
|
|
6961
|
-
if (field.default != null && String(field.default) === val) control.checked = true;
|
|
6962
|
-
const body = el("span", "aparte-elic-option-body");
|
|
6963
|
-
body.appendChild(el("span", "aparte-elic-option-title", label));
|
|
6964
|
-
row.append(control, body);
|
|
6965
|
-
list.appendChild(row);
|
|
6966
|
-
};
|
|
6967
|
-
const t = contextConfig();
|
|
6968
|
-
mk("true", field.trueLabel ?? t.t("elicitationYes"));
|
|
6969
|
-
mk("false", field.falseLabel ?? t.t("elicitationNo"));
|
|
6970
|
-
wrap.appendChild(list);
|
|
6971
|
-
list.addEventListener("change", onChange);
|
|
6972
|
-
const checked = () => list.querySelector("input:checked");
|
|
6973
|
-
return {
|
|
6974
|
-
el: wrap,
|
|
6975
|
-
getValue: () => checked()?.value === "true",
|
|
6976
|
-
isComplete: () => checked() != null,
|
|
6977
|
-
focus: () => list.querySelector("input")?.focus()
|
|
6978
|
-
};
|
|
6979
|
-
}
|
|
6980
|
-
function buildStringField(field, onChange, fallbackLabel) {
|
|
6981
|
-
const wrap = el("div", "aparte-elic-field aparte-elic-string");
|
|
6982
|
-
fieldHeader(wrap, field, fallbackLabel);
|
|
6983
|
-
const input = field.multiline ? el("textarea", "aparte-elic-text") : el("input", "aparte-elic-text");
|
|
6984
|
-
if (!field.multiline) input.type = "text";
|
|
6985
|
-
if (field.placeholder) input.setAttribute("placeholder", field.placeholder);
|
|
6986
|
-
if (field.default) input.value = field.default;
|
|
6987
|
-
if (field.maxLength != null) input.setAttribute("maxlength", String(field.maxLength));
|
|
6988
|
-
input.setAttribute("aria-label", field.title ?? field.description ?? fallbackLabel ?? contextConfig().t("elicitationAnswerLabel"));
|
|
6989
|
-
wrap.appendChild(input);
|
|
6990
|
-
input.addEventListener("input", onChange);
|
|
6991
|
-
const required = field.required ?? true;
|
|
6992
|
-
const value = () => input.value;
|
|
6993
|
-
return {
|
|
6994
|
-
el: wrap,
|
|
6995
|
-
getValue: () => value(),
|
|
6996
|
-
isComplete: () => {
|
|
6997
|
-
const v = value().trim();
|
|
6998
|
-
if (!required && v === "") return true;
|
|
6999
|
-
return v.length >= (field.minLength ?? 1);
|
|
7000
|
-
},
|
|
7001
|
-
focus: () => input.focus()
|
|
7002
|
-
};
|
|
7003
|
-
}
|
|
7004
|
-
function buildField(field, onChange, fallbackLabel, key) {
|
|
7005
|
-
const custom = contextConfig().getElicitationFieldRenderer()?.(field, { key, notifyChange: onChange });
|
|
7006
|
-
if (custom) {
|
|
7007
|
-
return {
|
|
7008
|
-
el: custom.el,
|
|
7009
|
-
getValue: () => custom.getValue(),
|
|
7010
|
-
isComplete: () => custom.isComplete(),
|
|
7011
|
-
focus: () => custom.focus?.()
|
|
7012
|
-
};
|
|
7013
|
-
}
|
|
7014
|
-
switch (field.type) {
|
|
7015
|
-
case "enum":
|
|
7016
|
-
return buildEnumField(field, onChange, fallbackLabel);
|
|
7017
|
-
case "boolean":
|
|
7018
|
-
return buildBooleanField(field, onChange, fallbackLabel);
|
|
7019
|
-
case "string":
|
|
7020
|
-
return buildStringField(field, onChange, fallbackLabel);
|
|
7021
|
-
}
|
|
7022
|
-
}
|
|
7023
|
-
function buildElicitationPanel(message, schema, onChange) {
|
|
7024
|
-
const panel = el("div", "aparte-elic-panel");
|
|
7025
|
-
if (message) panel.appendChild(el("p", "aparte-elic-message", message));
|
|
7026
|
-
const body = el("div", "aparte-elic-body");
|
|
7027
|
-
panel.appendChild(body);
|
|
7028
|
-
const dismiss = el("div", "aparte-elic-dismiss");
|
|
7029
|
-
panel.appendChild(dismiss);
|
|
7030
|
-
if (schema.type === "object") {
|
|
7031
|
-
let show = function(index) {
|
|
7032
|
-
current = index;
|
|
7033
|
-
fields.forEach((f, i) => {
|
|
7034
|
-
f.field.el.hidden = i !== index;
|
|
7035
|
-
});
|
|
7036
|
-
syncNav();
|
|
7037
|
-
};
|
|
7038
|
-
const entries = Object.entries(schema.properties);
|
|
7039
|
-
const requiredKeys = new Set(schema.required ?? entries.map(([k]) => k));
|
|
7040
|
-
const cfg = contextConfig();
|
|
7041
|
-
let syncNav = () => {
|
|
7042
|
-
};
|
|
7043
|
-
const notify = () => {
|
|
7044
|
-
syncNav();
|
|
7045
|
-
onChange();
|
|
7046
|
-
};
|
|
7047
|
-
const fields = entries.map(([key, field2]) => {
|
|
7048
|
-
const built = buildField(field2, notify, void 0, key);
|
|
7049
|
-
if (!field2.title && !built.el.querySelector(".aparte-elic-title")) {
|
|
7050
|
-
built.el.insertBefore(el("p", "aparte-elic-title", key), built.el.firstChild);
|
|
7051
|
-
}
|
|
7052
|
-
body.appendChild(built.el);
|
|
7053
|
-
return { key, field: built, required: requiredKeys.has(key), header: field2.header };
|
|
7054
|
-
});
|
|
7055
|
-
const isComplete = () => fields.every((f) => !f.required || f.field.isComplete());
|
|
7056
|
-
const api = {
|
|
7057
|
-
el: panel,
|
|
7058
|
-
dismiss,
|
|
7059
|
-
getContent: () => Object.fromEntries(fields.map((f) => [f.key, f.field.getValue()])),
|
|
7060
|
-
isComplete,
|
|
7061
|
-
focus: () => fields[0]?.field.focus(),
|
|
7062
|
-
// A form of one behaves like a single field; only a real form steps.
|
|
7063
|
-
mode: () => "submit",
|
|
7064
|
-
canProceed: isComplete,
|
|
7065
|
-
proceed: () => {
|
|
7066
|
-
}
|
|
7067
|
-
};
|
|
7068
|
-
if (fields.length < 2 || cfg.getElicitationOptions().layout !== "stepped") return api;
|
|
7069
|
-
panel.classList.add("aparte-elic-panel--stepped");
|
|
7070
|
-
let current = 0;
|
|
7071
|
-
const steps = el("div", "aparte-elic-steps");
|
|
7072
|
-
steps.setAttribute("role", "tablist");
|
|
7073
|
-
const chips = fields.map((f, i) => {
|
|
7074
|
-
const chip = el("button", "aparte-elic-step");
|
|
7075
|
-
chip.type = "button";
|
|
7076
|
-
chip.setAttribute("role", "tab");
|
|
7077
|
-
chip.textContent = f.header?.trim() || String(i + 1);
|
|
7078
|
-
chip.addEventListener("click", () => {
|
|
7079
|
-
show(i);
|
|
7080
|
-
f.field.focus();
|
|
7081
|
-
});
|
|
7082
|
-
steps.appendChild(chip);
|
|
7083
|
-
return chip;
|
|
7084
|
-
});
|
|
7085
|
-
panel.insertBefore(steps, body);
|
|
7086
|
-
syncNav = () => {
|
|
7087
|
-
chips.forEach((chip, i) => {
|
|
7088
|
-
chip.setAttribute("aria-selected", String(i === current));
|
|
7089
|
-
chip.toggleAttribute("data-answered", fields[i].field.isComplete());
|
|
7090
|
-
});
|
|
7091
|
-
};
|
|
7092
|
-
show(0);
|
|
7093
|
-
const last = () => current === fields.length - 1;
|
|
7094
|
-
return {
|
|
7095
|
-
...api,
|
|
7096
|
-
focus: () => fields[current]?.field.focus(),
|
|
7097
|
-
mode: () => last() ? "submit" : "advance",
|
|
7098
|
-
// Advancing needs only THIS question answered; submitting needs them all,
|
|
7099
|
-
// which is what stops the last question from accepting a form with a hole
|
|
7100
|
-
// in it.
|
|
7101
|
-
canProceed: () => last() ? isComplete() : !!fields[current]?.field.isComplete(),
|
|
7102
|
-
proceed: () => {
|
|
7103
|
-
if (last()) return;
|
|
7104
|
-
show(current + 1);
|
|
7105
|
-
fields[current]?.field.focus();
|
|
7106
|
-
}
|
|
7107
|
-
};
|
|
7108
|
-
}
|
|
7109
|
-
const field = buildField(schema, onChange, message);
|
|
7110
|
-
body.appendChild(field.el);
|
|
7111
|
-
return {
|
|
7112
|
-
el: panel,
|
|
7113
|
-
dismiss,
|
|
7114
|
-
getContent: () => field.getValue(),
|
|
7115
|
-
isComplete: () => field.isComplete(),
|
|
7116
|
-
focus: () => field.focus(),
|
|
7117
|
-
mode: () => "submit",
|
|
7118
|
-
canProceed: () => field.isComplete(),
|
|
7119
|
-
proceed: () => {
|
|
7120
|
-
}
|
|
7121
|
-
};
|
|
7122
|
-
}
|
|
7123
|
-
function requestUserInput(request) {
|
|
7124
|
-
return resolveConfig(request.target ?? null).requestUserInput(request);
|
|
7125
|
-
}
|
|
7126
7306
|
export {
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7307
|
+
resolveConfig as $,
|
|
7308
|
+
APARTE_CONFIG_CHANGE as A,
|
|
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 _,
|
|
7335
|
+
APARTE_CONVERSATION_SCHEMA_VERSION as a,
|
|
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,
|
|
7351
|
+
APARTE_DEFAULT_BUBBLE_ACTIONS as b,
|
|
7352
|
+
APARTE_DEFAULT_HOST_HANDLERS as c,
|
|
7353
|
+
APARTE_DEFAULT_ICON_FALLBACKS as d,
|
|
7354
|
+
APARTE_DEFAULT_LOCALE as e,
|
|
7355
|
+
APARTE_DEFAULT_SKELETON_FALLBACKS as f,
|
|
7356
|
+
APARTE_DEFAULT_UI_EVENTS as g,
|
|
7357
|
+
APARTE_HOST_ATTR as h,
|
|
7358
|
+
AparteBackendTransport as i,
|
|
7359
|
+
AparteChatHost as j,
|
|
7360
|
+
AparteClient as k,
|
|
7361
|
+
AparteConfig as l,
|
|
7362
|
+
AparteConversationController as m,
|
|
7363
|
+
AparteConversationManager as n,
|
|
7364
|
+
AparteDirectTransport as o,
|
|
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
|
|
7189
7376
|
};
|
|
7190
|
-
//# sourceMappingURL=
|
|
7377
|
+
//# sourceMappingURL=is-awaiting-reply-Cb7H3YfN.js.map
|