@aparte/core 0.19.2 → 0.20.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/README.md +6 -1
- package/dist/client/aparte-client.d.ts +76 -97
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/client/stream-adapter.d.ts +7 -13
- package/dist/client/stream-adapter.d.ts.map +1 -1
- package/dist/client/wire-history.d.ts +4 -5
- package/dist/client/wire-history.d.ts.map +1 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts +89 -58
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/bubble/bubble-sync.d.ts +1 -1
- package/dist/components/chat/aparte-chat.d.ts +15 -13
- package/dist/components/chat/aparte-chat.d.ts.map +1 -1
- package/dist/components/chat/glide-gate.d.ts +13 -0
- package/dist/components/chat/glide-gate.d.ts.map +1 -0
- package/dist/components/composer/aparte-composer-action.d.ts +3 -3
- package/dist/components/composer/aparte-composer-add-attachment.d.ts +2 -2
- package/dist/components/composer/aparte-composer-cancel.d.ts +1 -1
- package/dist/components/composer/aparte-composer-input.d.ts +4 -7
- package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-send.d.ts +9 -11
- package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer.d.ts +50 -80
- package/dist/components/composer/aparte-composer.d.ts.map +1 -1
- package/dist/components/context/aparte-context.d.ts +13 -7
- package/dist/components/context/aparte-context.d.ts.map +1 -1
- package/dist/components/conversation-list/aparte-conversation-list.d.ts +25 -13
- package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
- package/dist/components/elicitation/aparte-elicitation.d.ts +15 -22
- package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
- package/dist/components/scroll-rail/aparte-scroll-rail.d.ts +10 -11
- package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -1
- package/dist/components/sidebar/aparte-sidebar.d.ts +16 -3
- package/dist/components/sidebar/aparte-sidebar.d.ts.map +1 -1
- package/dist/components/split/aparte-split.d.ts +12 -3
- package/dist/components/split/aparte-split.d.ts.map +1 -1
- package/dist/components/status/aparte-chat-status.d.ts +5 -3
- package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
- package/dist/components/suggestions/aparte-suggestions.d.ts +6 -9
- package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +76 -69
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +169 -141
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/icon-provider.d.ts +19 -2
- package/dist/config/icon-provider.d.ts.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/locale.d.ts +61 -89
- package/dist/config/locale.d.ts.map +1 -1
- package/dist/config/sanitize.d.ts.map +1 -1
- package/dist/conversations/conversation-controller.d.ts +18 -3
- package/dist/conversations/conversation-controller.d.ts.map +1 -1
- package/dist/conversations/conversation-manager.d.ts +6 -6
- package/dist/conversations/conversation-manager.d.ts.map +1 -1
- package/dist/conversations/index.d.ts +1 -1
- package/dist/conversations/index.d.ts.map +1 -1
- package/dist/custom-elements.json +5656 -4759
- package/dist/{dialog-triggers-CEazY76J.js → dialog-triggers-3Rk7PuZa.js} +555 -375
- package/dist/dialog-triggers-3Rk7PuZa.js.map +1 -0
- package/dist/elicitation/approval-panel.d.ts.map +1 -1
- package/dist/elicitation/index.d.ts +6 -1
- package/dist/elicitation/index.d.ts.map +1 -1
- package/dist/elicitation/panel.d.ts +11 -21
- package/dist/elicitation/panel.d.ts.map +1 -1
- package/dist/elicitation/types.d.ts +19 -27
- package/dist/elicitation/types.d.ts.map +1 -1
- package/dist/generated/element-attributes.d.ts +16 -10
- package/dist/generated/element-attributes.d.ts.map +1 -1
- package/dist/host/aparte-chat-host.d.ts +17 -32
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/icons/extended.d.ts +1 -2
- package/dist/icons/extended.d.ts.map +1 -1
- package/dist/icons.d.ts +6 -8
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +13 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +775 -337
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +7 -4
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +2 -2
- package/dist/index.node.js.map +1 -1
- package/dist/interop/dialog-triggers.d.ts +2 -2
- package/dist/interop/element-props.d.ts +16 -27
- package/dist/interop/element-props.d.ts.map +1 -1
- package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
- package/dist/primitives/icon/aparte-icon.d.ts.map +1 -1
- package/dist/primitives/select/aparte-option.d.ts +3 -2
- package/dist/primitives/select/aparte-option.d.ts.map +1 -1
- package/dist/primitives/select/aparte-select.d.ts +24 -21
- package/dist/primitives/select/aparte-select.d.ts.map +1 -1
- package/dist/renderers/markdown-stream.d.ts.map +1 -1
- package/dist/renderers/segment-renderers.d.ts +11 -17
- package/dist/renderers/segment-renderers.d.ts.map +1 -1
- package/dist/renderers/segments/code.d.ts.map +1 -1
- package/dist/renderers/segments/error.d.ts.map +1 -1
- package/dist/renderers/segments/text.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/runtime/message-repository.d.ts +8 -15
- package/dist/runtime/message-repository.d.ts.map +1 -1
- package/dist/transport/agent-handler.d.ts +2 -0
- package/dist/transport/agent-handler.d.ts.map +1 -1
- package/dist/transport/backend-handler.d.ts +4 -6
- package/dist/transport/backend-handler.d.ts.map +1 -1
- package/dist/transport/backend-transport.d.ts +15 -1
- package/dist/transport/backend-transport.d.ts.map +1 -1
- package/dist/transport/types.d.ts +7 -10
- package/dist/transport/types.d.ts.map +1 -1
- package/dist/types/chat.d.ts +10 -18
- package/dist/types/chat.d.ts.map +1 -1
- package/dist/types/element-attributes.d.ts +6 -12
- package/dist/types/element-attributes.d.ts.map +1 -1
- package/dist/types/element-map.d.ts +6 -19
- package/dist/types/element-map.d.ts.map +1 -1
- package/dist/types/event-map.d.ts +11 -17
- package/dist/types/event-map.d.ts.map +1 -1
- package/dist/types/events.d.ts +5 -14
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/imperative-api.d.ts +3 -1
- package/dist/types/imperative-api.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/model-provider.d.ts +7 -9
- package/dist/types/model-provider.d.ts.map +1 -1
- package/dist/types/segments.d.ts +29 -13
- package/dist/types/segments.d.ts.map +1 -1
- package/dist/types/tools.d.ts +21 -23
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/utils/copy-text.d.ts +1 -2
- package/dist/utils/copy-text.d.ts.map +1 -1
- package/dist/utils/css-escape.d.ts +1 -1
- package/dist/utils/escape.d.ts +4 -19
- package/dist/utils/escape.d.ts.map +1 -1
- package/dist/utils/files-to-attachments.d.ts +2 -2
- package/dist/utils/fold-text.d.ts +13 -0
- package/dist/utils/fold-text.d.ts.map +1 -0
- package/dist/utils/is-awaiting-reply.d.ts +2 -5
- package/dist/utils/is-awaiting-reply.d.ts.map +1 -1
- package/dist/utils/presence.d.ts +5 -5
- package/dist/utils/segments.d.ts +54 -78
- package/dist/utils/segments.d.ts.map +1 -1
- package/dist/utils/tool-input.d.ts +1 -1
- package/dist/utils/tool-input.d.ts.map +1 -1
- package/dist/utils/transcript.d.ts +2 -2
- package/dist/utils/uuid.d.ts +0 -11
- package/dist/utils/uuid.d.ts.map +1 -1
- package/package.json +30 -4
- package/dist/dialog-triggers-CEazY76J.js.map +0 -1
|
@@ -84,7 +84,7 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
84
84
|
copy: "Copy",
|
|
85
85
|
copied: "Copied!",
|
|
86
86
|
retry: "Retry",
|
|
87
|
-
thinking: "Thinking
|
|
87
|
+
thinking: "Thinking",
|
|
88
88
|
elicitationOther: "Other…",
|
|
89
89
|
elicitationOtherPlaceholder: "Type your answer…",
|
|
90
90
|
elicitationOtherLabel: "Custom answer",
|
|
@@ -92,6 +92,8 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
92
92
|
stopButton: "Stop",
|
|
93
93
|
elicitationRecommended: "Recommended",
|
|
94
94
|
elicitationSkip: "Skip",
|
|
95
|
+
elicitationHide: "Hide for now",
|
|
96
|
+
elicitationWaiting: "Waiting for your answer",
|
|
95
97
|
elicitationYes: "Yes",
|
|
96
98
|
elicitationNo: "No",
|
|
97
99
|
elicitationAnswerLabel: "Your answer",
|
|
@@ -113,10 +115,13 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
113
115
|
approvalArgsLabel: "Arguments",
|
|
114
116
|
actionUpload: "Attach file",
|
|
115
117
|
removeAttachment: "Remove {name}",
|
|
118
|
+
messageFrom: "Message from {name}",
|
|
116
119
|
selectSearchPlaceholder: "Search…",
|
|
117
120
|
selectSearchLabel: "Search options",
|
|
118
121
|
loading: "Loading…",
|
|
119
122
|
download: "Download",
|
|
123
|
+
openArtifact: "Open",
|
|
124
|
+
modelContextWindow: "{n} context",
|
|
120
125
|
previewPending: "Press Preview to run this artifact.",
|
|
121
126
|
sandboxError: "The sandbox failed during generation.",
|
|
122
127
|
sandboxErrorHint: "Common cause: the model produced invalid code (undefined variable, wrong argument type). Retry the request — the model may produce different code.",
|
|
@@ -124,7 +129,7 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
124
129
|
code: "Code",
|
|
125
130
|
generating: "Generating…",
|
|
126
131
|
rebuildingPreview: "Rebuilding preview…",
|
|
127
|
-
typing: "Typing
|
|
132
|
+
typing: "Typing",
|
|
128
133
|
error: "Error",
|
|
129
134
|
running: "Running...",
|
|
130
135
|
run: "Run",
|
|
@@ -148,6 +153,8 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
148
153
|
messageInfo: "Details",
|
|
149
154
|
newChat: "New Chat",
|
|
150
155
|
loadingConversations: "Loading conversations",
|
|
156
|
+
conversationSearchEmpty: "No conversation matches",
|
|
157
|
+
conversationList: "Conversations",
|
|
151
158
|
loadingConversation: "Loading the conversation",
|
|
152
159
|
deleteConversation: "Delete",
|
|
153
160
|
archiveConversation: "Archive",
|
|
@@ -698,8 +705,7 @@ class AparteStreamParser {
|
|
|
698
705
|
return {
|
|
699
706
|
id: this._options.autoGenerateIds ? this._generateId() : "",
|
|
700
707
|
type: "thinking",
|
|
701
|
-
content
|
|
702
|
-
label: "Thinking"
|
|
708
|
+
content
|
|
703
709
|
};
|
|
704
710
|
}
|
|
705
711
|
_isPartialMatch(buffer, pattern) {
|
|
@@ -1025,6 +1031,46 @@ function renumberSegments(segments) {
|
|
|
1025
1031
|
segments[i] = { ...segment, index: i };
|
|
1026
1032
|
}
|
|
1027
1033
|
}
|
|
1034
|
+
const APARTE_HOST_ATTR = "data-aparte-host";
|
|
1035
|
+
const CONFIG_SLOT = Symbol.for("aparte.instanceConfig");
|
|
1036
|
+
function notifyConfigChanged(root, next, previous) {
|
|
1037
|
+
if (next === previous) return;
|
|
1038
|
+
const call = (el2) => {
|
|
1039
|
+
const hook = el2.aparteConfigChanged;
|
|
1040
|
+
if (typeof hook === "function") hook.call(el2, next, previous);
|
|
1041
|
+
};
|
|
1042
|
+
call(root);
|
|
1043
|
+
for (const el2 of root.querySelectorAll("*")) call(el2);
|
|
1044
|
+
}
|
|
1045
|
+
function attachConfig(el2, config) {
|
|
1046
|
+
const previous = el2[CONFIG_SLOT] ?? resolveConfig(el2.parentElement);
|
|
1047
|
+
el2[CONFIG_SLOT] = config;
|
|
1048
|
+
el2.setAttribute(APARTE_HOST_ATTR, "");
|
|
1049
|
+
notifyConfigChanged(el2, config, previous);
|
|
1050
|
+
}
|
|
1051
|
+
function detachConfig(el2) {
|
|
1052
|
+
const previous = el2[CONFIG_SLOT];
|
|
1053
|
+
delete el2[CONFIG_SLOT];
|
|
1054
|
+
el2.removeAttribute(APARTE_HOST_ATTR);
|
|
1055
|
+
if (previous) notifyConfigChanged(el2, resolveConfig(el2.parentElement), previous);
|
|
1056
|
+
}
|
|
1057
|
+
function resolveConfig(el2) {
|
|
1058
|
+
const host = el2?.closest?.(`[${APARTE_HOST_ATTR}]`);
|
|
1059
|
+
return host?.[CONFIG_SLOT] ?? aparteGlobalConfig;
|
|
1060
|
+
}
|
|
1061
|
+
let _renderConfig = null;
|
|
1062
|
+
function runWithConfig(config, fn) {
|
|
1063
|
+
const prev = _renderConfig;
|
|
1064
|
+
_renderConfig = config;
|
|
1065
|
+
try {
|
|
1066
|
+
return fn();
|
|
1067
|
+
} finally {
|
|
1068
|
+
_renderConfig = prev;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function contextConfig(el2) {
|
|
1072
|
+
return _renderConfig ?? resolveConfig(el2);
|
|
1073
|
+
}
|
|
1028
1074
|
function dispatchLifecycleEvent(target, name, detail, targetId) {
|
|
1029
1075
|
const id = targetId ?? (target.id || void 0);
|
|
1030
1076
|
const stamped = detail && typeof detail === "object" ? { targetId: id, ...detail } : detail;
|
|
@@ -1061,9 +1107,7 @@ function createStreamAdapter(opts) {
|
|
|
1061
1107
|
const seg = {
|
|
1062
1108
|
id: `think-${uuid()}`,
|
|
1063
1109
|
type: "thinking",
|
|
1064
|
-
content: thinkingContent
|
|
1065
|
-
collapsed: true,
|
|
1066
|
-
label: "Thinking"
|
|
1110
|
+
content: thinkingContent
|
|
1067
1111
|
};
|
|
1068
1112
|
thinkingId = seg.id;
|
|
1069
1113
|
streaming.add(seg.id);
|
|
@@ -1075,7 +1119,9 @@ function createStreamAdapter(opts) {
|
|
|
1075
1119
|
}
|
|
1076
1120
|
case "text-delta": {
|
|
1077
1121
|
if (thinkingId && !thinkingCollapsed) {
|
|
1078
|
-
target
|
|
1122
|
+
const chatConfig = typeof Element !== "undefined" && target instanceof Element ? resolveConfig(target) : config;
|
|
1123
|
+
const keepOpen = chatConfig.getSegmentDefaults("thinking")?.collapsed === false;
|
|
1124
|
+
target.updateSegment?.(thinkingId, keepOpen ? { isStreaming: false } : { collapsed: true, isStreaming: false });
|
|
1079
1125
|
thinkingCollapsed = true;
|
|
1080
1126
|
}
|
|
1081
1127
|
const result = parser.parse(e.delta);
|
|
@@ -1814,6 +1860,26 @@ const APARTE_DEFAULT_HOST_HANDLERS = {
|
|
|
1814
1860
|
};
|
|
1815
1861
|
const NOOP = () => {
|
|
1816
1862
|
};
|
|
1863
|
+
const renderingVersions = /* @__PURE__ */ new WeakMap();
|
|
1864
|
+
function renderingVersionOf(cfg) {
|
|
1865
|
+
return renderingVersions.get(cfg) ?? 0;
|
|
1866
|
+
}
|
|
1867
|
+
function bumpRenderingVersion(cfg) {
|
|
1868
|
+
renderingVersions.set(cfg, renderingVersionOf(cfg) + 1);
|
|
1869
|
+
}
|
|
1870
|
+
function drawingOf(provider, name) {
|
|
1871
|
+
if (!provider || !(name in provider) || name in Object.prototype) return void 0;
|
|
1872
|
+
const fn = provider[name];
|
|
1873
|
+
return typeof fn === "function" ? fn.bind(provider) : void 0;
|
|
1874
|
+
}
|
|
1875
|
+
const iconLayers = /* @__PURE__ */ new WeakSet();
|
|
1876
|
+
function namesOf(provider) {
|
|
1877
|
+
const names = /* @__PURE__ */ new Set();
|
|
1878
|
+
for (let o = provider; o && o !== Object.prototype; o = Object.getPrototypeOf(o)) {
|
|
1879
|
+
for (const name of Object.getOwnPropertyNames(o)) if (name !== "constructor") names.add(name);
|
|
1880
|
+
}
|
|
1881
|
+
return [...names];
|
|
1882
|
+
}
|
|
1817
1883
|
class AparteConfig {
|
|
1818
1884
|
_markdownProvider;
|
|
1819
1885
|
_streamingMarkdownProvider;
|
|
@@ -1845,15 +1911,13 @@ class AparteConfig {
|
|
|
1845
1911
|
/**
|
|
1846
1912
|
* What `refreshProviderModels()` last brought back, per provider.
|
|
1847
1913
|
*
|
|
1848
|
-
*
|
|
1849
|
-
*
|
|
1850
|
-
*
|
|
1851
|
-
*
|
|
1852
|
-
*
|
|
1853
|
-
* it
|
|
1854
|
-
*
|
|
1855
|
-
* the model. A registered tool, an approval gate, `<aparte-elicitation>` — all
|
|
1856
|
-
* dead, with the model answering, correctly, that it had been given no tools.
|
|
1914
|
+
* `getCurrentModel()` reads it beside `provider.getModels()`, the SYNCHRONOUS,
|
|
1915
|
+
* hand-declared list, which every preset of `@aparte/provider-openai-compat`
|
|
1916
|
+
* leaves empty because a compat endpoint's list is fetched at runtime. Without
|
|
1917
|
+
* the fetched list the current model resolves to `undefined`, and its declared
|
|
1918
|
+
* capabilities are unknown: `AparteClient._toolsForCurrentModel()` still sends the
|
|
1919
|
+
* tools then, and it is the fetched list that lets a model which declares no
|
|
1920
|
+
* `function_calling` be honoured.
|
|
1857
1921
|
*/
|
|
1858
1922
|
_fetchedModels = /* @__PURE__ */ new Map();
|
|
1859
1923
|
_modelConfig = {};
|
|
@@ -1888,9 +1952,8 @@ class AparteConfig {
|
|
|
1888
1952
|
* silently, because the warning fires at most once per config.
|
|
1889
1953
|
*
|
|
1890
1954
|
* Each entry carries the element that registered it, so a request naming a `target`
|
|
1891
|
-
* is routed to the presenter in the SAME chat
|
|
1892
|
-
*
|
|
1893
|
-
* is what made that impossible.
|
|
1955
|
+
* is routed to the presenter in the SAME chat, as `AparteElicitationRequest.target`
|
|
1956
|
+
* documents ("used to resolve WHICH instance presents").
|
|
1894
1957
|
*/
|
|
1895
1958
|
_elicitationPresenters = [];
|
|
1896
1959
|
/** Warn once per config, not once per request — a tool loop can ask repeatedly. */
|
|
@@ -1900,22 +1963,21 @@ class AparteConfig {
|
|
|
1900
1963
|
*
|
|
1901
1964
|
* One request reaches the presenter at a time — not because a presenter could not
|
|
1902
1965
|
* cope, but because the COMPOSER cannot: it has one panel slot, and a second
|
|
1903
|
-
* request
|
|
1904
|
-
*
|
|
1905
|
-
*
|
|
1906
|
-
* lie as telling the model a stopped turn was refused. WAITING is the honest
|
|
1907
|
-
* behaviour.
|
|
1966
|
+
* request would clobber the first's DOM. A second request WAITS: answering it
|
|
1967
|
+
* `cancel` on the spot would be a refusal invented for a question nobody was ever
|
|
1968
|
+
* shown.
|
|
1908
1969
|
*
|
|
1909
1970
|
* Here rather than in the presenter because this is the only place that sees
|
|
1910
|
-
* EVERY request whatever raised it — a tool handler, an app's own button,
|
|
1911
|
-
*
|
|
1912
|
-
*
|
|
1971
|
+
* EVERY request whatever raised it — a tool handler, an app's own button, the
|
|
1972
|
+
* tool-approval gate. A custom presenter registered by a consumer gets the same
|
|
1973
|
+
* protection for free.
|
|
1913
1974
|
*/
|
|
1914
1975
|
_elicitationQueue = /* @__PURE__ */ new Map();
|
|
1915
1976
|
// Tool Registry
|
|
1916
1977
|
_tools = /* @__PURE__ */ new Map();
|
|
1917
1978
|
_toolRenderers = /* @__PURE__ */ new Map();
|
|
1918
1979
|
_toolSummaries = /* @__PURE__ */ new Map();
|
|
1980
|
+
_toolInputFormatters = /* @__PURE__ */ new Map();
|
|
1919
1981
|
/** Tagged blocks the stream parser recognises in the prose — see {@link registerStreamBlock}. */
|
|
1920
1982
|
_streamBlocks = /* @__PURE__ */ new Map();
|
|
1921
1983
|
_segmentDefaults = /* @__PURE__ */ new Map();
|
|
@@ -2008,6 +2070,8 @@ class AparteConfig {
|
|
|
2008
2070
|
*/
|
|
2009
2071
|
setMarkdownProvider(fn) {
|
|
2010
2072
|
this._markdownProvider = fn;
|
|
2073
|
+
bumpRenderingVersion(this);
|
|
2074
|
+
this._notify();
|
|
2011
2075
|
}
|
|
2012
2076
|
/**
|
|
2013
2077
|
* Set an incremental (streaming) Markdown renderer provider. Optional —
|
|
@@ -2022,6 +2086,8 @@ class AparteConfig {
|
|
|
2022
2086
|
*/
|
|
2023
2087
|
setStreamingMarkdownProvider(fn) {
|
|
2024
2088
|
this._streamingMarkdownProvider = fn;
|
|
2089
|
+
bumpRenderingVersion(this);
|
|
2090
|
+
this._notify();
|
|
2025
2091
|
}
|
|
2026
2092
|
/**
|
|
2027
2093
|
* Create an incremental Markdown renderer bound to `target`, or `null` when
|
|
@@ -2037,6 +2103,8 @@ class AparteConfig {
|
|
|
2037
2103
|
*/
|
|
2038
2104
|
setHighlightProvider(fn) {
|
|
2039
2105
|
this._highlightProvider = fn;
|
|
2106
|
+
bumpRenderingVersion(this);
|
|
2107
|
+
this._notify();
|
|
2040
2108
|
}
|
|
2041
2109
|
/**
|
|
2042
2110
|
* Whether a syntax-highlight provider is registered. Lets consumers (e.g.
|
|
@@ -2054,7 +2122,7 @@ class AparteConfig {
|
|
|
2054
2122
|
* @param sanitizer A sanitizer function (e.g. a DOMPurify wrapper) for
|
|
2055
2123
|
* hardened coverage, or `null` to DISABLE sanitization. Disabling exposes
|
|
2056
2124
|
* you to XSS from LLM-authored content — only do so for content you fully
|
|
2057
|
-
* trust and have already sanitized upstream.
|
|
2125
|
+
* trust and have already sanitized upstream. `null` prints a console warning, once.
|
|
2058
2126
|
* @example aparteGlobalConfig.setHtmlSanitizer((html) => DOMPurify.sanitize(html));
|
|
2059
2127
|
*/
|
|
2060
2128
|
setHtmlSanitizer(sanitizer) {
|
|
@@ -2104,30 +2172,54 @@ class AparteConfig {
|
|
|
2104
2172
|
return resolved.trim() || null;
|
|
2105
2173
|
}
|
|
2106
2174
|
/**
|
|
2107
|
-
* Set a custom Icon provider (e.g., Lucide, FontAwesome, Material)
|
|
2175
|
+
* Set a custom Icon provider (e.g., Lucide, FontAwesome, Material). It replaces the
|
|
2176
|
+
* whole set, icons added with `registerIcons` included; a name it leaves out keeps
|
|
2177
|
+
* core's drawing.
|
|
2108
2178
|
* @param provider Object implementing AparteIconProvider interface
|
|
2109
2179
|
*/
|
|
2110
2180
|
setIconProvider(provider) {
|
|
2111
2181
|
this._iconProvider = provider;
|
|
2112
2182
|
this._notify();
|
|
2113
2183
|
}
|
|
2184
|
+
/**
|
|
2185
|
+
* Add icons to the set, or redraw some of it, keeping the others. A name core does not
|
|
2186
|
+
* draw is welcome: an action's `icon`, `<aparte-composer-action icon>` and
|
|
2187
|
+
* `<aparte-icon name>` then find it. To type such a name, merge it into
|
|
2188
|
+
* `AparteIconProvider`: `declare module '@aparte/core' { interface AparteIconProvider { share?: () => string } }`.
|
|
2189
|
+
*
|
|
2190
|
+
* @example
|
|
2191
|
+
* aparteGlobalConfig.registerIcons({ share: () => '<svg class="aparte-icon" viewBox="0 0 24 24">…</svg>' });
|
|
2192
|
+
* aparteGlobalConfig.registerAction({ id: 'share', icon: 'share', label: 'Share', zones: ['bubble'] });
|
|
2193
|
+
*/
|
|
2194
|
+
registerIcons(icons) {
|
|
2195
|
+
const current = this._iconProvider;
|
|
2196
|
+
if (current && iconLayers.has(current)) {
|
|
2197
|
+
Object.assign(current, icons);
|
|
2198
|
+
} else {
|
|
2199
|
+
const layer = Object.assign(Object.create(current ?? null), icons);
|
|
2200
|
+
iconLayers.add(layer);
|
|
2201
|
+
this._iconProvider = layer;
|
|
2202
|
+
}
|
|
2203
|
+
this._notify();
|
|
2204
|
+
}
|
|
2114
2205
|
/**
|
|
2115
2206
|
* The icon set as a **complete** provider: every name resolves, falling back
|
|
2116
2207
|
* to `APARTE_DEFAULT_ICON_FALLBACKS` for anything the registered provider doesn't
|
|
2117
2208
|
* implement. Callers (bubble action bar, composer controls) can therefore
|
|
2118
2209
|
* invoke `icons.copy()` unconditionally.
|
|
2119
2210
|
*
|
|
2120
|
-
*
|
|
2121
|
-
*
|
|
2122
|
-
* the type now states — crashed every other icon with
|
|
2123
|
-
* "icons.retry is not a function".
|
|
2211
|
+
* A registered provider may implement only a subset of the names; the missing ones
|
|
2212
|
+
* fall back, so `icons.retry()` never throws "is not a function".
|
|
2124
2213
|
*/
|
|
2125
2214
|
getIconProvider() {
|
|
2126
2215
|
const registered = this._iconProvider;
|
|
2127
2216
|
const complete = {};
|
|
2128
2217
|
for (const name of Object.keys(APARTE_DEFAULT_ICON_FALLBACKS)) {
|
|
2129
|
-
|
|
2130
|
-
|
|
2218
|
+
complete[name] = drawingOf(registered, name) ?? (() => APARTE_DEFAULT_ICON_FALLBACKS[name]);
|
|
2219
|
+
}
|
|
2220
|
+
for (const name of namesOf(registered)) {
|
|
2221
|
+
const fn = drawingOf(registered, name);
|
|
2222
|
+
if (fn && !(name in complete)) complete[name] = fn;
|
|
2131
2223
|
}
|
|
2132
2224
|
return complete;
|
|
2133
2225
|
}
|
|
@@ -2259,14 +2351,10 @@ class AparteConfig {
|
|
|
2259
2351
|
this._locale = { ...this._locale, ...translations };
|
|
2260
2352
|
this._notify();
|
|
2261
2353
|
}
|
|
2262
|
-
/**
|
|
2263
|
-
* Get icon HTML string by name
|
|
2264
|
-
* Falls back to textual representation if no provider is set
|
|
2265
|
-
*/
|
|
2266
2354
|
getIcon(name) {
|
|
2267
|
-
const icon = this._iconProvider
|
|
2355
|
+
const icon = drawingOf(this._iconProvider, name);
|
|
2268
2356
|
if (icon) return icon();
|
|
2269
|
-
return APARTE_DEFAULT_ICON_FALLBACKS[name];
|
|
2357
|
+
return Object.prototype.hasOwnProperty.call(APARTE_DEFAULT_ICON_FALLBACKS, name) ? APARTE_DEFAULT_ICON_FALLBACKS[name] : void 0;
|
|
2270
2358
|
}
|
|
2271
2359
|
/**
|
|
2272
2360
|
* Set a custom Key provider (e.g., AparteVault override)
|
|
@@ -2275,26 +2363,16 @@ class AparteConfig {
|
|
|
2275
2363
|
this._keyProvider = provider;
|
|
2276
2364
|
}
|
|
2277
2365
|
/**
|
|
2278
|
-
* Add a key source
|
|
2279
|
-
*
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
2282
|
-
*
|
|
2283
|
-
*
|
|
2284
|
-
*
|
|
2285
|
-
*
|
|
2286
|
-
*
|
|
2287
|
-
*
|
|
2288
|
-
* The direction matters (decision #9): the client registers a source ON the
|
|
2289
|
-
* config, so the capability is never hostage to the client — a page that only
|
|
2290
|
-
* ever calls `setKeyProvider` is unchanged, and a page that constructs no
|
|
2291
|
-
* client at all still lists models.
|
|
2292
|
-
*
|
|
2293
|
-
* Every call is its own source, even when the same function is handed in
|
|
2294
|
-
* twice: two panes sharing one module-level resolver are two mounts, and the
|
|
2295
|
-
* first one to unmount must not take the second one's key with it. A `Set`
|
|
2296
|
-
* keyed by the function itself made them one entry, which either teardown
|
|
2297
|
-
* removed.
|
|
2366
|
+
* Add a key source beside the one {@link AparteConfig.setKeyProvider | setKeyProvider()}
|
|
2367
|
+
* holds, and return the function that removes it. The sources are asked in the order they
|
|
2368
|
+
* were added, then `setKeyProvider`'s, and the first answer wins. `AparteClient`'s
|
|
2369
|
+
* `keyResolver` is registered this way, so the model selector can list a cloud provider's
|
|
2370
|
+
* models too. Each call is a source of its own, even with the same function.
|
|
2371
|
+
* @example
|
|
2372
|
+
* const release = aparteGlobalConfig.registerKeyProvider((providerId) =>
|
|
2373
|
+
* providerId === 'openrouter' ? localStorage.getItem('openrouter-key') : undefined);
|
|
2374
|
+
* // when what owns the key goes away:
|
|
2375
|
+
* release();
|
|
2298
2376
|
*/
|
|
2299
2377
|
registerKeyProvider(provider) {
|
|
2300
2378
|
const entry = (providerId) => provider(providerId);
|
|
@@ -2330,9 +2408,10 @@ class AparteConfig {
|
|
|
2330
2408
|
return void 0;
|
|
2331
2409
|
}
|
|
2332
2410
|
/**
|
|
2333
|
-
*
|
|
2334
|
-
*
|
|
2335
|
-
*
|
|
2411
|
+
* Fetch a provider's models, with the key and endpoint its key sources answer, so a
|
|
2412
|
+
* component that lists models never handles a key. The list is kept, and
|
|
2413
|
+
* `getCurrentModel()` reads the selected model's capabilities from it. Answers `[]` for an
|
|
2414
|
+
* unknown provider, one with no `fetchModels`, or a fetch that fails (with a console warning).
|
|
2336
2415
|
*/
|
|
2337
2416
|
async refreshProviderModels(providerId) {
|
|
2338
2417
|
const provider = this._aiProviders.get(providerId);
|
|
@@ -2351,7 +2430,13 @@ class AparteConfig {
|
|
|
2351
2430
|
// AI Provider Management (BYORK - Bring Your Own Key)
|
|
2352
2431
|
// ─────────────────────────────────────────────────────────────────────────
|
|
2353
2432
|
/**
|
|
2354
|
-
* Register one or more AI providers (e.g., OpenRouter, Gemini, Anthropic)
|
|
2433
|
+
* Register one or more AI providers (e.g., OpenRouter, Gemini, Anthropic).
|
|
2434
|
+
*
|
|
2435
|
+
* When this leaves exactly one provider registered, offering exactly one model it knows
|
|
2436
|
+
* without a request (a scripted or an in-browser provider), and no model is selected,
|
|
2437
|
+
* that model is selected. A provider that lists its models over the network selects
|
|
2438
|
+
* nothing: name one with {@link AparteConfig.setModelConfig | setModelConfig()}, or let the
|
|
2439
|
+
* person pick in `<aparte-model-selector>`.
|
|
2355
2440
|
* @param providers AparteAIProvider implementations
|
|
2356
2441
|
* @example
|
|
2357
2442
|
* aparteGlobalConfig.registerAIProvider(OpenRouterProvider);
|
|
@@ -2416,11 +2501,14 @@ class AparteConfig {
|
|
|
2416
2501
|
/**
|
|
2417
2502
|
* Register a model preference provider for agnostic persistence.
|
|
2418
2503
|
* The host app decides how/where to store the selected provider & model.
|
|
2504
|
+
* `load` answers `{ provider, model }` — the names {@link AparteModelPreference} reads —
|
|
2505
|
+
* and {@link AparteConfig.restoreModelPreference | restoreModelPreference()} applies it.
|
|
2419
2506
|
* @example
|
|
2420
2507
|
* aparteGlobalConfig.setModelPreferenceProvider({
|
|
2421
|
-
* save: (
|
|
2422
|
-
* load: () => JSON.parse(localStorage.getItem('model') ?? 'null')
|
|
2508
|
+
* save: (provider, model) => localStorage.setItem('model', JSON.stringify({ provider, model })),
|
|
2509
|
+
* load: () => JSON.parse(localStorage.getItem('model') ?? 'null'),
|
|
2423
2510
|
* });
|
|
2511
|
+
* aparteGlobalConfig.restoreModelPreference();
|
|
2424
2512
|
*/
|
|
2425
2513
|
setModelPreferenceProvider(provider) {
|
|
2426
2514
|
this._modelPreferenceProvider = provider;
|
|
@@ -2467,9 +2555,10 @@ class AparteConfig {
|
|
|
2467
2555
|
return { verdict: tool?.needsApproval ? "ask" : "allow" };
|
|
2468
2556
|
}
|
|
2469
2557
|
/**
|
|
2470
|
-
* Restore
|
|
2558
|
+
* Restore the saved model preference via the registered provider.
|
|
2471
2559
|
* Should be called once at app startup, before any component mounts.
|
|
2472
|
-
* No-op if no provider is registered or nothing was saved.
|
|
2560
|
+
* No-op if no provider is registered or nothing was saved. The saved selection is
|
|
2561
|
+
* applied without being saved again.
|
|
2473
2562
|
*/
|
|
2474
2563
|
restoreModelPreference() {
|
|
2475
2564
|
if (!this._modelPreferenceProvider) return null;
|
|
@@ -2482,7 +2571,8 @@ class AparteConfig {
|
|
|
2482
2571
|
return null;
|
|
2483
2572
|
}
|
|
2484
2573
|
/**
|
|
2485
|
-
*
|
|
2574
|
+
* Select the default provider and model, merged over the current selection. With a
|
|
2575
|
+
* model-preference provider registered and both set, the selection is saved through it.
|
|
2486
2576
|
*/
|
|
2487
2577
|
setModelConfig(config) {
|
|
2488
2578
|
this._modelConfig = { ...this._modelConfig, ...config };
|
|
@@ -2526,34 +2616,20 @@ class AparteConfig {
|
|
|
2526
2616
|
hasSelectedModel() {
|
|
2527
2617
|
return !!(this._modelConfig.defaultProvider && this._modelConfig.defaultModel);
|
|
2528
2618
|
}
|
|
2529
|
-
/**
|
|
2530
|
-
* Opt-in UX: when enabled, `<aparte-composer>` blocks sending and greys out
|
|
2531
|
-
* until {@link hasSelectedModel} is true (e.g. while the model selector is
|
|
2532
|
-
* still fetching its list). Off by default so single-model / backend setups
|
|
2533
|
-
* that never select a model are unaffected.
|
|
2534
|
-
*/
|
|
2535
2619
|
/**
|
|
2536
2620
|
* Elicitation policy: whether a choice offers a free-text "Other…" escape.
|
|
2537
2621
|
*
|
|
2538
|
-
* This is the HOST's decision, not the model's
|
|
2539
|
-
* `allow_other` field
|
|
2540
|
-
*
|
|
2541
|
-
*
|
|
2542
|
-
*
|
|
2543
|
-
*
|
|
2544
|
-
* Every serious implementation of this pattern makes the escape hatch a property
|
|
2545
|
-
* of the surface rather than of the request. Default `true`, which is what the
|
|
2546
|
-
* panel did before, so nothing a user sees changes — only who gets to say so.
|
|
2547
|
-
* A direct `requestUserInput` caller can still set `allowOther` per field; that
|
|
2548
|
-
* is the app talking, and it wins.
|
|
2622
|
+
* This is the HOST's decision, not the model's: the `ask_user` schema handed to the
|
|
2623
|
+
* model carries no `allow_other` field, because a small model fills a field it does
|
|
2624
|
+
* not understand (a question with `allow_other: true` and no options renders a radio
|
|
2625
|
+
* list whose only entry is "Other…"). Default `true`. A direct `requestUserInput`
|
|
2626
|
+
* caller can still set `allowOther` per field; that is the app talking, and it wins.
|
|
2549
2627
|
*
|
|
2550
2628
|
* `layout` decides how a form of SEVERAL questions is presented.
|
|
2551
2629
|
* `'stepped'` (default) asks them one at a time with a chip per question;
|
|
2552
|
-
* `'stacked'` puts them all in the panel at once
|
|
2553
|
-
*
|
|
2554
|
-
*
|
|
2555
|
-
* No product asks a person two questions by stacking them in one box; it is kept
|
|
2556
|
-
* as an option because the form case is real, not because it was the right default.
|
|
2630
|
+
* `'stacked'` puts them all in the panel at once — the shape of an MCP elicitation
|
|
2631
|
+
* FORM collecting structured data, which is why it stays an option, but not the
|
|
2632
|
+
* right default for two different questions asked mid-conversation.
|
|
2557
2633
|
*
|
|
2558
2634
|
* `answerOnClick` decides how a question asked ON ITS OWN with a single choice — an
|
|
2559
2635
|
* `enum` without `multiple` or a `default`, a `boolean` without a `default` — is
|
|
@@ -2590,6 +2666,12 @@ class AparteConfig {
|
|
|
2590
2666
|
getElicitationFieldRenderer() {
|
|
2591
2667
|
return this._elicitationFieldRenderer;
|
|
2592
2668
|
}
|
|
2669
|
+
/**
|
|
2670
|
+
* Opt-in UX: when enabled, `<aparte-composer>` blocks sending and greys out
|
|
2671
|
+
* until {@link hasSelectedModel} is true (e.g. while the model selector is
|
|
2672
|
+
* still fetching its list). Off by default so single-model / backend setups
|
|
2673
|
+
* that never select a model are unaffected.
|
|
2674
|
+
*/
|
|
2593
2675
|
setRequireModelSelection(required) {
|
|
2594
2676
|
if (this._requireModelSelection === required) return;
|
|
2595
2677
|
this._requireModelSelection = required;
|
|
@@ -2625,7 +2707,8 @@ class AparteConfig {
|
|
|
2625
2707
|
// Public Rendering Methods (with Fallbacks)
|
|
2626
2708
|
// ─────────────────────────────────────────────────────────────────────────
|
|
2627
2709
|
/**
|
|
2628
|
-
*
|
|
2710
|
+
* The locale's string for `key`, or the default (English) locale's when the locale
|
|
2711
|
+
* leaves it out; `''` for a key neither declares.
|
|
2629
2712
|
*/
|
|
2630
2713
|
t(key) {
|
|
2631
2714
|
const val = this._locale[key] || APARTE_DEFAULT_LOCALE[key];
|
|
@@ -2665,7 +2748,7 @@ class AparteConfig {
|
|
|
2665
2748
|
}
|
|
2666
2749
|
/**
|
|
2667
2750
|
* Highlight code block
|
|
2668
|
-
* Fallback: Returns raw code wrapped in
|
|
2751
|
+
* Fallback: Returns raw code wrapped in `<pre><code>`
|
|
2669
2752
|
* Supports Promise if provider is async/streaming
|
|
2670
2753
|
*/
|
|
2671
2754
|
async highlightCode(code, lang) {
|
|
@@ -2701,18 +2784,11 @@ class AparteConfig {
|
|
|
2701
2784
|
return Array.from(this._tools.values()).map((e) => e.tool);
|
|
2702
2785
|
}
|
|
2703
2786
|
/**
|
|
2704
|
-
* The registered tools' own `systemPrompt`s,
|
|
2705
|
-
*
|
|
2706
|
-
*
|
|
2707
|
-
*
|
|
2708
|
-
*
|
|
2709
|
-
* writes it, sees the tool work (the model has the schema either way), and never learns
|
|
2710
|
-
* that the sentence explaining WHEN to reach for it was dropped. `@aparte/plugin-ask-user`
|
|
2711
|
-
* sets one, so a shipped plugin was losing its instructions.
|
|
2712
|
-
*
|
|
2713
|
-
* Registration order, blank line between, because two tools' instructions are two
|
|
2714
|
-
* paragraphs and not one run-on sentence. The app's own template stays a separate
|
|
2715
|
-
* message — see `resolveSystemPrompt`, which is about the app and not about the tools.
|
|
2787
|
+
* The registered tools' own `systemPrompt`s, in registration order with a blank line
|
|
2788
|
+
* between, or `null` if none sets one. `AparteClient` sends them with every request, as a
|
|
2789
|
+
* system message after the app's own ({@link AparteConfig.resolveSystemPrompt | resolveSystemPrompt()});
|
|
2790
|
+
* a request you assemble yourself should send them the same way, or the model loses the
|
|
2791
|
+
* tools' instructions.
|
|
2716
2792
|
*/
|
|
2717
2793
|
resolveToolSystemPrompts() {
|
|
2718
2794
|
const prompts = Array.from(this._tools.values()).map((entry) => entry.tool.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
|
|
@@ -2753,8 +2829,8 @@ class AparteConfig {
|
|
|
2753
2829
|
* is a string, so a consumer's own segment type is covered by the same call.
|
|
2754
2830
|
*
|
|
2755
2831
|
* The problem it solves: a consumer streaming a reply does not CONSTRUCT its
|
|
2756
|
-
* segments — the parser does — so
|
|
2757
|
-
* open in my app"
|
|
2832
|
+
* segments — the parser does — so a per-segment field cannot express "reasoning
|
|
2833
|
+
* blocks open in my app": per-segment fields only reach segments you build yourself.
|
|
2758
2834
|
*
|
|
2759
2835
|
* Applied where identity is stamped (`utils/segments.ts`), so it covers every
|
|
2760
2836
|
* path a segment can arrive by — the parser, `addSegment`, and the segments
|
|
@@ -2795,8 +2871,8 @@ class AparteConfig {
|
|
|
2795
2871
|
* Say what a call targets, in the default row, after the tool's name.
|
|
2796
2872
|
*
|
|
2797
2873
|
* The default row names the tool and keeps its arguments behind a disclosure, so a
|
|
2798
|
-
* reader
|
|
2799
|
-
* `registerToolRenderer` for that one line
|
|
2874
|
+
* reader sees that a file was read, not which one. Replacing the row with
|
|
2875
|
+
* `registerToolRenderer` for that one line would mean re-implementing its six states,
|
|
2800
2876
|
* the spinner, the disclosure and the relabel contract. This keeps all of them and
|
|
2801
2877
|
* adds the line. A tool with its own renderer draws its own row, so its summary is
|
|
2802
2878
|
* never read.
|
|
@@ -2822,6 +2898,27 @@ class AparteConfig {
|
|
|
2822
2898
|
getToolSummary(toolName) {
|
|
2823
2899
|
return this._toolSummaries.get(toolName);
|
|
2824
2900
|
}
|
|
2901
|
+
/**
|
|
2902
|
+
* Show a tool's arguments as a person reads them, in the approval panel and in the
|
|
2903
|
+
* default tool row's disclosure — one formatter for both. Without one, both show the
|
|
2904
|
+
* JSON the model sent.
|
|
2905
|
+
*
|
|
2906
|
+
* @example
|
|
2907
|
+
* // Approve "write_file notes.md (2 KB)" rather than a JSON object.
|
|
2908
|
+
* aparteGlobalConfig.registerToolInputFormatter('write_file', (input) =>
|
|
2909
|
+
* `${String(input['path'] ?? '')} (${Math.ceil(String(input['content'] ?? '').length / 1024)} KB)`);
|
|
2910
|
+
*/
|
|
2911
|
+
registerToolInputFormatter(toolName, formatter) {
|
|
2912
|
+
this._toolInputFormatters.set(toolName, formatter);
|
|
2913
|
+
}
|
|
2914
|
+
/** Unregister a tool's input formatter. */
|
|
2915
|
+
unregisterToolInputFormatter(toolName) {
|
|
2916
|
+
this._toolInputFormatters.delete(toolName);
|
|
2917
|
+
}
|
|
2918
|
+
/** Get the input formatter for a tool name. Returns undefined if none registered. */
|
|
2919
|
+
getToolInputFormatter(toolName) {
|
|
2920
|
+
return this._toolInputFormatters.get(toolName);
|
|
2921
|
+
}
|
|
2825
2922
|
/**
|
|
2826
2923
|
* Teach the stream parser a tagged block: `<tag attr="…">…</tag>` in the model's
|
|
2827
2924
|
* prose becomes the segment `toSegment` builds, streamed delta by delta.
|
|
@@ -2876,6 +2973,12 @@ class AparteConfig {
|
|
|
2876
2973
|
* confirmation, a text field, a form) and resolves with the user's answer.
|
|
2877
2974
|
* The `<aparte-elicitation>` Web Component registers itself here; an app can
|
|
2878
2975
|
* override with its own framework-native presenter. Pass `null` to clear.
|
|
2976
|
+
*
|
|
2977
|
+
* Several can be registered, one per chat: `owner` is an element of the chat the
|
|
2978
|
+
* presenter belongs to (`<aparte-elicitation>` passes itself). A request whose `target`
|
|
2979
|
+
* is inside that chat reaches its presenter; a request with no `target`, or from a chat
|
|
2980
|
+
* with no presenter of its own, reaches the most recently registered one. Registering a
|
|
2981
|
+
* presenter again makes it the most recent.
|
|
2879
2982
|
*/
|
|
2880
2983
|
setElicitationPresenter(presenter, owner) {
|
|
2881
2984
|
if (presenter === null) {
|
|
@@ -2910,7 +3013,7 @@ class AparteConfig {
|
|
|
2910
3013
|
* the same chat is the right one — found by comparing chat boundaries rather than by
|
|
2911
3014
|
* `contains`, because the presenter is a sibling of the transcript, not an ancestor
|
|
2912
3015
|
* of it. With no target, no match, or no boundary to compare, the top of the stack
|
|
2913
|
-
* is the answer
|
|
3016
|
+
* is the answer.
|
|
2914
3017
|
*/
|
|
2915
3018
|
_presenterFor(target) {
|
|
2916
3019
|
const stack = this._elicitationPresenters;
|
|
@@ -2927,35 +3030,21 @@ class AparteConfig {
|
|
|
2927
3030
|
return stack[stack.length - 1]?.fn;
|
|
2928
3031
|
}
|
|
2929
3032
|
/**
|
|
2930
|
-
* Ask the
|
|
2931
|
-
*
|
|
2932
|
-
* question is the schema, not a bespoke tool.
|
|
3033
|
+
* Ask the person for typed input in the middle of a run, and wait for the answer. The
|
|
3034
|
+
* primitive behind `ask_user` and tool approval: the kind of question is its schema.
|
|
2933
3035
|
*
|
|
2934
|
-
* **Resolves** `accept` with the value, or `decline` when the
|
|
3036
|
+
* **Resolves** `accept` with the value, or `decline` when the person declines.
|
|
2935
3037
|
*
|
|
2936
|
-
* **Rejects
|
|
2937
|
-
*
|
|
2938
|
-
*
|
|
2939
|
-
*
|
|
2940
|
-
* `await` it inside a `try`, or attach a `.catch()`.
|
|
3038
|
+
* **Rejects**, with no answer, with an {@link AparteElicitationAbortError} (its `name` is
|
|
3039
|
+
* `'AbortError'`) whose `reason` is `'aborted'` — the turn was stopped, the signal fired,
|
|
3040
|
+
* or another request took the question away — or `'no-presenter'`, when nothing is mounted
|
|
3041
|
+
* to ask it (with a console warning, once). `await` it in a `try`, or attach a `.catch()`.
|
|
2941
3042
|
*
|
|
2942
|
-
*
|
|
2943
|
-
*
|
|
2944
|
-
*
|
|
2945
|
-
*
|
|
2946
|
-
*
|
|
2947
|
-
* in the `.d.ts` and in the custom-elements manifest, which is the API surface the
|
|
2948
|
-
* docs site renders, so it was the most-read wrong sentence in the library.
|
|
2949
|
-
*
|
|
2950
|
-
* A value was removed rather than renamed on purpose: `cancel` was easy to handle as
|
|
2951
|
-
* though it were an answer, and the approval gate did exactly that — it read a stopped
|
|
2952
|
-
* turn as the user refusing a tool they were never shown.
|
|
2953
|
-
*
|
|
2954
|
-
* With no presenter it rejects `'no-presenter'` and warns once, because a request
|
|
2955
|
-
* nobody could see is a setup mistake only the developer can fix. The default
|
|
2956
|
-
* presenter is `<aparte-elicitation>`, which registers itself from its
|
|
2957
|
-
* `connectedCallback` — so it has to be IN THE DOM. A docs page of ours claimed it
|
|
2958
|
-
* "installs itself — nothing to register", which is how that was found.
|
|
3043
|
+
* The question goes to the presenter of the chat that holds `request.target` (see
|
|
3044
|
+
* {@link AparteConfig.setElicitationPresenter | setElicitationPresenter()});
|
|
3045
|
+
* `<aparte-elicitation>` is the default one, and registers itself once it is in the DOM.
|
|
3046
|
+
* A chat shows one question at a time: a second request in the same chat waits until the
|
|
3047
|
+
* first is answered or ends, and requests with no `target` share one queue.
|
|
2959
3048
|
*/
|
|
2960
3049
|
requestUserInput(request) {
|
|
2961
3050
|
const queue = this._elicitationQueueKey(request);
|
|
@@ -2975,11 +3064,10 @@ class AparteConfig {
|
|
|
2975
3064
|
*
|
|
2976
3065
|
* The wait exists so a second question cannot take the panel away from the one on
|
|
2977
3066
|
* screen — a property of ONE panel, and `_present` routes by target, so a chat is
|
|
2978
|
-
* the right unit.
|
|
2979
|
-
*
|
|
2980
|
-
*
|
|
2981
|
-
*
|
|
2982
|
-
* queue, because they all reach the same presenter — the top of the stack.
|
|
3067
|
+
* the right unit. One queue for the whole config would let an approval waiting in
|
|
3068
|
+
* the first chat hold the second chat's question behind it for ever. Requests that
|
|
3069
|
+
* name no chat share the `null` queue, because they all reach the same presenter —
|
|
3070
|
+
* the top of the stack.
|
|
2983
3071
|
*/
|
|
2984
3072
|
_elicitationQueueKey(request) {
|
|
2985
3073
|
return chatBoundaryOf(request.target);
|
|
@@ -2997,13 +3085,23 @@ class AparteConfig {
|
|
|
2997
3085
|
return Promise.reject(new AparteElicitationAbortError("no-presenter"));
|
|
2998
3086
|
}
|
|
2999
3087
|
if (request.signal?.aborted) return Promise.reject(new AparteElicitationAbortError("aborted"));
|
|
3000
|
-
|
|
3088
|
+
const answer = presenter(request);
|
|
3089
|
+
const signal = request.signal;
|
|
3090
|
+
if (!signal) return answer;
|
|
3091
|
+
return new Promise((resolve, reject) => {
|
|
3092
|
+
const onAbort = () => reject(new AparteElicitationAbortError("aborted"));
|
|
3093
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3094
|
+
answer.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
3095
|
+
});
|
|
3001
3096
|
}
|
|
3002
3097
|
/**
|
|
3003
|
-
*
|
|
3004
|
-
*
|
|
3005
|
-
*
|
|
3006
|
-
*
|
|
3098
|
+
* Put the configuration back to its defaults: the providers and the selected model,
|
|
3099
|
+
* the tools with their renderers and summaries, the renderers and render hooks, the
|
|
3100
|
+
* Markdown and highlight providers, the sanitizer, the system prompt, the locale, icons,
|
|
3101
|
+
* avatars, actions and host handlers, the conversation manager, the question presenters
|
|
3102
|
+
* and their queue. Three things stay: the transport, the key sources added with
|
|
3103
|
+
* {@link AparteConfig.registerKeyProvider | registerKeyProvider()} (an `AparteClient`'s
|
|
3104
|
+
* `keyResolver` among them), and the {@link AparteConfig.subscribe | subscribe()} listeners.
|
|
3007
3105
|
*/
|
|
3008
3106
|
reset() {
|
|
3009
3107
|
this._markdownProvider = void 0;
|
|
@@ -3031,6 +3129,7 @@ class AparteConfig {
|
|
|
3031
3129
|
this._tools.clear();
|
|
3032
3130
|
this._toolRenderers.clear();
|
|
3033
3131
|
this._toolSummaries.clear();
|
|
3132
|
+
this._toolInputFormatters.clear();
|
|
3034
3133
|
this._streamBlocks.clear();
|
|
3035
3134
|
this._modelConfig = {};
|
|
3036
3135
|
this._requireModelSelection = false;
|
|
@@ -3066,46 +3165,6 @@ function getGlobalConfig() {
|
|
|
3066
3165
|
return new AparteConfig();
|
|
3067
3166
|
}
|
|
3068
3167
|
const aparteGlobalConfig = getGlobalConfig();
|
|
3069
|
-
const APARTE_HOST_ATTR = "data-aparte-host";
|
|
3070
|
-
const CONFIG_SLOT = Symbol.for("aparte.instanceConfig");
|
|
3071
|
-
function notifyConfigChanged(root, next, previous) {
|
|
3072
|
-
if (next === previous) return;
|
|
3073
|
-
const call = (el2) => {
|
|
3074
|
-
const hook = el2.aparteConfigChanged;
|
|
3075
|
-
if (typeof hook === "function") hook.call(el2, next, previous);
|
|
3076
|
-
};
|
|
3077
|
-
call(root);
|
|
3078
|
-
for (const el2 of root.querySelectorAll("*")) call(el2);
|
|
3079
|
-
}
|
|
3080
|
-
function attachConfig(el2, config) {
|
|
3081
|
-
const previous = el2[CONFIG_SLOT] ?? resolveConfig(el2.parentElement);
|
|
3082
|
-
el2[CONFIG_SLOT] = config;
|
|
3083
|
-
el2.setAttribute(APARTE_HOST_ATTR, "");
|
|
3084
|
-
notifyConfigChanged(el2, config, previous);
|
|
3085
|
-
}
|
|
3086
|
-
function detachConfig(el2) {
|
|
3087
|
-
const previous = el2[CONFIG_SLOT];
|
|
3088
|
-
delete el2[CONFIG_SLOT];
|
|
3089
|
-
el2.removeAttribute(APARTE_HOST_ATTR);
|
|
3090
|
-
if (previous) notifyConfigChanged(el2, resolveConfig(el2.parentElement), previous);
|
|
3091
|
-
}
|
|
3092
|
-
function resolveConfig(el2) {
|
|
3093
|
-
const host = el2?.closest?.(`[${APARTE_HOST_ATTR}]`);
|
|
3094
|
-
return host?.[CONFIG_SLOT] ?? aparteGlobalConfig;
|
|
3095
|
-
}
|
|
3096
|
-
let _renderConfig = null;
|
|
3097
|
-
function runWithConfig(config, fn) {
|
|
3098
|
-
const prev = _renderConfig;
|
|
3099
|
-
_renderConfig = config;
|
|
3100
|
-
try {
|
|
3101
|
-
return fn();
|
|
3102
|
-
} finally {
|
|
3103
|
-
_renderConfig = prev;
|
|
3104
|
-
}
|
|
3105
|
-
}
|
|
3106
|
-
function contextConfig(el2) {
|
|
3107
|
-
return _renderConfig ?? resolveConfig(el2);
|
|
3108
|
-
}
|
|
3109
3168
|
const APARTE_CONFIG_CHANGE = "aparte-config-change";
|
|
3110
3169
|
function subscribeConfigChange(el2, handler) {
|
|
3111
3170
|
const listener = (e) => {
|
|
@@ -3163,6 +3222,7 @@ function writeStreamedMarkdown(host, contentEl, content, streaming) {
|
|
|
3163
3222
|
contentEl.innerHTML = contextConfig().renderMarkdown(content);
|
|
3164
3223
|
}
|
|
3165
3224
|
function highlightMarkdownFences(contentEl) {
|
|
3225
|
+
if (!contextConfig().hasHighlightProvider()) return;
|
|
3166
3226
|
const blocks = contentEl.querySelectorAll("pre > code:not([data-aparte-highlighted])");
|
|
3167
3227
|
for (const block of blocks) {
|
|
3168
3228
|
const pre = block.parentElement;
|
|
@@ -3179,6 +3239,16 @@ function highlightMarkdownFences(contentEl) {
|
|
|
3179
3239
|
const textRenderer = {
|
|
3180
3240
|
type: "text",
|
|
3181
3241
|
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>`,
|
|
3242
|
+
/*
|
|
3243
|
+
* A fence in a settled text segment is highlighted, as the thinking renderer does for
|
|
3244
|
+
* its own. The parser turns a streamed fence into a `code` segment, so this only ever
|
|
3245
|
+
* mattered for text built by hand (`setSegments`, a restored message, a server's) — and
|
|
3246
|
+
* the bubble's own pass skips any message that has segments, so that fence stayed bare.
|
|
3247
|
+
*/
|
|
3248
|
+
setup: (el2, segment) => {
|
|
3249
|
+
const contentEl = el2.querySelector(".aparte-segment-content");
|
|
3250
|
+
if (contentEl && segment.isStreaming !== true) highlightMarkdownFences(contentEl);
|
|
3251
|
+
},
|
|
3182
3252
|
update: (el2, segment) => {
|
|
3183
3253
|
const contentEl = el2.querySelector(".aparte-segment-content");
|
|
3184
3254
|
if (!contentEl) return;
|
|
@@ -3188,10 +3258,14 @@ const textRenderer = {
|
|
|
3188
3258
|
segment.content,
|
|
3189
3259
|
segment.isStreaming !== false
|
|
3190
3260
|
);
|
|
3261
|
+
if (segment.isStreaming === false) highlightMarkdownFences(contentEl);
|
|
3191
3262
|
},
|
|
3192
3263
|
getStyles: () => ``
|
|
3193
3264
|
};
|
|
3194
3265
|
const THINKING_ANCHOR_SLACK = 24;
|
|
3266
|
+
function labelOf(segment) {
|
|
3267
|
+
return segment.label && segment.label !== "Thinking" ? segment.label : contextConfig().t("thinking");
|
|
3268
|
+
}
|
|
3195
3269
|
const thinkingRenderer = {
|
|
3196
3270
|
type: "thinking",
|
|
3197
3271
|
/*
|
|
@@ -3222,13 +3296,13 @@ const thinkingRenderer = {
|
|
|
3222
3296
|
* only part that is genuinely about reasoning rather than about disclosure.
|
|
3223
3297
|
* `.aparte-thinking-label` stays too: `relabel` below queries it.
|
|
3224
3298
|
*/
|
|
3225
|
-
render: (segment) => `<details class="aparte-segment aparte-segment-thinking aparte-accordion__item" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed === false ? "open" : ""}><summary class="aparte-accordion__header aparte-thinking-header"><span class="aparte-thinking-label">${escapeHtml(segment
|
|
3299
|
+
render: (segment) => `<details class="aparte-segment aparte-segment-thinking aparte-accordion__item" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed === false ? "open" : ""}><summary class="aparte-accordion__header aparte-thinking-header"><span class="aparte-thinking-label">${escapeHtml(labelOf(segment))}</span><span class="aparte-accordion__icon">${contextConfig().getIcon("expand")}</span></summary><div class="aparte-accordion__panel aparte-thinking-content" role="region" tabindex="0" aria-label="${escapeAttr(labelOf(segment))}">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
|
|
3226
3300
|
/**
|
|
3227
3301
|
* The default label is the only config-derived text here — and `segment.label`
|
|
3228
3302
|
* still wins, exactly as in `render`, because that string is the app's.
|
|
3229
3303
|
*/
|
|
3230
3304
|
relabel: (el2, segment) => {
|
|
3231
|
-
const text = segment
|
|
3305
|
+
const text = labelOf(segment);
|
|
3232
3306
|
const label = el2.querySelector(".aparte-thinking-label");
|
|
3233
3307
|
if (label) label.textContent = text;
|
|
3234
3308
|
el2.querySelector(".aparte-thinking-content")?.setAttribute("aria-label", text);
|
|
@@ -3416,10 +3490,7 @@ const errorRenderer = {
|
|
|
3416
3490
|
`;
|
|
3417
3491
|
},
|
|
3418
3492
|
/**
|
|
3419
|
-
* The icon and the heading
|
|
3420
|
-
* while `locale.error` — a REQUIRED key, documented, and already translated to
|
|
3421
|
-
* "Erreur" — was read by nothing at all. A translated string with no consumer and
|
|
3422
|
-
* a literal with no translation, in the same card.
|
|
3493
|
+
* The icon and the heading (`locale.error`).
|
|
3423
3494
|
*
|
|
3424
3495
|
* Not the message: that is the model's or the transport's text, in whatever
|
|
3425
3496
|
* language it arrived in. Relabelling it would be inventing content.
|
|
@@ -3432,8 +3503,15 @@ const errorRenderer = {
|
|
|
3432
3503
|
},
|
|
3433
3504
|
getStyles: () => ``
|
|
3434
3505
|
};
|
|
3435
|
-
function describeToolInput(input) {
|
|
3506
|
+
function describeToolInput(input, format) {
|
|
3436
3507
|
if (!input || typeof input !== "object" || Object.keys(input).length === 0) return "";
|
|
3508
|
+
if (format) {
|
|
3509
|
+
try {
|
|
3510
|
+
const text = format(input);
|
|
3511
|
+
if (typeof text === "string") return text;
|
|
3512
|
+
} catch {
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3437
3515
|
try {
|
|
3438
3516
|
return JSON.stringify(input, null, 2);
|
|
3439
3517
|
} catch {
|
|
@@ -3441,7 +3519,7 @@ function describeToolInput(input) {
|
|
|
3441
3519
|
}
|
|
3442
3520
|
}
|
|
3443
3521
|
function describeInput(segment) {
|
|
3444
|
-
return describeToolInput(segment.toolCall?.input);
|
|
3522
|
+
return describeToolInput(segment.toolCall?.input, contextConfig().getToolInputFormatter(segment.toolCall?.name ?? ""));
|
|
3445
3523
|
}
|
|
3446
3524
|
function stateBadge(segment) {
|
|
3447
3525
|
const cfg = contextConfig();
|
|
@@ -3805,6 +3883,19 @@ function withoutDerivedTree(tree) {
|
|
|
3805
3883
|
messages: tree.messages.map((entry) => ({ ...entry, message: withoutDerived(entry.message) }))
|
|
3806
3884
|
};
|
|
3807
3885
|
}
|
|
3886
|
+
function stableStringify(value) {
|
|
3887
|
+
return JSON.stringify(value, (_key, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, v[k]])) : v);
|
|
3888
|
+
}
|
|
3889
|
+
function asContent(message) {
|
|
3890
|
+
return adoptMessageSegments(withoutDerived(message));
|
|
3891
|
+
}
|
|
3892
|
+
function contentOf(messages) {
|
|
3893
|
+
return stableStringify(messages.map(asContent));
|
|
3894
|
+
}
|
|
3895
|
+
function treeContentOf(tree) {
|
|
3896
|
+
const byId = [...tree.messages].sort((a, b) => a.message.id < b.message.id ? -1 : a.message.id > b.message.id ? 1 : 0);
|
|
3897
|
+
return stableStringify(byId.map((entry) => ({ parentId: entry.parentId, message: asContent(entry.message) })));
|
|
3898
|
+
}
|
|
3808
3899
|
class AparteConversationManager {
|
|
3809
3900
|
_adapter;
|
|
3810
3901
|
_conversations = [];
|
|
@@ -3995,9 +4086,9 @@ class AparteConversationManager {
|
|
|
3995
4086
|
*
|
|
3996
4087
|
* One append at a time per conversation: two sends that overlap (a suggestion and a
|
|
3997
4088
|
* quick type, coalesced into one new conversation) both suspend before reading the
|
|
3998
|
-
* record — for the messages, then for the title provider — so
|
|
3999
|
-
* conversation with no user message in it yet
|
|
4000
|
-
* title.
|
|
4089
|
+
* record — for the messages, then for the title provider — so the queue keeps the
|
|
4090
|
+
* second one from reading a conversation with no user message in it yet and
|
|
4091
|
+
* taking its text as the title.
|
|
4001
4092
|
*/
|
|
4002
4093
|
async addMessage(convId, msg) {
|
|
4003
4094
|
const previous = this._appending.get(convId);
|
|
@@ -4050,8 +4141,14 @@ class AparteConversationManager {
|
|
|
4050
4141
|
let contentChanged = true;
|
|
4051
4142
|
let treeChanged = tree !== void 0;
|
|
4052
4143
|
try {
|
|
4053
|
-
|
|
4054
|
-
|
|
4144
|
+
if (storedTree !== void 0 && conv.tree !== void 0) {
|
|
4145
|
+
const treesDiffer = treeContentOf(storedTree) !== treeContentOf(conv.tree);
|
|
4146
|
+
contentChanged = treesDiffer || storedTree.headId === conv.tree.headId && contentOf(storedMessages) !== contentOf(conv.messages);
|
|
4147
|
+
treeChanged = treesDiffer || storedTree.headId !== conv.tree.headId;
|
|
4148
|
+
} else {
|
|
4149
|
+
contentChanged = contentOf(storedMessages) !== contentOf(conv.messages);
|
|
4150
|
+
if (storedTree !== void 0) treeChanged = true;
|
|
4151
|
+
}
|
|
4055
4152
|
} catch {
|
|
4056
4153
|
}
|
|
4057
4154
|
let title;
|
|
@@ -4212,6 +4309,7 @@ function revokeAttachmentUrls(attachments) {
|
|
|
4212
4309
|
if (attachment.url?.startsWith("blob:")) URL.revokeObjectURL(attachment.url);
|
|
4213
4310
|
}
|
|
4214
4311
|
}
|
|
4312
|
+
const MAX_COMPACTION_SAVE_WAITS = 6;
|
|
4215
4313
|
class AparteConversationController {
|
|
4216
4314
|
_binding;
|
|
4217
4315
|
_options;
|
|
@@ -4227,6 +4325,7 @@ class AparteConversationController {
|
|
|
4227
4325
|
_onMessageAborted = null;
|
|
4228
4326
|
_onMessagePaused = null;
|
|
4229
4327
|
_onSelectConversation = null;
|
|
4328
|
+
_onCompactDone = null;
|
|
4230
4329
|
_unsubscribeManager = null;
|
|
4231
4330
|
/** The manager the subscription above is on, so a replaced manager is re-subscribed. */
|
|
4232
4331
|
_subscribedTo = null;
|
|
@@ -4289,9 +4388,9 @@ class AparteConversationController {
|
|
|
4289
4388
|
/**
|
|
4290
4389
|
* Degraded mode is legitimate — a host that never opted into the conversation
|
|
4291
4390
|
* lifecycle has no manager and should not be nagged. Registering one on the
|
|
4292
|
-
* WRONG config is not:
|
|
4293
|
-
*
|
|
4294
|
-
*
|
|
4391
|
+
* WRONG config is not: a manager registered on the global singleton while the chat
|
|
4392
|
+
* uses its own `config` looks like it works (the optimistic UI still appends) and
|
|
4393
|
+
* saves nothing at all.
|
|
4295
4394
|
*
|
|
4296
4395
|
* Warned once per controller, and only in the case that is unambiguously a
|
|
4297
4396
|
* mistake: no manager here, but one on the global.
|
|
@@ -4364,8 +4463,14 @@ class AparteConversationController {
|
|
|
4364
4463
|
if (targetId && targetId !== this._binding.hostId) return;
|
|
4365
4464
|
void this.setConversationId(evt.detail?.id ?? null);
|
|
4366
4465
|
};
|
|
4466
|
+
this._onCompactDone = (e) => {
|
|
4467
|
+
const detail = e.detail;
|
|
4468
|
+
if (detail?.targetId && detail.targetId !== this._binding.hostId) return;
|
|
4469
|
+
queueMicrotask(() => this._persistOnceSummarised(detail?.summary, 0));
|
|
4470
|
+
};
|
|
4367
4471
|
if (typeof window !== "undefined") {
|
|
4368
4472
|
window.addEventListener("aparte-conversation-select", this._onSelectConversation);
|
|
4473
|
+
window.addEventListener("aparte-compact-done", this._onCompactDone);
|
|
4369
4474
|
}
|
|
4370
4475
|
this._bound = true;
|
|
4371
4476
|
this._manager();
|
|
@@ -4409,6 +4514,10 @@ class AparteConversationController {
|
|
|
4409
4514
|
host.removeEventListener("aparte-message-paused", this._onMessagePaused);
|
|
4410
4515
|
this._onMessagePaused = null;
|
|
4411
4516
|
}
|
|
4517
|
+
if (this._onCompactDone && typeof window !== "undefined") {
|
|
4518
|
+
window.removeEventListener("aparte-compact-done", this._onCompactDone);
|
|
4519
|
+
this._onCompactDone = null;
|
|
4520
|
+
}
|
|
4412
4521
|
if (this._onSelectConversation && typeof window !== "undefined") {
|
|
4413
4522
|
window.removeEventListener("aparte-conversation-select", this._onSelectConversation);
|
|
4414
4523
|
this._onSelectConversation = null;
|
|
@@ -4552,6 +4661,22 @@ class AparteConversationController {
|
|
|
4552
4661
|
* re-enter streaming UI (caret, spinner, no action buttons) for content
|
|
4553
4662
|
* that was partially written before a switch or component destruction.
|
|
4554
4663
|
*/
|
|
4664
|
+
/**
|
|
4665
|
+
* Save once the binding holds the summary a compaction announced. A transcript kept in
|
|
4666
|
+
* a framework store reaches the binding only when that framework renders, so a save on
|
|
4667
|
+
* the microtask wrote the conversation as it stood before the compaction. Bounded: a
|
|
4668
|
+
* run with no summary (skipped), or a binding that never shows it, is saved as it is.
|
|
4669
|
+
*/
|
|
4670
|
+
_persistOnceSummarised(summary, waits) {
|
|
4671
|
+
const holds = !summary || this._binding.getMessages().some(
|
|
4672
|
+
(m) => m.compaction === true && typeof m.content === "string" && m.content.endsWith(summary)
|
|
4673
|
+
);
|
|
4674
|
+
if (holds || !this._binding.afterRender || waits >= MAX_COMPACTION_SAVE_WAITS) {
|
|
4675
|
+
this._persistActive();
|
|
4676
|
+
return;
|
|
4677
|
+
}
|
|
4678
|
+
this._binding.afterRender(() => this._persistOnceSummarised(summary, waits + 1));
|
|
4679
|
+
}
|
|
4555
4680
|
_persistActive() {
|
|
4556
4681
|
if (!this._activeId) return;
|
|
4557
4682
|
const manager = this._manager();
|
|
@@ -4609,7 +4734,27 @@ class AparteConversationController {
|
|
|
4609
4734
|
function cssEscape(value) {
|
|
4610
4735
|
return value.replace(/["\\]/g, "\\$&").replace(/[\n\r\f]/g, (c) => `\\${c.charCodeAt(0).toString(16)} `);
|
|
4611
4736
|
}
|
|
4737
|
+
function gateGlide(el2) {
|
|
4738
|
+
if (el2.hasAttribute("data-glide")) return () => {
|
|
4739
|
+
};
|
|
4740
|
+
const doc = el2.ownerDocument;
|
|
4741
|
+
const off = () => {
|
|
4742
|
+
doc.removeEventListener("pointerdown", lift, true);
|
|
4743
|
+
doc.removeEventListener("keydown", lift, true);
|
|
4744
|
+
el2.removeEventListener("aparte-send", lift);
|
|
4745
|
+
};
|
|
4746
|
+
const lift = () => {
|
|
4747
|
+
el2.setAttribute("data-glide", "");
|
|
4748
|
+
off();
|
|
4749
|
+
};
|
|
4750
|
+
doc.addEventListener("pointerdown", lift, true);
|
|
4751
|
+
doc.addEventListener("keydown", lift, true);
|
|
4752
|
+
el2.addEventListener("aparte-send", lift);
|
|
4753
|
+
return off;
|
|
4754
|
+
}
|
|
4612
4755
|
const MAX_SIBLING_RETRIES = 6;
|
|
4756
|
+
const TREE_OWNED_FIELDS = /* @__PURE__ */ new Set(["id", "branches", "activeBranchIndex", "isStreaming"]);
|
|
4757
|
+
const ALWAYS_PRESENT_FIELDS = /* @__PURE__ */ new Set(["content", "segments", "status"]);
|
|
4613
4758
|
class AparteChatHost {
|
|
4614
4759
|
binding;
|
|
4615
4760
|
options;
|
|
@@ -4668,6 +4813,7 @@ class AparteChatHost {
|
|
|
4668
4813
|
}
|
|
4669
4814
|
this._installHostMethods(host);
|
|
4670
4815
|
this._installLifecycleListeners(host);
|
|
4816
|
+
this._teardown.push(gateGlide(host));
|
|
4671
4817
|
this._installViewportListeners();
|
|
4672
4818
|
this._createController(host);
|
|
4673
4819
|
return () => this.unbind();
|
|
@@ -4747,6 +4893,9 @@ class AparteChatHost {
|
|
|
4747
4893
|
importTree: (tree) => {
|
|
4748
4894
|
this._vp()?.importTree?.(tree);
|
|
4749
4895
|
},
|
|
4896
|
+
// The framework's list arrives at its render, so a save that must see a write
|
|
4897
|
+
// the app made to its own store (a compaction through `resolveTarget`) waits for it.
|
|
4898
|
+
afterRender: (cb) => this.binding.afterRender(cb),
|
|
4750
4899
|
// One writer for the wait. A wrapper that hears `onLoadingChange` draws the
|
|
4751
4900
|
// wait itself and writes the viewport's attribute from its own
|
|
4752
4901
|
// `loading || hostLoading` — so the host writing it too meant that, with the
|
|
@@ -4909,11 +5058,8 @@ class AparteChatHost {
|
|
|
4909
5058
|
* the same direct path `appendToken` uses;
|
|
4910
5059
|
* 2. into the framework's message list, **coalesced to once per frame**.
|
|
4911
5060
|
*
|
|
4912
|
-
*
|
|
4913
|
-
* `
|
|
4914
|
-
* framework render per token. At 60 tok/s from a local model that is unusable,
|
|
4915
|
-
* and nothing in the imperative API hinted that `appendToSegment` cost so much
|
|
4916
|
-
* more than `injectTokenStream`. Consumers had to write their own rAF batcher.
|
|
5061
|
+
* Coalescing (2) avoids a full list rebuild plus `setMessages` +
|
|
5062
|
+
* `onMessagesChange`, i.e. one complete framework render, for every chunk.
|
|
4917
5063
|
*
|
|
4918
5064
|
* Any structural change flushes the buffer first (see
|
|
4919
5065
|
* {@link _flushStreamState}), so ordering is never observable.
|
|
@@ -4999,9 +5145,7 @@ class AparteChatHost {
|
|
|
4999
5145
|
/**
|
|
5000
5146
|
* Clear all messages + reset viewport state. The option travels to the viewport
|
|
5001
5147
|
* untouched: a compaction passes `{ revokeAttachments: false }` because it re-appends
|
|
5002
|
-
* the turns it keeps and revokes the dropped ones' object URLs itself
|
|
5003
|
-
* framework wrapper this bridge used to drop the argument, and every surviving
|
|
5004
|
-
* attachment came back broken.
|
|
5148
|
+
* the turns it keeps and revokes the dropped ones' object URLs itself.
|
|
5005
5149
|
*/
|
|
5006
5150
|
clearMessages(options) {
|
|
5007
5151
|
this._beginConversationSwap();
|
|
@@ -5045,10 +5189,9 @@ class AparteChatHost {
|
|
|
5045
5189
|
*
|
|
5046
5190
|
* Each token reaches the bubble immediately and the framework's message list
|
|
5047
5191
|
* is synced **once per frame** (same discipline as {@link appendToSegment}),
|
|
5048
|
-
* with a guaranteed flush before completion and on abort
|
|
5049
|
-
*
|
|
5050
|
-
*
|
|
5051
|
-
* bubble (which renders from that state) showed nothing at all.
|
|
5192
|
+
* with a guaranteed flush before completion and on abort, so the framework list
|
|
5193
|
+
* and the DOM agree and a custom bubble (which renders from that state) shows the
|
|
5194
|
+
* reply.
|
|
5052
5195
|
*/
|
|
5053
5196
|
async streamTokens(messageId, tokens) {
|
|
5054
5197
|
this.stopTokenStream();
|
|
@@ -5063,6 +5206,14 @@ class AparteChatHost {
|
|
|
5063
5206
|
}
|
|
5064
5207
|
ac.signal.addEventListener("abort", () => resolve("aborted"), { once: true });
|
|
5065
5208
|
});
|
|
5209
|
+
const { host, hostId } = this.binding;
|
|
5210
|
+
const onAbort = (e) => {
|
|
5211
|
+
const target = e.detail?.targetId;
|
|
5212
|
+
if ((!target || target === hostId) && this._streamAbort === ac) this.stopTokenStream();
|
|
5213
|
+
};
|
|
5214
|
+
window.addEventListener("aparte-abort", onAbort);
|
|
5215
|
+
const replaced = () => this._streamAbort !== void 0 && this._streamAbort !== ac;
|
|
5216
|
+
dispatchLifecycleEvent(host, "aparte-message-start", { messageId, role: "assistant" }, hostId);
|
|
5066
5217
|
try {
|
|
5067
5218
|
for (; ; ) {
|
|
5068
5219
|
const res = await Promise.race([iterator.next(), aborted]);
|
|
@@ -5072,12 +5223,24 @@ class AparteChatHost {
|
|
|
5072
5223
|
}
|
|
5073
5224
|
this._flushStreamState();
|
|
5074
5225
|
this._vp()?.completeMessage?.(messageId);
|
|
5075
|
-
|
|
5226
|
+
if (!replaced()) {
|
|
5227
|
+
this._setStreamingId(null);
|
|
5228
|
+
dispatchLifecycleEvent(
|
|
5229
|
+
host,
|
|
5230
|
+
ac.signal.aborted ? "aparte-message-aborted" : "aparte-message-done",
|
|
5231
|
+
ac.signal.aborted ? { messageId } : { messageId, role: "assistant" },
|
|
5232
|
+
hostId
|
|
5233
|
+
);
|
|
5234
|
+
}
|
|
5076
5235
|
} catch (err) {
|
|
5077
5236
|
this._flushStreamState();
|
|
5078
|
-
|
|
5237
|
+
if (!replaced()) {
|
|
5238
|
+
this._setStreamingId(null);
|
|
5239
|
+
dispatchLifecycleEvent(host, "aparte-message-error", { messageId, error: err }, hostId);
|
|
5240
|
+
}
|
|
5079
5241
|
throw err;
|
|
5080
5242
|
} finally {
|
|
5243
|
+
window.removeEventListener("aparte-abort", onAbort);
|
|
5081
5244
|
if (ac.signal.aborted) {
|
|
5082
5245
|
try {
|
|
5083
5246
|
await iterator.return?.();
|
|
@@ -5139,7 +5302,18 @@ class AparteChatHost {
|
|
|
5139
5302
|
if (msg.segments !== void 0) updates.segments = msg.segments;
|
|
5140
5303
|
if (msg.status !== void 0) updates.status = msg.status;
|
|
5141
5304
|
if (msg.usage !== void 0) updates.usage = msg.usage;
|
|
5305
|
+
if (msg.model !== void 0) updates.model = msg.model;
|
|
5142
5306
|
if (Object.keys(updates).length > 0) vp.updateMessage?.(msg.id, updates);
|
|
5307
|
+
const stored = vp.getMessage?.(msg.id);
|
|
5308
|
+
if (stored) {
|
|
5309
|
+
for (const [key, value] of Object.entries(msg)) {
|
|
5310
|
+
if (value !== void 0 && !(key in updates) && !TREE_OWNED_FIELDS.has(key)) stored[key] = value;
|
|
5311
|
+
}
|
|
5312
|
+
const own = msg;
|
|
5313
|
+
for (const key of Object.keys(stored)) {
|
|
5314
|
+
if (!TREE_OWNED_FIELDS.has(key) && !ALWAYS_PRESENT_FIELDS.has(key) && own[key] === void 0) delete stored[key];
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
5143
5317
|
}
|
|
5144
5318
|
}
|
|
5145
5319
|
}
|
|
@@ -5147,17 +5321,10 @@ class AparteChatHost {
|
|
|
5147
5321
|
* Write the branch counts onto their bubbles — and RESCHEDULE for the ones that are
|
|
5148
5322
|
* not on the page yet, instead of dropping them.
|
|
5149
5323
|
*
|
|
5150
|
-
*
|
|
5151
|
-
*
|
|
5152
|
-
*
|
|
5153
|
-
*
|
|
5154
|
-
*
|
|
5155
|
-
* It needed a framework that renders LATE, which is every framework here. React's
|
|
5156
|
-
* `afterRender` is `requestAnimationFrame(() => cb())` — a bet that the next paint
|
|
5157
|
-
* lands after React's commit, and it does not always; the repo has lost that same bet
|
|
5158
|
-
* before (`25f356b`, "a bet on rAF phase"). The fix is here rather than in the
|
|
5159
|
-
* wrapper's rAF call, because ANY binding whose `afterRender` can precede its commit
|
|
5160
|
-
* hits this, and one fix at the host covers all four.
|
|
5324
|
+
* Frameworks render LATE: a binding's `afterRender` can run before its commit, so a
|
|
5325
|
+
* bubble may not exist yet when its count arrives. A count that was dropped would
|
|
5326
|
+
* leave the bubble at one sibling, the picker hidden and the other version
|
|
5327
|
+
* unreachable. The retry lives here rather than in a wrapper, so it covers all four.
|
|
5161
5328
|
*
|
|
5162
5329
|
* Bounded, because a sibling can legitimately never arrive — a message that left the
|
|
5163
5330
|
* path has no bubble and never will, and an unbounded retry would hold a callback for
|
|
@@ -5205,12 +5372,11 @@ class AparteChatHost {
|
|
|
5205
5372
|
* The bubble the streaming writes below belong to: the one carrying that message's
|
|
5206
5373
|
* id, and NOTHING when it is not on the page yet.
|
|
5207
5374
|
*
|
|
5208
|
-
* It
|
|
5375
|
+
* It is not "the last bubble in the viewport", which is a different message
|
|
5209
5376
|
* whenever the DOM is a render behind the list — which is every framework here.
|
|
5210
|
-
* `appendMessage` puts the new reply in
|
|
5211
|
-
* schedule, so a first chunk arriving in between
|
|
5212
|
-
*
|
|
5213
|
-
* `data-segment-id` was on two bubbles.
|
|
5377
|
+
* `appendMessage` puts the new reply in the framework's state and the framework
|
|
5378
|
+
* paints it on its own schedule, so a first chunk arriving in between would go into
|
|
5379
|
+
* the PREVIOUS reply's bubble.
|
|
5214
5380
|
*
|
|
5215
5381
|
* Skipping the paint is safe because it is only ever a paint: the message list is
|
|
5216
5382
|
* written either way, and `syncBubbles` — called by the framework after each render,
|
|
@@ -5814,12 +5980,15 @@ function el(tag, className, text) {
|
|
|
5814
5980
|
if (text != null) node.textContent = text;
|
|
5815
5981
|
return node;
|
|
5816
5982
|
}
|
|
5983
|
+
let askSeq = 0;
|
|
5817
5984
|
function buildApprovalPanel(message, options, onChange, details) {
|
|
5818
5985
|
const cfg = contextConfig();
|
|
5819
5986
|
const panel = el("div", "aparte-elic-panel aparte-approval-panel");
|
|
5987
|
+
const body = el("div", "aparte-elic-body");
|
|
5988
|
+
panel.appendChild(body);
|
|
5820
5989
|
const askText = () => typeof message === "function" ? message() : message;
|
|
5821
5990
|
const ask = askText() ? el("p", "aparte-elic-message", askText()) : null;
|
|
5822
|
-
if (ask)
|
|
5991
|
+
if (ask) body.appendChild(ask);
|
|
5823
5992
|
let argsLabel = null;
|
|
5824
5993
|
if (details) {
|
|
5825
5994
|
argsLabel = el("div", "aparte-approval-args-label", cfg.t("approvalArgsLabel"));
|
|
@@ -5829,7 +5998,7 @@ function buildApprovalPanel(message, options, onChange, details) {
|
|
|
5829
5998
|
pre.setAttribute("role", "group");
|
|
5830
5999
|
pre.setAttribute("aria-labelledby", argsLabel.id);
|
|
5831
6000
|
pre.tabIndex = 0;
|
|
5832
|
-
|
|
6001
|
+
body.append(argsLabel, pre);
|
|
5833
6002
|
}
|
|
5834
6003
|
const dismiss = el("div", "aparte-elic-dismiss");
|
|
5835
6004
|
panel.appendChild(dismiss);
|
|
@@ -5843,6 +6012,10 @@ function buildApprovalPanel(message, options, onChange, details) {
|
|
|
5843
6012
|
const row = el("div", "aparte-approval-options");
|
|
5844
6013
|
row.setAttribute("role", "group");
|
|
5845
6014
|
row.setAttribute("aria-label", cfg.t("approvalOptionsLabel"));
|
|
6015
|
+
if (ask) {
|
|
6016
|
+
ask.id ||= `aparte-approval-ask-${++askSeq}`;
|
|
6017
|
+
row.setAttribute("aria-describedby", ask.id);
|
|
6018
|
+
}
|
|
5846
6019
|
const labelled = [];
|
|
5847
6020
|
const textOf = (option) => typeof option.label === "function" ? option.label() : option.label;
|
|
5848
6021
|
const descriptionOf = (option) => typeof option.description === "function" ? option.description() : option.description;
|
|
@@ -5856,13 +6029,13 @@ function buildApprovalPanel(message, options, onChange, details) {
|
|
|
5856
6029
|
row.appendChild(button);
|
|
5857
6030
|
labelled.push({ button, option });
|
|
5858
6031
|
}
|
|
5859
|
-
|
|
6032
|
+
body.appendChild(row);
|
|
5860
6033
|
const instruction = el("textarea", "aparte-approval-instruction");
|
|
5861
6034
|
instruction.rows = 1;
|
|
5862
6035
|
instruction.placeholder = cfg.t("approvalInstructionPlaceholder");
|
|
5863
6036
|
instruction.setAttribute("aria-label", cfg.t("approvalInstructionPlaceholder"));
|
|
5864
6037
|
instruction.addEventListener("input", onChange);
|
|
5865
|
-
|
|
6038
|
+
body.appendChild(instruction);
|
|
5866
6039
|
const typed = () => instruction.value.trim();
|
|
5867
6040
|
return {
|
|
5868
6041
|
el: panel,
|
|
@@ -5891,6 +6064,8 @@ function requestUserInput(request) {
|
|
|
5891
6064
|
return resolveConfig(request.target ?? null).requestUserInput(request);
|
|
5892
6065
|
}
|
|
5893
6066
|
const COMPACTION_PREAMBLE = "The earlier part of this conversation was compacted. This is a summary of what came before; continue from it.";
|
|
6067
|
+
const NOT_RUN_AFTER_A_REFUSAL = "Not run: an earlier call of the same step was refused.";
|
|
6068
|
+
const approvalKey = (call) => JSON.stringify([call.id, call.name, call.input]);
|
|
5894
6069
|
const warnedUninlinedFiles = /* @__PURE__ */ new Set();
|
|
5895
6070
|
const CHAT_SELECTOR = "aparte-chat, aparte-chat-viewport, [data-aparte-chat]";
|
|
5896
6071
|
function remoteProvider(id) {
|
|
@@ -5917,15 +6092,13 @@ class AparteClient {
|
|
|
5917
6092
|
/**
|
|
5918
6093
|
* Does this window event belong to this client?
|
|
5919
6094
|
*
|
|
5920
|
-
* One rule for all
|
|
5921
|
-
*
|
|
5922
|
-
*
|
|
5923
|
-
* two paid summarisation calls against whichever chat the DOM scan found
|
|
5924
|
-
* first, and a global reset that wiped the other conversation.
|
|
6095
|
+
* One rule for all the window-event handlers, so none of them can lack the guard:
|
|
6096
|
+
* in a two-client layout an unguarded event would make BOTH clients run — two paid
|
|
6097
|
+
* calls against whichever chat the DOM scan found first.
|
|
5925
6098
|
*
|
|
5926
|
-
* A scoped client also answers only events ADDRESSED to it
|
|
5927
|
-
*
|
|
5928
|
-
*
|
|
6099
|
+
* A scoped client also answers only events ADDRESSED to it; an untargeted event is
|
|
6100
|
+
* not let through to every scoped client, or one broadcast would act on every chat
|
|
6101
|
+
* on the page.
|
|
5929
6102
|
*/
|
|
5930
6103
|
_isForThisInstance(e) {
|
|
5931
6104
|
if (!e) return true;
|
|
@@ -5943,6 +6116,11 @@ class AparteClient {
|
|
|
5943
6116
|
options;
|
|
5944
6117
|
/** Config read by this client — an instance config, or the global default. */
|
|
5945
6118
|
_config;
|
|
6119
|
+
/**
|
|
6120
|
+
* A `keyResolver` becomes a key source on the client's config at once, before `start()`,
|
|
6121
|
+
* so everything that needs a key can use it — the model selector's list included.
|
|
6122
|
+
* `autoRegister` (on by default) installs core's built-in renderers on that config.
|
|
6123
|
+
*/
|
|
5946
6124
|
constructor(options = {}) {
|
|
5947
6125
|
this.options = {
|
|
5948
6126
|
autoRegister: true,
|
|
@@ -5960,17 +6138,15 @@ class AparteClient {
|
|
|
5960
6138
|
/**
|
|
5961
6139
|
* What every fresh user action does before it starts a turn.
|
|
5962
6140
|
*
|
|
5963
|
-
* Clearing the abort flag
|
|
6141
|
+
* Clearing the abort flag happens at the START of the action rather than inside
|
|
5964
6142
|
* `_streamTurn`, which runs after the auth and file-reading awaits — a reset there
|
|
5965
|
-
*
|
|
5966
|
-
* to be read.
|
|
6143
|
+
* would erase an abort that arrived while the user was still waiting for a large
|
|
6144
|
+
* attachment to be read.
|
|
5967
6145
|
*
|
|
5968
6146
|
* The superseded turn's TOOL handlers are not cancelled here, and do not need a
|
|
5969
6147
|
* registry of their own: `_streamTurn` aborts the previous `_streamController`, and
|
|
5970
6148
|
* the engine links every tool handler's controller to that run signal
|
|
5971
|
-
* (`invokeToolHandler`), so cutting the stream cuts its tools.
|
|
5972
|
-
* used to sit here for that job and was never added to — three lines of loop over an
|
|
5973
|
-
* empty set, plus a paragraph describing a cancellation the method did not perform.
|
|
6149
|
+
* (`invokeToolHandler`), so cutting the stream cuts its tools.
|
|
5974
6150
|
*/
|
|
5975
6151
|
_beginUserTurn() {
|
|
5976
6152
|
this._isAborted = false;
|
|
@@ -5979,10 +6155,9 @@ class AparteClient {
|
|
|
5979
6155
|
* Put this client's `keyResolver` on its config, and hold the teardown so
|
|
5980
6156
|
* `stop()` can take it back off.
|
|
5981
6157
|
*
|
|
5982
|
-
* The
|
|
5983
|
-
*
|
|
5984
|
-
*
|
|
5985
|
-
* one it meant to replace instead of replacing it. Idempotent, so the usual
|
|
6158
|
+
* The teardown means a stopped client stops signing model-list requests, and a
|
|
6159
|
+
* remount — the way the wrappers document swapping options — replaces the source
|
|
6160
|
+
* instead of adding one behind it. Idempotent, so the usual
|
|
5986
6161
|
* `new AparteClient({…}).start()` registers once.
|
|
5987
6162
|
*/
|
|
5988
6163
|
_registerKeyResolver() {
|
|
@@ -6004,7 +6179,9 @@ class AparteClient {
|
|
|
6004
6179
|
};
|
|
6005
6180
|
}
|
|
6006
6181
|
/**
|
|
6007
|
-
*
|
|
6182
|
+
* Answer the chats: listen on `window` for `aparte-send`, `aparte-retry`, `aparte-edit`,
|
|
6183
|
+
* `aparte-abort` and `aparte-conversation-loaded`. Nothing streams before this is
|
|
6184
|
+
* called. Starting a stopped client puts its `keyResolver` back on the config.
|
|
6008
6185
|
*/
|
|
6009
6186
|
start() {
|
|
6010
6187
|
this._registerKeyResolver();
|
|
@@ -6047,7 +6224,8 @@ class AparteClient {
|
|
|
6047
6224
|
window.addEventListener("aparte-conversation-loaded", this._boundLoadedHandler);
|
|
6048
6225
|
}
|
|
6049
6226
|
/**
|
|
6050
|
-
* Stop
|
|
6227
|
+
* Stop answering: abort the reply in flight, take this client's `keyResolver` off the
|
|
6228
|
+
* config, and remove the listeners.
|
|
6051
6229
|
*/
|
|
6052
6230
|
stop() {
|
|
6053
6231
|
this.abort();
|
|
@@ -6081,41 +6259,24 @@ class AparteClient {
|
|
|
6081
6259
|
this._isAborted = true;
|
|
6082
6260
|
this._streamController?.abort();
|
|
6083
6261
|
}
|
|
6084
|
-
/*
|
|
6085
|
-
* Two docblocks stood here, both describing `_awaitToolDecision` — the method
|
|
6086
|
-
* this replaced. Deleting a method and leaving its documentation is worse than
|
|
6087
|
-
* leaving the method: only the LAST comment before a declaration is its JSDoc,
|
|
6088
|
-
* so those two were floating prose, and one of them explained a DOM-containment
|
|
6089
|
-
* check as a live security property. It was the fix for a cross-chat hazard that
|
|
6090
|
-
* only existed because the decision travelled as a bubbling `document` event.
|
|
6091
|
-
* Routing through `requestUserInput` removed the hazard rather than guarding it,
|
|
6092
|
-
* so the guard is gone too — and a reader who found that comment would have
|
|
6093
|
-
* believed a containment check was still protecting them.
|
|
6094
|
-
*/
|
|
6095
6262
|
/**
|
|
6096
6263
|
* Ask the human at the COMPOSER, and report what they decided.
|
|
6097
6264
|
*
|
|
6098
|
-
* The default channel
|
|
6099
|
-
* that existed only because the buttons lived in a segment renderer with no
|
|
6100
|
-
* reference to this client. Routing through `requestUserInput` puts the decision
|
|
6265
|
+
* The default channel. Routing through `requestUserInput` puts the decision
|
|
6101
6266
|
* where every other request for the user already goes, which is the whole point:
|
|
6102
6267
|
* one queue, one panel slot, one teardown, and the same behaviour whether the
|
|
6103
6268
|
* request came from a tool handler or from this gate.
|
|
6104
6269
|
*
|
|
6105
6270
|
* The tool NAME is the question and its ARGUMENTS are the body, because the
|
|
6106
6271
|
* arguments are what is being approved — a decision surface that names the act and
|
|
6107
|
-
* hides its subject asks for a signature on a blank page.
|
|
6108
|
-
*
|
|
6109
|
-
* it had always promised what the panel did not do. The transcript row stays the
|
|
6110
|
-
* anchor; it is no longer the only place the call can be read. The panel is capped
|
|
6272
|
+
* hides its subject asks for a signature on a blank page. The transcript row stays the
|
|
6273
|
+
* anchor, but it is not the only place the call can be read. The panel is capped
|
|
6111
6274
|
* at 50vh and the block inside it scrolls, so a long input cannot push the options
|
|
6112
6275
|
* off the surface they are asked on.
|
|
6113
6276
|
*
|
|
6114
|
-
* `aparte-tool-decision`
|
|
6115
|
-
*
|
|
6116
|
-
*
|
|
6117
|
-
* decision are one seam too many, and the ratified elicitation rule already said so
|
|
6118
|
-
* — "a typed presenter registered per config instance, never window events". A host
|
|
6277
|
+
* There is no `aparte-tool-decision` event and no `document` listener that answers
|
|
6278
|
+
* one: a decision is answered by a typed presenter registered per config instance,
|
|
6279
|
+
* never by a window event. A host
|
|
6119
6280
|
* that wants to answer programmatically registers an `approvalResolver` (headless,
|
|
6120
6281
|
* no DOM) or its own presenter, both strictly more capable than an id on an event.
|
|
6121
6282
|
*/
|
|
@@ -6130,13 +6291,14 @@ class AparteClient {
|
|
|
6130
6291
|
*/
|
|
6131
6292
|
message: () => {
|
|
6132
6293
|
const loc = this._config.getLocale();
|
|
6133
|
-
return (loc.approvalAsk ?? "Run {tool}?").replace("{tool}", event.name);
|
|
6294
|
+
return (loc.approvalAsk ?? "Run {tool}?").replace("{tool}", () => event.name);
|
|
6134
6295
|
},
|
|
6135
|
-
// A plain string, not a function: the arguments are
|
|
6136
|
-
// the
|
|
6137
|
-
//
|
|
6138
|
-
//
|
|
6139
|
-
|
|
6296
|
+
// A plain string, not a function: the arguments are the model's — its JSON, or
|
|
6297
|
+
// the tool's own formatter's reading of it — and do not change with the
|
|
6298
|
+
// language. Same text the transcript row shows, from the same function and the
|
|
6299
|
+
// same config — the chat's, which the row renders with — so the two surfaces
|
|
6300
|
+
// cannot disagree about what is being run.
|
|
6301
|
+
details: describeToolInput(event.input, resolveConfig(target).getToolInputFormatter(event.name)),
|
|
6140
6302
|
// Functions, not strings, same as the question: these follow a language
|
|
6141
6303
|
// switch while the request is open, which a resolved string cannot.
|
|
6142
6304
|
options: [
|
|
@@ -6232,6 +6394,8 @@ class AparteClient {
|
|
|
6232
6394
|
let result = opts?.resume ? { usage: void 0, suspended: opts.resume } : await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController);
|
|
6233
6395
|
const maxResumes = this.options.maxTurns ?? 10;
|
|
6234
6396
|
let resumes = 0;
|
|
6397
|
+
let approvedAtLastPause = /* @__PURE__ */ new Set();
|
|
6398
|
+
const rowEvents = createStreamAdapter({ target: targetElement, config: this._config, messageId, ...chatId ? { targetId: chatId } : {} });
|
|
6235
6399
|
while (result.suspended) {
|
|
6236
6400
|
const stopped = this._isAborted || streamController.signal.aborted;
|
|
6237
6401
|
if (!stopped && ++resumes > maxResumes) {
|
|
@@ -6241,7 +6405,9 @@ class AparteClient {
|
|
|
6241
6405
|
);
|
|
6242
6406
|
break;
|
|
6243
6407
|
}
|
|
6244
|
-
const
|
|
6408
|
+
const approvedNow = /* @__PURE__ */ new Set();
|
|
6409
|
+
const outcomes = stopped ? void 0 : await this._answerDeferred(targetElement, result.suspended.pending, streamController.signal, rowEvents, approvedAtLastPause, approvedNow);
|
|
6410
|
+
approvedAtLastPause = approvedNow;
|
|
6245
6411
|
if (!outcomes) {
|
|
6246
6412
|
this._closeOpenCalls(targetElement, messageId);
|
|
6247
6413
|
if (this._isAborted || streamController.signal.aborted) {
|
|
@@ -6290,29 +6456,21 @@ class AparteClient {
|
|
|
6290
6456
|
/**
|
|
6291
6457
|
* The registered tools, unless the current model says it cannot call one.
|
|
6292
6458
|
*
|
|
6293
|
-
* Single source for the gate, so send / retry / edit cannot drift
|
|
6294
|
-
* is what once shipped `tools` on the initial send while retry and edit omitted
|
|
6295
|
-
* them.
|
|
6296
|
-
*
|
|
6297
|
-
* The gate used to be "the model DECLARES function_calling", defaulting to
|
|
6298
|
-
* stripping — and that made the whole tool surface unreachable on the primary
|
|
6299
|
-
* documented path. Three things had to line up for a tool to be sent, and one
|
|
6300
|
-
* never did: the app sets a model id by hand (`setModelConfig`) or a selector
|
|
6301
|
-
* sets it from a fetched list, and in both cases the model's declared
|
|
6302
|
-
* capabilities are commonly unknown. `undefined` capabilities then read as "no
|
|
6303
|
-
* function calling", so `tools: []` went on the wire while `getTools()` held
|
|
6304
|
-
* the tool the app had explicitly registered. The model answered, correctly,
|
|
6305
|
-
* that it had no such tool, and nothing anywhere said why.
|
|
6459
|
+
* Single source for the gate, so send / retry / edit cannot drift.
|
|
6306
6460
|
*
|
|
6307
|
-
*
|
|
6308
|
-
*
|
|
6461
|
+
* The question is "did this model say it cannot", not "did it say it can": the
|
|
6462
|
+
* app sets a model id by hand (`setModelConfig`) or a selector sets it from a
|
|
6463
|
+
* fetched list, and in both cases the model's declared capabilities are commonly
|
|
6464
|
+
* unknown. `undefined` capabilities read as "can", or `tools: []` would go on
|
|
6465
|
+
* the wire while `getTools()` held the tool the app had explicitly registered,
|
|
6466
|
+
* and the model would answer that it had no such tool. Registering a tool is an explicit act by the app; silently dropping it
|
|
6309
6467
|
* because a `/models` listing is terse is the library second-guessing the
|
|
6310
6468
|
* developer. A model that declares its capabilities and omits function calling
|
|
6311
6469
|
* is still honoured — that is a statement, and it is respected.
|
|
6312
6470
|
*
|
|
6313
6471
|
* Failure modes, both ways round: over-sending means a model that cannot use
|
|
6314
6472
|
* tools does not call one, or its own endpoint rejects the array with an error
|
|
6315
|
-
* the developer can read. Under-sending
|
|
6473
|
+
* the developer can read. Under-sending would be silent, total, and look like a
|
|
6316
6474
|
* lying model.
|
|
6317
6475
|
*/
|
|
6318
6476
|
_toolsForCurrentModel() {
|
|
@@ -6363,10 +6521,9 @@ class AparteClient {
|
|
|
6363
6521
|
* The system messages a turn opens with: the app's own template, then the registered
|
|
6364
6522
|
* tools' `systemPrompt`s.
|
|
6365
6523
|
*
|
|
6366
|
-
*
|
|
6367
|
-
*
|
|
6368
|
-
*
|
|
6369
|
-
* the abort-flag asymmetry `_beginUserTurn` exists to prevent.
|
|
6524
|
+
* One helper for the three turn entry points (send, retry, edit), so the tool half
|
|
6525
|
+
* cannot be added to two of them and forgotten in the third — the same shape as the
|
|
6526
|
+
* abort-flag asymmetry `_beginUserTurn` exists to prevent.
|
|
6370
6527
|
*/
|
|
6371
6528
|
_systemMessages() {
|
|
6372
6529
|
const messages = [];
|
|
@@ -6449,11 +6606,11 @@ class AparteClient {
|
|
|
6449
6606
|
* The trees a lookup must search, the one the gesture came from FIRST.
|
|
6450
6607
|
*
|
|
6451
6608
|
* `document.getElementById` and `document.querySelectorAll` cannot see into a shadow
|
|
6452
|
-
* root, so a chat mounted in one — the arrangement the theming guide endorses —
|
|
6453
|
-
* invisible to both: alone, the send
|
|
6454
|
-
* holds a chat in the light DOM, the scan
|
|
6455
|
-
* person's message AND the model's reply
|
|
6456
|
-
* into. Nearest tree first
|
|
6609
|
+
* root, so a chat mounted in one — the arrangement the theming guide endorses — is
|
|
6610
|
+
* invisible to both: alone, the send would be dropped with a warning; on a page that
|
|
6611
|
+
* also holds a chat in the light DOM, the scan would answer with the WRONG chat, and
|
|
6612
|
+
* the person's message AND the model's reply would land in a transcript they never
|
|
6613
|
+
* typed into. Nearest tree first keeps it from swapping the other way round.
|
|
6457
6614
|
*/
|
|
6458
6615
|
_rootsFor(event) {
|
|
6459
6616
|
const roots = [];
|
|
@@ -6551,8 +6708,8 @@ class AparteClient {
|
|
|
6551
6708
|
* The SLICE is each caller's own business — retry cuts before the reply it is
|
|
6552
6709
|
* regenerating, edit cuts after the message being reworded, send cuts at the last
|
|
6553
6710
|
* answered turn. What a message CONTRIBUTES to the wire is not: send, retry and
|
|
6554
|
-
* edit must not disagree about it
|
|
6555
|
-
*
|
|
6711
|
+
* edit must not disagree about it. A failed turn and a turn stopped before its
|
|
6712
|
+
* first token are filtered out rather than sent as
|
|
6556
6713
|
* `{ role: 'assistant', content: '' }` — a claim the assistant said nothing, which
|
|
6557
6714
|
* some providers reject outright and the rest read as an empty reply worth
|
|
6558
6715
|
* imitating.
|
|
@@ -6643,7 +6800,7 @@ class AparteClient {
|
|
|
6643
6800
|
if (!this._warnedNoModel) {
|
|
6644
6801
|
this._warnedNoModel = true;
|
|
6645
6802
|
console.warn(
|
|
6646
|
-
"[AparteClient] Send dropped: no model is selected. Call aparteGlobalConfig.setModelConfig({ defaultProvider, defaultModel }), mount <aparte-model-selector auto-select>, or register one provider with a single model — that one is selected on its own."
|
|
6803
|
+
"[AparteClient] Send dropped: no model is selected. Call aparteGlobalConfig.setModelConfig({ defaultProvider, defaultModel }), mount <aparte-model-selector auto-select persist>, or register one provider with a single model — that one is selected on its own."
|
|
6647
6804
|
);
|
|
6648
6805
|
}
|
|
6649
6806
|
this._handleLifecycleError(targetElement, messageId, new AparteError(
|
|
@@ -6710,12 +6867,12 @@ class AparteClient {
|
|
|
6710
6867
|
* the question the history gates can ask is "is this still in flight?", never
|
|
6711
6868
|
* "is this completed?".
|
|
6712
6869
|
*
|
|
6713
|
-
* The gates below
|
|
6870
|
+
* The gates below ask the first, because a host that SEEDS a transcript
|
|
6714
6871
|
* — restoring a saved conversation, hydrating a server-rendered one — has no
|
|
6715
|
-
* reason to invent a status for turns that are already over.
|
|
6716
|
-
*
|
|
6717
|
-
* model
|
|
6718
|
-
* because the viewport still
|
|
6872
|
+
* reason to invent a status for turns that are already over. Asking "is this
|
|
6873
|
+
* completed?" would leave the cutoff at 0 and slice away the whole transcript: the
|
|
6874
|
+
* model would get the new question with no context, and nothing in the UI would say
|
|
6875
|
+
* so, because the viewport still shows every message.
|
|
6719
6876
|
*/
|
|
6720
6877
|
static _inFlight(m) {
|
|
6721
6878
|
return m.status === "streaming" || m.status === "pending";
|
|
@@ -6746,23 +6903,9 @@ ${text}` : text;
|
|
|
6746
6903
|
* What the model is told it said last turn — reconstructed from what was
|
|
6747
6904
|
* RENDERED, not from the raw-text field.
|
|
6748
6905
|
*
|
|
6749
|
-
* The
|
|
6750
|
-
*
|
|
6751
|
-
*
|
|
6752
|
-
* correct-in-isolation decisions composed into it:
|
|
6753
|
-
*
|
|
6754
|
-
* 1. the parser withholds an ambiguous prefix (``` ``` ```, `` ` ``, `<`)
|
|
6755
|
-
* waiting for the next delta, and creates no active segment while it waits;
|
|
6756
|
-
* 2. so `_streamLoop` sees zero segments, concludes the parser produced
|
|
6757
|
-
* nothing, and appends the raw delta straight to `message.content`;
|
|
6758
|
-
* 3. and this method preferred `content`.
|
|
6759
|
-
*
|
|
6760
|
-
* The bubble hides its content element as soon as segments exist, so the UI was
|
|
6761
|
-
* perfect and only the NEXT request showed it. No unit test, no example and
|
|
6762
|
-
* no browser test could see it.
|
|
6763
|
-
*
|
|
6764
|
-
* `content` stays as the fallback, and that is not vestigial: a non-streaming
|
|
6765
|
-
* transport writes the whole reply there and creates no segments at all.
|
|
6906
|
+
* The segments are what the reader saw, so they come first. `content` is the
|
|
6907
|
+
* fallback, and that is not vestigial: a non-streaming transport writes the whole
|
|
6908
|
+
* reply there and creates no segments at all.
|
|
6766
6909
|
*/
|
|
6767
6910
|
_extractText(message) {
|
|
6768
6911
|
const rendered = this._segmentsToText(message.segments);
|
|
@@ -6780,10 +6923,9 @@ ${text}` : text;
|
|
|
6780
6923
|
* A type core does not know — a registered block grammar's (an artifact, a
|
|
6781
6924
|
* citation, a file), a consumer's custom segment — follows one rule: its
|
|
6782
6925
|
* `content` if it has one, else its `fallback`, else nothing. Dropping such a
|
|
6783
|
-
* segment
|
|
6784
|
-
* turn, so it could not be asked to change the thing it had built
|
|
6785
|
-
*
|
|
6786
|
-
* now that the artifact is a plugin's type like any other.
|
|
6926
|
+
* segment would make a document the model had just produced invisible on the very
|
|
6927
|
+
* next turn, so it could not be asked to change the thing it had built. An artifact
|
|
6928
|
+
* is a plugin's type like any other and follows this rule.
|
|
6787
6929
|
*
|
|
6788
6930
|
* `thinking` and `tool_call` stay out on purpose — the first is the model's own
|
|
6789
6931
|
* scratchpad, and most APIs neither want it back nor bill for it kindly; the
|
|
@@ -7003,10 +7145,15 @@ ${content}
|
|
|
7003
7145
|
* nobody answered — the turn was stopped, or the channel gave up — and there is no
|
|
7004
7146
|
* resume. A channel that CRASHES fails the turn, as it does for a run in the page.
|
|
7005
7147
|
*/
|
|
7006
|
-
async _answerDeferred(targetElement, pending, signal) {
|
|
7148
|
+
async _answerDeferred(targetElement, pending, signal, rowEvents, approvedBefore = /* @__PURE__ */ new Set(), approvedNow = /* @__PURE__ */ new Set()) {
|
|
7007
7149
|
const outcomes = {};
|
|
7008
7150
|
const resolver = this._approvalResolverFor(targetElement, true);
|
|
7009
|
-
|
|
7151
|
+
const refuseTheRest = (after) => {
|
|
7152
|
+
for (const rest of pending.slice(after + 1)) {
|
|
7153
|
+
outcomes[rest.toolCallId] = { kind: "decision", approved: false, reason: NOT_RUN_AFTER_A_REFUSAL };
|
|
7154
|
+
}
|
|
7155
|
+
};
|
|
7156
|
+
for (const [index, p] of pending.entries()) {
|
|
7010
7157
|
if (signal.aborted) break;
|
|
7011
7158
|
const call = { id: p.toolCallId, name: p.name, input: p.input ?? {} };
|
|
7012
7159
|
if (p.reason === "approval") {
|
|
@@ -7018,11 +7165,34 @@ ${content}
|
|
|
7018
7165
|
}
|
|
7019
7166
|
if (!decision || signal.aborted) return void 0;
|
|
7020
7167
|
outcomes[p.toolCallId] = { kind: "decision", ...decision };
|
|
7168
|
+
if (decision.approved) approvedNow.add(approvalKey(call));
|
|
7169
|
+
else {
|
|
7170
|
+
refuseTheRest(index);
|
|
7171
|
+
break;
|
|
7172
|
+
}
|
|
7021
7173
|
} else if (p.reason === "external") {
|
|
7174
|
+
let gate = null;
|
|
7175
|
+
const key = approvalKey(call);
|
|
7176
|
+
if (!approvedBefore.has(key) && !approvedNow.has(key)) {
|
|
7177
|
+
try {
|
|
7178
|
+
gate = await this._gateBrowserTool(call, signal, targetElement, rowEvents);
|
|
7179
|
+
} catch (error) {
|
|
7180
|
+
if (!signal.aborted && error?.name !== "AbortError") throw error;
|
|
7181
|
+
gate = void 0;
|
|
7182
|
+
}
|
|
7183
|
+
}
|
|
7184
|
+
if (gate === void 0 || signal.aborted) return void 0;
|
|
7185
|
+
if (gate && !gate.approved) {
|
|
7186
|
+
outcomes[p.toolCallId] = { kind: "decision", ...gate };
|
|
7187
|
+
refuseTheRest(index);
|
|
7188
|
+
break;
|
|
7189
|
+
}
|
|
7190
|
+
const payload = gate?.payload;
|
|
7191
|
+
const ran = payload && typeof payload === "object" && !Array.isArray(payload) ? { ...call, input: { ...call.input, ...payload } } : call;
|
|
7022
7192
|
const handler = this._config.getToolHandler(p.name);
|
|
7023
7193
|
try {
|
|
7024
7194
|
if (!handler) throw new Error(`No browser handler is registered for "${p.name}".`);
|
|
7025
|
-
const result = await handler(
|
|
7195
|
+
const result = await handler(ran, signal, { target: targetElement, config: this._config });
|
|
7026
7196
|
outcomes[p.toolCallId] = {
|
|
7027
7197
|
kind: "result",
|
|
7028
7198
|
content: result.content,
|
|
@@ -7036,6 +7206,21 @@ ${content}
|
|
|
7036
7206
|
}
|
|
7037
7207
|
return signal.aborted ? void 0 : outcomes;
|
|
7038
7208
|
}
|
|
7209
|
+
/**
|
|
7210
|
+
* Whether a browser tool a paused run asked for may run, decided as a run in the page
|
|
7211
|
+
* decides it. With a host `approvalResolver`, the tool's own `needsApproval` says whether
|
|
7212
|
+
* to ask and the resolver answers; without one, the approval policy and `needsApproval`
|
|
7213
|
+
* rule (`ruleOnToolCall`) and the default channel asks. `null` when nothing gates the
|
|
7214
|
+
* call, the decision when something did, `undefined` when nobody answered.
|
|
7215
|
+
*/
|
|
7216
|
+
async _gateBrowserTool(call, signal, targetElement, rowEvents) {
|
|
7217
|
+
const ruling = this.options.approvalResolver ? this._config.getTools().find((t) => t.name === call.name)?.needsApproval ? "ask" : "allow" : this._config.ruleOnToolCall(call).verdict;
|
|
7218
|
+
if (ruling === "allow") return null;
|
|
7219
|
+
if (ruling === "ask") rowEvents?.({ type: "tool-awaiting-approval", toolCallId: call.id, name: call.name, input: call.input });
|
|
7220
|
+
const decision = await this._approvalResolverFor(targetElement)(call, signal) ?? void 0;
|
|
7221
|
+
if (decision?.approved && ruling === "ask") targetElement.updateSegment?.(`tool-${call.id}`, { status: "pending" });
|
|
7222
|
+
return decision;
|
|
7223
|
+
}
|
|
7039
7224
|
/**
|
|
7040
7225
|
* The ticket is spent the moment the resume leaves: the server answers these calls
|
|
7041
7226
|
* now, so a pause saved from here on — the conversation manager saves before it cuts a
|
|
@@ -7058,8 +7243,8 @@ ${content}
|
|
|
7058
7243
|
* Close the reply's calls that can no longer finish — paused, queued, or running on a
|
|
7059
7244
|
* server the page stopped listening to: each says it stopped, and forgets the pause.
|
|
7060
7245
|
* A cut connection carries none of their last events, so nothing else would: the rows
|
|
7061
|
-
*
|
|
7062
|
-
* server that had moved on. By the reply's id, not the chat's head — a retry during
|
|
7246
|
+
* would spin, and a queued call would keep its ticket, which a reload would then
|
|
7247
|
+
* resume against a server that had moved on. By the reply's id, not the chat's head — a retry during
|
|
7063
7248
|
* the wait moves the head. A run in the page ends every call itself: nothing to do.
|
|
7064
7249
|
*/
|
|
7065
7250
|
_closeOpenCalls(targetElement, messageId) {
|
|
@@ -7304,10 +7489,9 @@ class AparteMessageRepository {
|
|
|
7304
7489
|
/**
|
|
7305
7490
|
* Every message the tree holds — every branch, not just the active path.
|
|
7306
7491
|
*
|
|
7307
|
-
* `getMessages()
|
|
7308
|
-
*
|
|
7309
|
-
*
|
|
7310
|
-
* segments into the bubble which replaced it. No order is promised beyond
|
|
7492
|
+
* Unlike `getMessages()`, which walks head → root, this also sees a turn that a
|
|
7493
|
+
* retry has taken off the active path, so it answers questions about the whole
|
|
7494
|
+
* conversation ("is anything still streaming?"). No order is promised beyond
|
|
7311
7495
|
* insertion: a caller that needs the topology reads `export()`.
|
|
7312
7496
|
*/
|
|
7313
7497
|
getAllMessages() {
|
|
@@ -7376,9 +7560,7 @@ class AparteMessageRepository {
|
|
|
7376
7560
|
* Remove all descendants of `messageId` (inclusive) and, when the head was among
|
|
7377
7561
|
* them, set `head` to the parent of `messageId`. Used by edit/truncate flows.
|
|
7378
7562
|
*
|
|
7379
|
-
* A head in a branch this call did not touch stays where it is
|
|
7380
|
-
* relocated regardless, which took the active path out of the version the reader was
|
|
7381
|
-
* on for a truncate somewhere else entirely.
|
|
7563
|
+
* A head in a branch this call did not touch stays where it is.
|
|
7382
7564
|
*
|
|
7383
7565
|
* @param messageId The first message to remove
|
|
7384
7566
|
*/
|
|
@@ -7432,9 +7614,8 @@ class AparteMessageRepository {
|
|
|
7432
7614
|
/**
|
|
7433
7615
|
* Import a previously-exported snapshot, rebuilding the full tree and
|
|
7434
7616
|
* restoring the active head. Replaces any existing content (calls `clear()`
|
|
7435
|
-
* first).
|
|
7436
|
-
*
|
|
7437
|
-
* Map and parents are always inserted before their children.
|
|
7617
|
+
* first). `export()` writes messages in topological order (parent before
|
|
7618
|
+
* child), which is the order this expects.
|
|
7438
7619
|
*/
|
|
7439
7620
|
import(exported) {
|
|
7440
7621
|
this.clear();
|
|
@@ -7462,11 +7643,8 @@ class AparteMessageRepository {
|
|
|
7462
7643
|
* Compatible with the assistant-ui ExportedMessageRepository format.
|
|
7463
7644
|
*
|
|
7464
7645
|
* Walked from the root through each node's `children`, so a parent is always
|
|
7465
|
-
* written before its children
|
|
7466
|
-
*
|
|
7467
|
-
* under one inserted after it (`addOrUpdateMessage` with a known id and a newer
|
|
7468
|
-
* parent). The snapshot then listed the child first, and `import()` threw "parent
|
|
7469
|
-
* not found" on the very tree this method had just produced.
|
|
7646
|
+
* written before its children, even after `addOrUpdateMessage` re-parented an
|
|
7647
|
+
* existing node under a newer one; `import()` relies on that order.
|
|
7470
7648
|
*/
|
|
7471
7649
|
export() {
|
|
7472
7650
|
const messages = [];
|
|
@@ -7690,18 +7868,20 @@ export {
|
|
|
7690
7868
|
uuid as a8,
|
|
7691
7869
|
eventOrigin as a9,
|
|
7692
7870
|
activeElementIn as aa,
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7871
|
+
gateGlide as ab,
|
|
7872
|
+
renderingVersionOf as ac,
|
|
7873
|
+
mergeSegmentUpdate as ad,
|
|
7874
|
+
writeStreamedMarkdown as ae,
|
|
7875
|
+
stampSegmentActivity as af,
|
|
7876
|
+
stampSegmentOnInsert as ag,
|
|
7877
|
+
stampSegmentOnUpdate as ah,
|
|
7878
|
+
renumberSegments as ai,
|
|
7879
|
+
openSegmentIds as aj,
|
|
7880
|
+
isTerminalStatus as ak,
|
|
7881
|
+
adoptMessageSegments as al,
|
|
7882
|
+
APARTE_DERIVED_SEGMENTS as am,
|
|
7883
|
+
chatBoundaryOf as an,
|
|
7884
|
+
rootOf as ao,
|
|
7705
7885
|
APARTE_DEFAULT_BUBBLE_ACTIONS as b,
|
|
7706
7886
|
APARTE_DEFAULT_HOST_HANDLERS as c,
|
|
7707
7887
|
APARTE_DEFAULT_ICON_FALLBACKS as d,
|
|
@@ -7728,4 +7908,4 @@ export {
|
|
|
7728
7908
|
collectRendererStyles as y,
|
|
7729
7909
|
contentToText as z
|
|
7730
7910
|
};
|
|
7731
|
-
//# sourceMappingURL=dialog-triggers-
|
|
7911
|
+
//# sourceMappingURL=dialog-triggers-3Rk7PuZa.js.map
|