@aparte/core 0.19.3 → 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/dist/client/aparte-client.d.ts +66 -95
- 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 +88 -57
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +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 +2 -2
- package/dist/components/composer/aparte-composer-add-attachment.d.ts +1 -1
- package/dist/components/composer/aparte-composer-input.d.ts +1 -5
- package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-send.d.ts +8 -10
- package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer.d.ts +48 -80
- package/dist/components/composer/aparte-composer.d.ts.map +1 -1
- package/dist/components/context/aparte-context.d.ts +9 -4
- package/dist/components/context/aparte-context.d.ts.map +1 -1
- package/dist/components/conversation-list/aparte-conversation-list.d.ts +23 -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.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 +70 -70
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +87 -66
- 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 +57 -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 +3 -3
- package/dist/conversations/conversation-manager.d.ts.map +1 -1
- package/dist/custom-elements.json +13054 -12334
- package/dist/{dialog-triggers-ChnPFG9M.js → dialog-triggers-3Rk7PuZa.js} +444 -287
- 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 +11 -5
- 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 +12 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +682 -327
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +6 -3
- 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/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/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/backend-handler.d.ts +4 -6
- package/dist/transport/backend-handler.d.ts.map +1 -1
- package/dist/transport/backend-transport.d.ts +14 -0
- 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/index.d.ts +2 -2
- 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 +21 -13
- package/dist/types/segments.d.ts.map +1 -1
- package/dist/types/tools.d.ts +20 -22
- 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 +29 -3
- package/dist/dialog-triggers-ChnPFG9M.js.map +0 -1
|
@@ -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.",
|
|
@@ -148,6 +153,7 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
148
153
|
messageInfo: "Details",
|
|
149
154
|
newChat: "New Chat",
|
|
150
155
|
loadingConversations: "Loading conversations",
|
|
156
|
+
conversationSearchEmpty: "No conversation matches",
|
|
151
157
|
conversationList: "Conversations",
|
|
152
158
|
loadingConversation: "Loading the conversation",
|
|
153
159
|
deleteConversation: "Delete",
|
|
@@ -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,8 +1107,7 @@ function createStreamAdapter(opts) {
|
|
|
1061
1107
|
const seg = {
|
|
1062
1108
|
id: `think-${uuid()}`,
|
|
1063
1109
|
type: "thinking",
|
|
1064
|
-
content: thinkingContent
|
|
1065
|
-
collapsed: true
|
|
1110
|
+
content: thinkingContent
|
|
1066
1111
|
};
|
|
1067
1112
|
thinkingId = seg.id;
|
|
1068
1113
|
streaming.add(seg.id);
|
|
@@ -1074,7 +1119,9 @@ function createStreamAdapter(opts) {
|
|
|
1074
1119
|
}
|
|
1075
1120
|
case "text-delta": {
|
|
1076
1121
|
if (thinkingId && !thinkingCollapsed) {
|
|
1077
|
-
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 });
|
|
1078
1125
|
thinkingCollapsed = true;
|
|
1079
1126
|
}
|
|
1080
1127
|
const result = parser.parse(e.delta);
|
|
@@ -1813,6 +1860,26 @@ const APARTE_DEFAULT_HOST_HANDLERS = {
|
|
|
1813
1860
|
};
|
|
1814
1861
|
const NOOP = () => {
|
|
1815
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
|
+
}
|
|
1816
1883
|
class AparteConfig {
|
|
1817
1884
|
_markdownProvider;
|
|
1818
1885
|
_streamingMarkdownProvider;
|
|
@@ -1844,15 +1911,13 @@ class AparteConfig {
|
|
|
1844
1911
|
/**
|
|
1845
1912
|
* What `refreshProviderModels()` last brought back, per provider.
|
|
1846
1913
|
*
|
|
1847
|
-
*
|
|
1848
|
-
*
|
|
1849
|
-
*
|
|
1850
|
-
*
|
|
1851
|
-
*
|
|
1852
|
-
* it
|
|
1853
|
-
*
|
|
1854
|
-
* the model. A registered tool, an approval gate, `<aparte-elicitation>` — all
|
|
1855
|
-
* 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.
|
|
1856
1921
|
*/
|
|
1857
1922
|
_fetchedModels = /* @__PURE__ */ new Map();
|
|
1858
1923
|
_modelConfig = {};
|
|
@@ -1887,9 +1952,8 @@ class AparteConfig {
|
|
|
1887
1952
|
* silently, because the warning fires at most once per config.
|
|
1888
1953
|
*
|
|
1889
1954
|
* Each entry carries the element that registered it, so a request naming a `target`
|
|
1890
|
-
* is routed to the presenter in the SAME chat
|
|
1891
|
-
*
|
|
1892
|
-
* 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").
|
|
1893
1957
|
*/
|
|
1894
1958
|
_elicitationPresenters = [];
|
|
1895
1959
|
/** Warn once per config, not once per request — a tool loop can ask repeatedly. */
|
|
@@ -1899,22 +1963,21 @@ class AparteConfig {
|
|
|
1899
1963
|
*
|
|
1900
1964
|
* One request reaches the presenter at a time — not because a presenter could not
|
|
1901
1965
|
* cope, but because the COMPOSER cannot: it has one panel slot, and a second
|
|
1902
|
-
* request
|
|
1903
|
-
*
|
|
1904
|
-
*
|
|
1905
|
-
* lie as telling the model a stopped turn was refused. WAITING is the honest
|
|
1906
|
-
* 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.
|
|
1907
1969
|
*
|
|
1908
1970
|
* Here rather than in the presenter because this is the only place that sees
|
|
1909
|
-
* EVERY request whatever raised it — a tool handler, an app's own button,
|
|
1910
|
-
*
|
|
1911
|
-
*
|
|
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.
|
|
1912
1974
|
*/
|
|
1913
1975
|
_elicitationQueue = /* @__PURE__ */ new Map();
|
|
1914
1976
|
// Tool Registry
|
|
1915
1977
|
_tools = /* @__PURE__ */ new Map();
|
|
1916
1978
|
_toolRenderers = /* @__PURE__ */ new Map();
|
|
1917
1979
|
_toolSummaries = /* @__PURE__ */ new Map();
|
|
1980
|
+
_toolInputFormatters = /* @__PURE__ */ new Map();
|
|
1918
1981
|
/** Tagged blocks the stream parser recognises in the prose — see {@link registerStreamBlock}. */
|
|
1919
1982
|
_streamBlocks = /* @__PURE__ */ new Map();
|
|
1920
1983
|
_segmentDefaults = /* @__PURE__ */ new Map();
|
|
@@ -2007,6 +2070,8 @@ class AparteConfig {
|
|
|
2007
2070
|
*/
|
|
2008
2071
|
setMarkdownProvider(fn) {
|
|
2009
2072
|
this._markdownProvider = fn;
|
|
2073
|
+
bumpRenderingVersion(this);
|
|
2074
|
+
this._notify();
|
|
2010
2075
|
}
|
|
2011
2076
|
/**
|
|
2012
2077
|
* Set an incremental (streaming) Markdown renderer provider. Optional —
|
|
@@ -2021,6 +2086,8 @@ class AparteConfig {
|
|
|
2021
2086
|
*/
|
|
2022
2087
|
setStreamingMarkdownProvider(fn) {
|
|
2023
2088
|
this._streamingMarkdownProvider = fn;
|
|
2089
|
+
bumpRenderingVersion(this);
|
|
2090
|
+
this._notify();
|
|
2024
2091
|
}
|
|
2025
2092
|
/**
|
|
2026
2093
|
* Create an incremental Markdown renderer bound to `target`, or `null` when
|
|
@@ -2036,6 +2103,8 @@ class AparteConfig {
|
|
|
2036
2103
|
*/
|
|
2037
2104
|
setHighlightProvider(fn) {
|
|
2038
2105
|
this._highlightProvider = fn;
|
|
2106
|
+
bumpRenderingVersion(this);
|
|
2107
|
+
this._notify();
|
|
2039
2108
|
}
|
|
2040
2109
|
/**
|
|
2041
2110
|
* Whether a syntax-highlight provider is registered. Lets consumers (e.g.
|
|
@@ -2103,30 +2172,54 @@ class AparteConfig {
|
|
|
2103
2172
|
return resolved.trim() || null;
|
|
2104
2173
|
}
|
|
2105
2174
|
/**
|
|
2106
|
-
* 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.
|
|
2107
2178
|
* @param provider Object implementing AparteIconProvider interface
|
|
2108
2179
|
*/
|
|
2109
2180
|
setIconProvider(provider) {
|
|
2110
2181
|
this._iconProvider = provider;
|
|
2111
2182
|
this._notify();
|
|
2112
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
|
+
}
|
|
2113
2205
|
/**
|
|
2114
2206
|
* The icon set as a **complete** provider: every name resolves, falling back
|
|
2115
2207
|
* to `APARTE_DEFAULT_ICON_FALLBACKS` for anything the registered provider doesn't
|
|
2116
2208
|
* implement. Callers (bubble action bar, composer controls) can therefore
|
|
2117
2209
|
* invoke `icons.copy()` unconditionally.
|
|
2118
2210
|
*
|
|
2119
|
-
*
|
|
2120
|
-
*
|
|
2121
|
-
* the type now states — crashed every other icon with
|
|
2122
|
-
* "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".
|
|
2123
2213
|
*/
|
|
2124
2214
|
getIconProvider() {
|
|
2125
2215
|
const registered = this._iconProvider;
|
|
2126
2216
|
const complete = {};
|
|
2127
2217
|
for (const name of Object.keys(APARTE_DEFAULT_ICON_FALLBACKS)) {
|
|
2128
|
-
|
|
2129
|
-
|
|
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;
|
|
2130
2223
|
}
|
|
2131
2224
|
return complete;
|
|
2132
2225
|
}
|
|
@@ -2258,14 +2351,10 @@ class AparteConfig {
|
|
|
2258
2351
|
this._locale = { ...this._locale, ...translations };
|
|
2259
2352
|
this._notify();
|
|
2260
2353
|
}
|
|
2261
|
-
/**
|
|
2262
|
-
* Get icon HTML string by name
|
|
2263
|
-
* Falls back to textual representation if no provider is set
|
|
2264
|
-
*/
|
|
2265
2354
|
getIcon(name) {
|
|
2266
|
-
const icon = this._iconProvider
|
|
2355
|
+
const icon = drawingOf(this._iconProvider, name);
|
|
2267
2356
|
if (icon) return icon();
|
|
2268
|
-
return APARTE_DEFAULT_ICON_FALLBACKS[name];
|
|
2357
|
+
return Object.prototype.hasOwnProperty.call(APARTE_DEFAULT_ICON_FALLBACKS, name) ? APARTE_DEFAULT_ICON_FALLBACKS[name] : void 0;
|
|
2269
2358
|
}
|
|
2270
2359
|
/**
|
|
2271
2360
|
* Set a custom Key provider (e.g., AparteVault override)
|
|
@@ -2412,11 +2501,14 @@ class AparteConfig {
|
|
|
2412
2501
|
/**
|
|
2413
2502
|
* Register a model preference provider for agnostic persistence.
|
|
2414
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.
|
|
2415
2506
|
* @example
|
|
2416
2507
|
* aparteGlobalConfig.setModelPreferenceProvider({
|
|
2417
|
-
* save: (
|
|
2418
|
-
* 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'),
|
|
2419
2510
|
* });
|
|
2511
|
+
* aparteGlobalConfig.restoreModelPreference();
|
|
2420
2512
|
*/
|
|
2421
2513
|
setModelPreferenceProvider(provider) {
|
|
2422
2514
|
this._modelPreferenceProvider = provider;
|
|
@@ -2463,7 +2555,7 @@ class AparteConfig {
|
|
|
2463
2555
|
return { verdict: tool?.needsApproval ? "ask" : "allow" };
|
|
2464
2556
|
}
|
|
2465
2557
|
/**
|
|
2466
|
-
* Restore
|
|
2558
|
+
* Restore the saved model preference via the registered provider.
|
|
2467
2559
|
* Should be called once at app startup, before any component mounts.
|
|
2468
2560
|
* No-op if no provider is registered or nothing was saved. The saved selection is
|
|
2469
2561
|
* applied without being saved again.
|
|
@@ -2527,25 +2619,17 @@ class AparteConfig {
|
|
|
2527
2619
|
/**
|
|
2528
2620
|
* Elicitation policy: whether a choice offers a free-text "Other…" escape.
|
|
2529
2621
|
*
|
|
2530
|
-
* This is the HOST's decision, not the model's
|
|
2531
|
-
* `allow_other` field
|
|
2532
|
-
*
|
|
2533
|
-
*
|
|
2534
|
-
*
|
|
2535
|
-
*
|
|
2536
|
-
* Every serious implementation of this pattern makes the escape hatch a property
|
|
2537
|
-
* of the surface rather than of the request. Default `true`, which is what the
|
|
2538
|
-
* panel did before, so nothing a user sees changes — only who gets to say so.
|
|
2539
|
-
* A direct `requestUserInput` caller can still set `allowOther` per field; that
|
|
2540
|
-
* 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.
|
|
2541
2627
|
*
|
|
2542
2628
|
* `layout` decides how a form of SEVERAL questions is presented.
|
|
2543
2629
|
* `'stepped'` (default) asks them one at a time with a chip per question;
|
|
2544
|
-
* `'stacked'` puts them all in the panel at once
|
|
2545
|
-
*
|
|
2546
|
-
*
|
|
2547
|
-
* No product asks a person two questions by stacking them in one box; it is kept
|
|
2548
|
-
* 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.
|
|
2549
2633
|
*
|
|
2550
2634
|
* `answerOnClick` decides how a question asked ON ITS OWN with a single choice — an
|
|
2551
2635
|
* `enum` without `multiple` or a `default`, a `boolean` without a `default` — is
|
|
@@ -2745,8 +2829,8 @@ class AparteConfig {
|
|
|
2745
2829
|
* is a string, so a consumer's own segment type is covered by the same call.
|
|
2746
2830
|
*
|
|
2747
2831
|
* The problem it solves: a consumer streaming a reply does not CONSTRUCT its
|
|
2748
|
-
* segments — the parser does — so
|
|
2749
|
-
* 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.
|
|
2750
2834
|
*
|
|
2751
2835
|
* Applied where identity is stamped (`utils/segments.ts`), so it covers every
|
|
2752
2836
|
* path a segment can arrive by — the parser, `addSegment`, and the segments
|
|
@@ -2787,8 +2871,8 @@ class AparteConfig {
|
|
|
2787
2871
|
* Say what a call targets, in the default row, after the tool's name.
|
|
2788
2872
|
*
|
|
2789
2873
|
* The default row names the tool and keeps its arguments behind a disclosure, so a
|
|
2790
|
-
* reader
|
|
2791
|
-
* `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,
|
|
2792
2876
|
* the spinner, the disclosure and the relabel contract. This keeps all of them and
|
|
2793
2877
|
* adds the line. A tool with its own renderer draws its own row, so its summary is
|
|
2794
2878
|
* never read.
|
|
@@ -2814,6 +2898,27 @@ class AparteConfig {
|
|
|
2814
2898
|
getToolSummary(toolName) {
|
|
2815
2899
|
return this._toolSummaries.get(toolName);
|
|
2816
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
|
+
}
|
|
2817
2922
|
/**
|
|
2818
2923
|
* Teach the stream parser a tagged block: `<tag attr="…">…</tag>` in the model's
|
|
2819
2924
|
* prose becomes the segment `toSegment` builds, streamed delta by delta.
|
|
@@ -2908,7 +3013,7 @@ class AparteConfig {
|
|
|
2908
3013
|
* the same chat is the right one — found by comparing chat boundaries rather than by
|
|
2909
3014
|
* `contains`, because the presenter is a sibling of the transcript, not an ancestor
|
|
2910
3015
|
* of it. With no target, no match, or no boundary to compare, the top of the stack
|
|
2911
|
-
* is the answer
|
|
3016
|
+
* is the answer.
|
|
2912
3017
|
*/
|
|
2913
3018
|
_presenterFor(target) {
|
|
2914
3019
|
const stack = this._elicitationPresenters;
|
|
@@ -2959,11 +3064,10 @@ class AparteConfig {
|
|
|
2959
3064
|
*
|
|
2960
3065
|
* The wait exists so a second question cannot take the panel away from the one on
|
|
2961
3066
|
* screen — a property of ONE panel, and `_present` routes by target, so a chat is
|
|
2962
|
-
* the right unit.
|
|
2963
|
-
*
|
|
2964
|
-
*
|
|
2965
|
-
*
|
|
2966
|
-
* 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.
|
|
2967
3071
|
*/
|
|
2968
3072
|
_elicitationQueueKey(request) {
|
|
2969
3073
|
return chatBoundaryOf(request.target);
|
|
@@ -2981,7 +3085,14 @@ class AparteConfig {
|
|
|
2981
3085
|
return Promise.reject(new AparteElicitationAbortError("no-presenter"));
|
|
2982
3086
|
}
|
|
2983
3087
|
if (request.signal?.aborted) return Promise.reject(new AparteElicitationAbortError("aborted"));
|
|
2984
|
-
|
|
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
|
+
});
|
|
2985
3096
|
}
|
|
2986
3097
|
/**
|
|
2987
3098
|
* Put the configuration back to its defaults: the providers and the selected model,
|
|
@@ -3018,6 +3129,7 @@ class AparteConfig {
|
|
|
3018
3129
|
this._tools.clear();
|
|
3019
3130
|
this._toolRenderers.clear();
|
|
3020
3131
|
this._toolSummaries.clear();
|
|
3132
|
+
this._toolInputFormatters.clear();
|
|
3021
3133
|
this._streamBlocks.clear();
|
|
3022
3134
|
this._modelConfig = {};
|
|
3023
3135
|
this._requireModelSelection = false;
|
|
@@ -3053,46 +3165,6 @@ function getGlobalConfig() {
|
|
|
3053
3165
|
return new AparteConfig();
|
|
3054
3166
|
}
|
|
3055
3167
|
const aparteGlobalConfig = getGlobalConfig();
|
|
3056
|
-
const APARTE_HOST_ATTR = "data-aparte-host";
|
|
3057
|
-
const CONFIG_SLOT = Symbol.for("aparte.instanceConfig");
|
|
3058
|
-
function notifyConfigChanged(root, next, previous) {
|
|
3059
|
-
if (next === previous) return;
|
|
3060
|
-
const call = (el2) => {
|
|
3061
|
-
const hook = el2.aparteConfigChanged;
|
|
3062
|
-
if (typeof hook === "function") hook.call(el2, next, previous);
|
|
3063
|
-
};
|
|
3064
|
-
call(root);
|
|
3065
|
-
for (const el2 of root.querySelectorAll("*")) call(el2);
|
|
3066
|
-
}
|
|
3067
|
-
function attachConfig(el2, config) {
|
|
3068
|
-
const previous = el2[CONFIG_SLOT] ?? resolveConfig(el2.parentElement);
|
|
3069
|
-
el2[CONFIG_SLOT] = config;
|
|
3070
|
-
el2.setAttribute(APARTE_HOST_ATTR, "");
|
|
3071
|
-
notifyConfigChanged(el2, config, previous);
|
|
3072
|
-
}
|
|
3073
|
-
function detachConfig(el2) {
|
|
3074
|
-
const previous = el2[CONFIG_SLOT];
|
|
3075
|
-
delete el2[CONFIG_SLOT];
|
|
3076
|
-
el2.removeAttribute(APARTE_HOST_ATTR);
|
|
3077
|
-
if (previous) notifyConfigChanged(el2, resolveConfig(el2.parentElement), previous);
|
|
3078
|
-
}
|
|
3079
|
-
function resolveConfig(el2) {
|
|
3080
|
-
const host = el2?.closest?.(`[${APARTE_HOST_ATTR}]`);
|
|
3081
|
-
return host?.[CONFIG_SLOT] ?? aparteGlobalConfig;
|
|
3082
|
-
}
|
|
3083
|
-
let _renderConfig = null;
|
|
3084
|
-
function runWithConfig(config, fn) {
|
|
3085
|
-
const prev = _renderConfig;
|
|
3086
|
-
_renderConfig = config;
|
|
3087
|
-
try {
|
|
3088
|
-
return fn();
|
|
3089
|
-
} finally {
|
|
3090
|
-
_renderConfig = prev;
|
|
3091
|
-
}
|
|
3092
|
-
}
|
|
3093
|
-
function contextConfig(el2) {
|
|
3094
|
-
return _renderConfig ?? resolveConfig(el2);
|
|
3095
|
-
}
|
|
3096
3168
|
const APARTE_CONFIG_CHANGE = "aparte-config-change";
|
|
3097
3169
|
function subscribeConfigChange(el2, handler) {
|
|
3098
3170
|
const listener = (e) => {
|
|
@@ -3150,6 +3222,7 @@ function writeStreamedMarkdown(host, contentEl, content, streaming) {
|
|
|
3150
3222
|
contentEl.innerHTML = contextConfig().renderMarkdown(content);
|
|
3151
3223
|
}
|
|
3152
3224
|
function highlightMarkdownFences(contentEl) {
|
|
3225
|
+
if (!contextConfig().hasHighlightProvider()) return;
|
|
3153
3226
|
const blocks = contentEl.querySelectorAll("pre > code:not([data-aparte-highlighted])");
|
|
3154
3227
|
for (const block of blocks) {
|
|
3155
3228
|
const pre = block.parentElement;
|
|
@@ -3166,6 +3239,16 @@ function highlightMarkdownFences(contentEl) {
|
|
|
3166
3239
|
const textRenderer = {
|
|
3167
3240
|
type: "text",
|
|
3168
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
|
+
},
|
|
3169
3252
|
update: (el2, segment) => {
|
|
3170
3253
|
const contentEl = el2.querySelector(".aparte-segment-content");
|
|
3171
3254
|
if (!contentEl) return;
|
|
@@ -3175,6 +3258,7 @@ const textRenderer = {
|
|
|
3175
3258
|
segment.content,
|
|
3176
3259
|
segment.isStreaming !== false
|
|
3177
3260
|
);
|
|
3261
|
+
if (segment.isStreaming === false) highlightMarkdownFences(contentEl);
|
|
3178
3262
|
},
|
|
3179
3263
|
getStyles: () => ``
|
|
3180
3264
|
};
|
|
@@ -3406,10 +3490,7 @@ const errorRenderer = {
|
|
|
3406
3490
|
`;
|
|
3407
3491
|
},
|
|
3408
3492
|
/**
|
|
3409
|
-
* The icon and the heading
|
|
3410
|
-
* while `locale.error` — a REQUIRED key, documented, and already translated to
|
|
3411
|
-
* "Erreur" — was read by nothing at all. A translated string with no consumer and
|
|
3412
|
-
* a literal with no translation, in the same card.
|
|
3493
|
+
* The icon and the heading (`locale.error`).
|
|
3413
3494
|
*
|
|
3414
3495
|
* Not the message: that is the model's or the transport's text, in whatever
|
|
3415
3496
|
* language it arrived in. Relabelling it would be inventing content.
|
|
@@ -3422,8 +3503,15 @@ const errorRenderer = {
|
|
|
3422
3503
|
},
|
|
3423
3504
|
getStyles: () => ``
|
|
3424
3505
|
};
|
|
3425
|
-
function describeToolInput(input) {
|
|
3506
|
+
function describeToolInput(input, format) {
|
|
3426
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
|
+
}
|
|
3427
3515
|
try {
|
|
3428
3516
|
return JSON.stringify(input, null, 2);
|
|
3429
3517
|
} catch {
|
|
@@ -3431,7 +3519,7 @@ function describeToolInput(input) {
|
|
|
3431
3519
|
}
|
|
3432
3520
|
}
|
|
3433
3521
|
function describeInput(segment) {
|
|
3434
|
-
return describeToolInput(segment.toolCall?.input);
|
|
3522
|
+
return describeToolInput(segment.toolCall?.input, contextConfig().getToolInputFormatter(segment.toolCall?.name ?? ""));
|
|
3435
3523
|
}
|
|
3436
3524
|
function stateBadge(segment) {
|
|
3437
3525
|
const cfg = contextConfig();
|
|
@@ -3795,6 +3883,19 @@ function withoutDerivedTree(tree) {
|
|
|
3795
3883
|
messages: tree.messages.map((entry) => ({ ...entry, message: withoutDerived(entry.message) }))
|
|
3796
3884
|
};
|
|
3797
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
|
+
}
|
|
3798
3899
|
class AparteConversationManager {
|
|
3799
3900
|
_adapter;
|
|
3800
3901
|
_conversations = [];
|
|
@@ -3985,9 +4086,9 @@ class AparteConversationManager {
|
|
|
3985
4086
|
*
|
|
3986
4087
|
* One append at a time per conversation: two sends that overlap (a suggestion and a
|
|
3987
4088
|
* quick type, coalesced into one new conversation) both suspend before reading the
|
|
3988
|
-
* record — for the messages, then for the title provider — so
|
|
3989
|
-
* conversation with no user message in it yet
|
|
3990
|
-
* 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.
|
|
3991
4092
|
*/
|
|
3992
4093
|
async addMessage(convId, msg) {
|
|
3993
4094
|
const previous = this._appending.get(convId);
|
|
@@ -4040,8 +4141,14 @@ class AparteConversationManager {
|
|
|
4040
4141
|
let contentChanged = true;
|
|
4041
4142
|
let treeChanged = tree !== void 0;
|
|
4042
4143
|
try {
|
|
4043
|
-
|
|
4044
|
-
|
|
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
|
+
}
|
|
4045
4152
|
} catch {
|
|
4046
4153
|
}
|
|
4047
4154
|
let title;
|
|
@@ -4202,6 +4309,7 @@ function revokeAttachmentUrls(attachments) {
|
|
|
4202
4309
|
if (attachment.url?.startsWith("blob:")) URL.revokeObjectURL(attachment.url);
|
|
4203
4310
|
}
|
|
4204
4311
|
}
|
|
4312
|
+
const MAX_COMPACTION_SAVE_WAITS = 6;
|
|
4205
4313
|
class AparteConversationController {
|
|
4206
4314
|
_binding;
|
|
4207
4315
|
_options;
|
|
@@ -4217,6 +4325,7 @@ class AparteConversationController {
|
|
|
4217
4325
|
_onMessageAborted = null;
|
|
4218
4326
|
_onMessagePaused = null;
|
|
4219
4327
|
_onSelectConversation = null;
|
|
4328
|
+
_onCompactDone = null;
|
|
4220
4329
|
_unsubscribeManager = null;
|
|
4221
4330
|
/** The manager the subscription above is on, so a replaced manager is re-subscribed. */
|
|
4222
4331
|
_subscribedTo = null;
|
|
@@ -4279,9 +4388,9 @@ class AparteConversationController {
|
|
|
4279
4388
|
/**
|
|
4280
4389
|
* Degraded mode is legitimate — a host that never opted into the conversation
|
|
4281
4390
|
* lifecycle has no manager and should not be nagged. Registering one on the
|
|
4282
|
-
* WRONG config is not:
|
|
4283
|
-
*
|
|
4284
|
-
*
|
|
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.
|
|
4285
4394
|
*
|
|
4286
4395
|
* Warned once per controller, and only in the case that is unambiguously a
|
|
4287
4396
|
* mistake: no manager here, but one on the global.
|
|
@@ -4354,8 +4463,14 @@ class AparteConversationController {
|
|
|
4354
4463
|
if (targetId && targetId !== this._binding.hostId) return;
|
|
4355
4464
|
void this.setConversationId(evt.detail?.id ?? null);
|
|
4356
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
|
+
};
|
|
4357
4471
|
if (typeof window !== "undefined") {
|
|
4358
4472
|
window.addEventListener("aparte-conversation-select", this._onSelectConversation);
|
|
4473
|
+
window.addEventListener("aparte-compact-done", this._onCompactDone);
|
|
4359
4474
|
}
|
|
4360
4475
|
this._bound = true;
|
|
4361
4476
|
this._manager();
|
|
@@ -4399,6 +4514,10 @@ class AparteConversationController {
|
|
|
4399
4514
|
host.removeEventListener("aparte-message-paused", this._onMessagePaused);
|
|
4400
4515
|
this._onMessagePaused = null;
|
|
4401
4516
|
}
|
|
4517
|
+
if (this._onCompactDone && typeof window !== "undefined") {
|
|
4518
|
+
window.removeEventListener("aparte-compact-done", this._onCompactDone);
|
|
4519
|
+
this._onCompactDone = null;
|
|
4520
|
+
}
|
|
4402
4521
|
if (this._onSelectConversation && typeof window !== "undefined") {
|
|
4403
4522
|
window.removeEventListener("aparte-conversation-select", this._onSelectConversation);
|
|
4404
4523
|
this._onSelectConversation = null;
|
|
@@ -4542,6 +4661,22 @@ class AparteConversationController {
|
|
|
4542
4661
|
* re-enter streaming UI (caret, spinner, no action buttons) for content
|
|
4543
4662
|
* that was partially written before a switch or component destruction.
|
|
4544
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
|
+
}
|
|
4545
4680
|
_persistActive() {
|
|
4546
4681
|
if (!this._activeId) return;
|
|
4547
4682
|
const manager = this._manager();
|
|
@@ -4599,7 +4734,27 @@ class AparteConversationController {
|
|
|
4599
4734
|
function cssEscape(value) {
|
|
4600
4735
|
return value.replace(/["\\]/g, "\\$&").replace(/[\n\r\f]/g, (c) => `\\${c.charCodeAt(0).toString(16)} `);
|
|
4601
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
|
+
}
|
|
4602
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"]);
|
|
4603
4758
|
class AparteChatHost {
|
|
4604
4759
|
binding;
|
|
4605
4760
|
options;
|
|
@@ -4658,6 +4813,7 @@ class AparteChatHost {
|
|
|
4658
4813
|
}
|
|
4659
4814
|
this._installHostMethods(host);
|
|
4660
4815
|
this._installLifecycleListeners(host);
|
|
4816
|
+
this._teardown.push(gateGlide(host));
|
|
4661
4817
|
this._installViewportListeners();
|
|
4662
4818
|
this._createController(host);
|
|
4663
4819
|
return () => this.unbind();
|
|
@@ -4737,6 +4893,9 @@ class AparteChatHost {
|
|
|
4737
4893
|
importTree: (tree) => {
|
|
4738
4894
|
this._vp()?.importTree?.(tree);
|
|
4739
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),
|
|
4740
4899
|
// One writer for the wait. A wrapper that hears `onLoadingChange` draws the
|
|
4741
4900
|
// wait itself and writes the viewport's attribute from its own
|
|
4742
4901
|
// `loading || hostLoading` — so the host writing it too meant that, with the
|
|
@@ -4899,11 +5058,8 @@ class AparteChatHost {
|
|
|
4899
5058
|
* the same direct path `appendToken` uses;
|
|
4900
5059
|
* 2. into the framework's message list, **coalesced to once per frame**.
|
|
4901
5060
|
*
|
|
4902
|
-
*
|
|
4903
|
-
* `
|
|
4904
|
-
* framework render per token. At 60 tok/s from a local model that is unusable,
|
|
4905
|
-
* and nothing in the imperative API hinted that `appendToSegment` cost so much
|
|
4906
|
-
* 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.
|
|
4907
5063
|
*
|
|
4908
5064
|
* Any structural change flushes the buffer first (see
|
|
4909
5065
|
* {@link _flushStreamState}), so ordering is never observable.
|
|
@@ -4989,9 +5145,7 @@ class AparteChatHost {
|
|
|
4989
5145
|
/**
|
|
4990
5146
|
* Clear all messages + reset viewport state. The option travels to the viewport
|
|
4991
5147
|
* untouched: a compaction passes `{ revokeAttachments: false }` because it re-appends
|
|
4992
|
-
* the turns it keeps and revokes the dropped ones' object URLs itself
|
|
4993
|
-
* framework wrapper this bridge used to drop the argument, and every surviving
|
|
4994
|
-
* attachment came back broken.
|
|
5148
|
+
* the turns it keeps and revokes the dropped ones' object URLs itself.
|
|
4995
5149
|
*/
|
|
4996
5150
|
clearMessages(options) {
|
|
4997
5151
|
this._beginConversationSwap();
|
|
@@ -5035,10 +5189,9 @@ class AparteChatHost {
|
|
|
5035
5189
|
*
|
|
5036
5190
|
* Each token reaches the bubble immediately and the framework's message list
|
|
5037
5191
|
* is synced **once per frame** (same discipline as {@link appendToSegment}),
|
|
5038
|
-
* with a guaranteed flush before completion and on abort
|
|
5039
|
-
*
|
|
5040
|
-
*
|
|
5041
|
-
* 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.
|
|
5042
5195
|
*/
|
|
5043
5196
|
async streamTokens(messageId, tokens) {
|
|
5044
5197
|
this.stopTokenStream();
|
|
@@ -5149,7 +5302,18 @@ class AparteChatHost {
|
|
|
5149
5302
|
if (msg.segments !== void 0) updates.segments = msg.segments;
|
|
5150
5303
|
if (msg.status !== void 0) updates.status = msg.status;
|
|
5151
5304
|
if (msg.usage !== void 0) updates.usage = msg.usage;
|
|
5305
|
+
if (msg.model !== void 0) updates.model = msg.model;
|
|
5152
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
|
+
}
|
|
5153
5317
|
}
|
|
5154
5318
|
}
|
|
5155
5319
|
}
|
|
@@ -5157,17 +5321,10 @@ class AparteChatHost {
|
|
|
5157
5321
|
* Write the branch counts onto their bubbles — and RESCHEDULE for the ones that are
|
|
5158
5322
|
* not on the page yet, instead of dropping them.
|
|
5159
5323
|
*
|
|
5160
|
-
*
|
|
5161
|
-
*
|
|
5162
|
-
*
|
|
5163
|
-
*
|
|
5164
|
-
*
|
|
5165
|
-
* It needed a framework that renders LATE, which is every framework here. React's
|
|
5166
|
-
* `afterRender` is `requestAnimationFrame(() => cb())` — a bet that the next paint
|
|
5167
|
-
* lands after React's commit, and it does not always; the repo has lost that same bet
|
|
5168
|
-
* before (`25f356b`, "a bet on rAF phase"). The fix is here rather than in the
|
|
5169
|
-
* wrapper's rAF call, because ANY binding whose `afterRender` can precede its commit
|
|
5170
|
-
* 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.
|
|
5171
5328
|
*
|
|
5172
5329
|
* Bounded, because a sibling can legitimately never arrive — a message that left the
|
|
5173
5330
|
* path has no bubble and never will, and an unbounded retry would hold a callback for
|
|
@@ -5215,12 +5372,11 @@ class AparteChatHost {
|
|
|
5215
5372
|
* The bubble the streaming writes below belong to: the one carrying that message's
|
|
5216
5373
|
* id, and NOTHING when it is not on the page yet.
|
|
5217
5374
|
*
|
|
5218
|
-
* It
|
|
5375
|
+
* It is not "the last bubble in the viewport", which is a different message
|
|
5219
5376
|
* whenever the DOM is a render behind the list — which is every framework here.
|
|
5220
|
-
* `appendMessage` puts the new reply in
|
|
5221
|
-
* schedule, so a first chunk arriving in between
|
|
5222
|
-
*
|
|
5223
|
-
* `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.
|
|
5224
5380
|
*
|
|
5225
5381
|
* Skipping the paint is safe because it is only ever a paint: the message list is
|
|
5226
5382
|
* written either way, and `syncBubbles` — called by the framework after each render,
|
|
@@ -5828,9 +5984,11 @@ let askSeq = 0;
|
|
|
5828
5984
|
function buildApprovalPanel(message, options, onChange, details) {
|
|
5829
5985
|
const cfg = contextConfig();
|
|
5830
5986
|
const panel = el("div", "aparte-elic-panel aparte-approval-panel");
|
|
5987
|
+
const body = el("div", "aparte-elic-body");
|
|
5988
|
+
panel.appendChild(body);
|
|
5831
5989
|
const askText = () => typeof message === "function" ? message() : message;
|
|
5832
5990
|
const ask = askText() ? el("p", "aparte-elic-message", askText()) : null;
|
|
5833
|
-
if (ask)
|
|
5991
|
+
if (ask) body.appendChild(ask);
|
|
5834
5992
|
let argsLabel = null;
|
|
5835
5993
|
if (details) {
|
|
5836
5994
|
argsLabel = el("div", "aparte-approval-args-label", cfg.t("approvalArgsLabel"));
|
|
@@ -5840,7 +5998,7 @@ function buildApprovalPanel(message, options, onChange, details) {
|
|
|
5840
5998
|
pre.setAttribute("role", "group");
|
|
5841
5999
|
pre.setAttribute("aria-labelledby", argsLabel.id);
|
|
5842
6000
|
pre.tabIndex = 0;
|
|
5843
|
-
|
|
6001
|
+
body.append(argsLabel, pre);
|
|
5844
6002
|
}
|
|
5845
6003
|
const dismiss = el("div", "aparte-elic-dismiss");
|
|
5846
6004
|
panel.appendChild(dismiss);
|
|
@@ -5871,13 +6029,13 @@ function buildApprovalPanel(message, options, onChange, details) {
|
|
|
5871
6029
|
row.appendChild(button);
|
|
5872
6030
|
labelled.push({ button, option });
|
|
5873
6031
|
}
|
|
5874
|
-
|
|
6032
|
+
body.appendChild(row);
|
|
5875
6033
|
const instruction = el("textarea", "aparte-approval-instruction");
|
|
5876
6034
|
instruction.rows = 1;
|
|
5877
6035
|
instruction.placeholder = cfg.t("approvalInstructionPlaceholder");
|
|
5878
6036
|
instruction.setAttribute("aria-label", cfg.t("approvalInstructionPlaceholder"));
|
|
5879
6037
|
instruction.addEventListener("input", onChange);
|
|
5880
|
-
|
|
6038
|
+
body.appendChild(instruction);
|
|
5881
6039
|
const typed = () => instruction.value.trim();
|
|
5882
6040
|
return {
|
|
5883
6041
|
el: panel,
|
|
@@ -5906,6 +6064,8 @@ function requestUserInput(request) {
|
|
|
5906
6064
|
return resolveConfig(request.target ?? null).requestUserInput(request);
|
|
5907
6065
|
}
|
|
5908
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]);
|
|
5909
6069
|
const warnedUninlinedFiles = /* @__PURE__ */ new Set();
|
|
5910
6070
|
const CHAT_SELECTOR = "aparte-chat, aparte-chat-viewport, [data-aparte-chat]";
|
|
5911
6071
|
function remoteProvider(id) {
|
|
@@ -5932,15 +6092,13 @@ class AparteClient {
|
|
|
5932
6092
|
/**
|
|
5933
6093
|
* Does this window event belong to this client?
|
|
5934
6094
|
*
|
|
5935
|
-
* One rule for all
|
|
5936
|
-
*
|
|
5937
|
-
*
|
|
5938
|
-
* two paid summarisation calls against whichever chat the DOM scan found
|
|
5939
|
-
* 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.
|
|
5940
6098
|
*
|
|
5941
|
-
* A scoped client also answers only events ADDRESSED to it
|
|
5942
|
-
*
|
|
5943
|
-
*
|
|
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.
|
|
5944
6102
|
*/
|
|
5945
6103
|
_isForThisInstance(e) {
|
|
5946
6104
|
if (!e) return true;
|
|
@@ -5980,17 +6138,15 @@ class AparteClient {
|
|
|
5980
6138
|
/**
|
|
5981
6139
|
* What every fresh user action does before it starts a turn.
|
|
5982
6140
|
*
|
|
5983
|
-
* Clearing the abort flag
|
|
6141
|
+
* Clearing the abort flag happens at the START of the action rather than inside
|
|
5984
6142
|
* `_streamTurn`, which runs after the auth and file-reading awaits — a reset there
|
|
5985
|
-
*
|
|
5986
|
-
* to be read.
|
|
6143
|
+
* would erase an abort that arrived while the user was still waiting for a large
|
|
6144
|
+
* attachment to be read.
|
|
5987
6145
|
*
|
|
5988
6146
|
* The superseded turn's TOOL handlers are not cancelled here, and do not need a
|
|
5989
6147
|
* registry of their own: `_streamTurn` aborts the previous `_streamController`, and
|
|
5990
6148
|
* the engine links every tool handler's controller to that run signal
|
|
5991
|
-
* (`invokeToolHandler`), so cutting the stream cuts its tools.
|
|
5992
|
-
* used to sit here for that job and was never added to — three lines of loop over an
|
|
5993
|
-
* empty set, plus a paragraph describing a cancellation the method did not perform.
|
|
6149
|
+
* (`invokeToolHandler`), so cutting the stream cuts its tools.
|
|
5994
6150
|
*/
|
|
5995
6151
|
_beginUserTurn() {
|
|
5996
6152
|
this._isAborted = false;
|
|
@@ -5999,10 +6155,9 @@ class AparteClient {
|
|
|
5999
6155
|
* Put this client's `keyResolver` on its config, and hold the teardown so
|
|
6000
6156
|
* `stop()` can take it back off.
|
|
6001
6157
|
*
|
|
6002
|
-
* The
|
|
6003
|
-
*
|
|
6004
|
-
*
|
|
6005
|
-
* 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
|
|
6006
6161
|
* `new AparteClient({…}).start()` registers once.
|
|
6007
6162
|
*/
|
|
6008
6163
|
_registerKeyResolver() {
|
|
@@ -6104,41 +6259,24 @@ class AparteClient {
|
|
|
6104
6259
|
this._isAborted = true;
|
|
6105
6260
|
this._streamController?.abort();
|
|
6106
6261
|
}
|
|
6107
|
-
/*
|
|
6108
|
-
* Two docblocks stood here, both describing `_awaitToolDecision` — the method
|
|
6109
|
-
* this replaced. Deleting a method and leaving its documentation is worse than
|
|
6110
|
-
* leaving the method: only the LAST comment before a declaration is its JSDoc,
|
|
6111
|
-
* so those two were floating prose, and one of them explained a DOM-containment
|
|
6112
|
-
* check as a live security property. It was the fix for a cross-chat hazard that
|
|
6113
|
-
* only existed because the decision travelled as a bubbling `document` event.
|
|
6114
|
-
* Routing through `requestUserInput` removed the hazard rather than guarding it,
|
|
6115
|
-
* so the guard is gone too — and a reader who found that comment would have
|
|
6116
|
-
* believed a containment check was still protecting them.
|
|
6117
|
-
*/
|
|
6118
6262
|
/**
|
|
6119
6263
|
* Ask the human at the COMPOSER, and report what they decided.
|
|
6120
6264
|
*
|
|
6121
|
-
* The default channel
|
|
6122
|
-
* that existed only because the buttons lived in a segment renderer with no
|
|
6123
|
-
* reference to this client. Routing through `requestUserInput` puts the decision
|
|
6265
|
+
* The default channel. Routing through `requestUserInput` puts the decision
|
|
6124
6266
|
* where every other request for the user already goes, which is the whole point:
|
|
6125
6267
|
* one queue, one panel slot, one teardown, and the same behaviour whether the
|
|
6126
6268
|
* request came from a tool handler or from this gate.
|
|
6127
6269
|
*
|
|
6128
6270
|
* The tool NAME is the question and its ARGUMENTS are the body, because the
|
|
6129
6271
|
* arguments are what is being approved — a decision surface that names the act and
|
|
6130
|
-
* hides its subject asks for a signature on a blank page.
|
|
6131
|
-
*
|
|
6132
|
-
* it had always promised what the panel did not do. The transcript row stays the
|
|
6133
|
-
* 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
|
|
6134
6274
|
* at 50vh and the block inside it scrolls, so a long input cannot push the options
|
|
6135
6275
|
* off the surface they are asked on.
|
|
6136
6276
|
*
|
|
6137
|
-
* `aparte-tool-decision`
|
|
6138
|
-
*
|
|
6139
|
-
*
|
|
6140
|
-
* decision are one seam too many, and the ratified elicitation rule already said so
|
|
6141
|
-
* — "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
|
|
6142
6280
|
* that wants to answer programmatically registers an `approvalResolver` (headless,
|
|
6143
6281
|
* no DOM) or its own presenter, both strictly more capable than an id on an event.
|
|
6144
6282
|
*/
|
|
@@ -6153,13 +6291,14 @@ class AparteClient {
|
|
|
6153
6291
|
*/
|
|
6154
6292
|
message: () => {
|
|
6155
6293
|
const loc = this._config.getLocale();
|
|
6156
|
-
return (loc.approvalAsk ?? "Run {tool}?").replace("{tool}", event.name);
|
|
6294
|
+
return (loc.approvalAsk ?? "Run {tool}?").replace("{tool}", () => event.name);
|
|
6157
6295
|
},
|
|
6158
|
-
// A plain string, not a function: the arguments are
|
|
6159
|
-
// the
|
|
6160
|
-
//
|
|
6161
|
-
//
|
|
6162
|
-
|
|
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)),
|
|
6163
6302
|
// Functions, not strings, same as the question: these follow a language
|
|
6164
6303
|
// switch while the request is open, which a resolved string cannot.
|
|
6165
6304
|
options: [
|
|
@@ -6255,6 +6394,8 @@ class AparteClient {
|
|
|
6255
6394
|
let result = opts?.resume ? { usage: void 0, suspended: opts.resume } : await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController);
|
|
6256
6395
|
const maxResumes = this.options.maxTurns ?? 10;
|
|
6257
6396
|
let resumes = 0;
|
|
6397
|
+
let approvedAtLastPause = /* @__PURE__ */ new Set();
|
|
6398
|
+
const rowEvents = createStreamAdapter({ target: targetElement, config: this._config, messageId, ...chatId ? { targetId: chatId } : {} });
|
|
6258
6399
|
while (result.suspended) {
|
|
6259
6400
|
const stopped = this._isAborted || streamController.signal.aborted;
|
|
6260
6401
|
if (!stopped && ++resumes > maxResumes) {
|
|
@@ -6264,7 +6405,9 @@ class AparteClient {
|
|
|
6264
6405
|
);
|
|
6265
6406
|
break;
|
|
6266
6407
|
}
|
|
6267
|
-
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;
|
|
6268
6411
|
if (!outcomes) {
|
|
6269
6412
|
this._closeOpenCalls(targetElement, messageId);
|
|
6270
6413
|
if (this._isAborted || streamController.signal.aborted) {
|
|
@@ -6313,29 +6456,21 @@ class AparteClient {
|
|
|
6313
6456
|
/**
|
|
6314
6457
|
* The registered tools, unless the current model says it cannot call one.
|
|
6315
6458
|
*
|
|
6316
|
-
* Single source for the gate, so send / retry / edit cannot drift
|
|
6317
|
-
* is what once shipped `tools` on the initial send while retry and edit omitted
|
|
6318
|
-
* them.
|
|
6459
|
+
* Single source for the gate, so send / retry / edit cannot drift.
|
|
6319
6460
|
*
|
|
6320
|
-
* The
|
|
6321
|
-
*
|
|
6322
|
-
*
|
|
6323
|
-
*
|
|
6324
|
-
*
|
|
6325
|
-
*
|
|
6326
|
-
* function calling", so `tools: []` went on the wire while `getTools()` held
|
|
6327
|
-
* the tool the app had explicitly registered. The model answered, correctly,
|
|
6328
|
-
* that it had no such tool, and nothing anywhere said why.
|
|
6329
|
-
*
|
|
6330
|
-
* So the question is now "did this model say it cannot", not "did it say it
|
|
6331
|
-
* can". Registering a tool is an explicit act by the app; silently dropping it
|
|
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
|
|
6332
6467
|
* because a `/models` listing is terse is the library second-guessing the
|
|
6333
6468
|
* developer. A model that declares its capabilities and omits function calling
|
|
6334
6469
|
* is still honoured — that is a statement, and it is respected.
|
|
6335
6470
|
*
|
|
6336
6471
|
* Failure modes, both ways round: over-sending means a model that cannot use
|
|
6337
6472
|
* tools does not call one, or its own endpoint rejects the array with an error
|
|
6338
|
-
* the developer can read. Under-sending
|
|
6473
|
+
* the developer can read. Under-sending would be silent, total, and look like a
|
|
6339
6474
|
* lying model.
|
|
6340
6475
|
*/
|
|
6341
6476
|
_toolsForCurrentModel() {
|
|
@@ -6386,10 +6521,9 @@ class AparteClient {
|
|
|
6386
6521
|
* The system messages a turn opens with: the app's own template, then the registered
|
|
6387
6522
|
* tools' `systemPrompt`s.
|
|
6388
6523
|
*
|
|
6389
|
-
*
|
|
6390
|
-
*
|
|
6391
|
-
*
|
|
6392
|
-
* 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.
|
|
6393
6527
|
*/
|
|
6394
6528
|
_systemMessages() {
|
|
6395
6529
|
const messages = [];
|
|
@@ -6472,11 +6606,11 @@ class AparteClient {
|
|
|
6472
6606
|
* The trees a lookup must search, the one the gesture came from FIRST.
|
|
6473
6607
|
*
|
|
6474
6608
|
* `document.getElementById` and `document.querySelectorAll` cannot see into a shadow
|
|
6475
|
-
* root, so a chat mounted in one — the arrangement the theming guide endorses —
|
|
6476
|
-
* invisible to both: alone, the send
|
|
6477
|
-
* holds a chat in the light DOM, the scan
|
|
6478
|
-
* person's message AND the model's reply
|
|
6479
|
-
* 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.
|
|
6480
6614
|
*/
|
|
6481
6615
|
_rootsFor(event) {
|
|
6482
6616
|
const roots = [];
|
|
@@ -6574,8 +6708,8 @@ class AparteClient {
|
|
|
6574
6708
|
* The SLICE is each caller's own business — retry cuts before the reply it is
|
|
6575
6709
|
* regenerating, edit cuts after the message being reworded, send cuts at the last
|
|
6576
6710
|
* answered turn. What a message CONTRIBUTES to the wire is not: send, retry and
|
|
6577
|
-
* edit must not disagree about it
|
|
6578
|
-
*
|
|
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
|
|
6579
6713
|
* `{ role: 'assistant', content: '' }` — a claim the assistant said nothing, which
|
|
6580
6714
|
* some providers reject outright and the rest read as an empty reply worth
|
|
6581
6715
|
* imitating.
|
|
@@ -6666,7 +6800,7 @@ class AparteClient {
|
|
|
6666
6800
|
if (!this._warnedNoModel) {
|
|
6667
6801
|
this._warnedNoModel = true;
|
|
6668
6802
|
console.warn(
|
|
6669
|
-
"[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."
|
|
6670
6804
|
);
|
|
6671
6805
|
}
|
|
6672
6806
|
this._handleLifecycleError(targetElement, messageId, new AparteError(
|
|
@@ -6733,12 +6867,12 @@ class AparteClient {
|
|
|
6733
6867
|
* the question the history gates can ask is "is this still in flight?", never
|
|
6734
6868
|
* "is this completed?".
|
|
6735
6869
|
*
|
|
6736
|
-
* The gates below
|
|
6870
|
+
* The gates below ask the first, because a host that SEEDS a transcript
|
|
6737
6871
|
* — restoring a saved conversation, hydrating a server-rendered one — has no
|
|
6738
|
-
* reason to invent a status for turns that are already over.
|
|
6739
|
-
*
|
|
6740
|
-
* model
|
|
6741
|
-
* 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.
|
|
6742
6876
|
*/
|
|
6743
6877
|
static _inFlight(m) {
|
|
6744
6878
|
return m.status === "streaming" || m.status === "pending";
|
|
@@ -6769,23 +6903,9 @@ ${text}` : text;
|
|
|
6769
6903
|
* What the model is told it said last turn — reconstructed from what was
|
|
6770
6904
|
* RENDERED, not from the raw-text field.
|
|
6771
6905
|
*
|
|
6772
|
-
* The
|
|
6773
|
-
*
|
|
6774
|
-
*
|
|
6775
|
-
* correct-in-isolation decisions composed into it:
|
|
6776
|
-
*
|
|
6777
|
-
* 1. the parser withholds an ambiguous prefix (``` ``` ```, `` ` ``, `<`)
|
|
6778
|
-
* waiting for the next delta, and creates no active segment while it waits;
|
|
6779
|
-
* 2. so `_streamLoop` sees zero segments, concludes the parser produced
|
|
6780
|
-
* nothing, and appends the raw delta straight to `message.content`;
|
|
6781
|
-
* 3. and this method preferred `content`.
|
|
6782
|
-
*
|
|
6783
|
-
* The bubble hides its content element as soon as segments exist, so the UI was
|
|
6784
|
-
* perfect and only the NEXT request showed it. No unit test, no example and
|
|
6785
|
-
* no browser test could see it.
|
|
6786
|
-
*
|
|
6787
|
-
* `content` stays as the fallback, and that is not vestigial: a non-streaming
|
|
6788
|
-
* 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.
|
|
6789
6909
|
*/
|
|
6790
6910
|
_extractText(message) {
|
|
6791
6911
|
const rendered = this._segmentsToText(message.segments);
|
|
@@ -6803,10 +6923,9 @@ ${text}` : text;
|
|
|
6803
6923
|
* A type core does not know — a registered block grammar's (an artifact, a
|
|
6804
6924
|
* citation, a file), a consumer's custom segment — follows one rule: its
|
|
6805
6925
|
* `content` if it has one, else its `fallback`, else nothing. Dropping such a
|
|
6806
|
-
* segment
|
|
6807
|
-
* turn, so it could not be asked to change the thing it had built
|
|
6808
|
-
*
|
|
6809
|
-
* 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.
|
|
6810
6929
|
*
|
|
6811
6930
|
* `thinking` and `tool_call` stay out on purpose — the first is the model's own
|
|
6812
6931
|
* scratchpad, and most APIs neither want it back nor bill for it kindly; the
|
|
@@ -7026,10 +7145,15 @@ ${content}
|
|
|
7026
7145
|
* nobody answered — the turn was stopped, or the channel gave up — and there is no
|
|
7027
7146
|
* resume. A channel that CRASHES fails the turn, as it does for a run in the page.
|
|
7028
7147
|
*/
|
|
7029
|
-
async _answerDeferred(targetElement, pending, signal) {
|
|
7148
|
+
async _answerDeferred(targetElement, pending, signal, rowEvents, approvedBefore = /* @__PURE__ */ new Set(), approvedNow = /* @__PURE__ */ new Set()) {
|
|
7030
7149
|
const outcomes = {};
|
|
7031
7150
|
const resolver = this._approvalResolverFor(targetElement, true);
|
|
7032
|
-
|
|
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()) {
|
|
7033
7157
|
if (signal.aborted) break;
|
|
7034
7158
|
const call = { id: p.toolCallId, name: p.name, input: p.input ?? {} };
|
|
7035
7159
|
if (p.reason === "approval") {
|
|
@@ -7041,11 +7165,34 @@ ${content}
|
|
|
7041
7165
|
}
|
|
7042
7166
|
if (!decision || signal.aborted) return void 0;
|
|
7043
7167
|
outcomes[p.toolCallId] = { kind: "decision", ...decision };
|
|
7168
|
+
if (decision.approved) approvedNow.add(approvalKey(call));
|
|
7169
|
+
else {
|
|
7170
|
+
refuseTheRest(index);
|
|
7171
|
+
break;
|
|
7172
|
+
}
|
|
7044
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;
|
|
7045
7192
|
const handler = this._config.getToolHandler(p.name);
|
|
7046
7193
|
try {
|
|
7047
7194
|
if (!handler) throw new Error(`No browser handler is registered for "${p.name}".`);
|
|
7048
|
-
const result = await handler(
|
|
7195
|
+
const result = await handler(ran, signal, { target: targetElement, config: this._config });
|
|
7049
7196
|
outcomes[p.toolCallId] = {
|
|
7050
7197
|
kind: "result",
|
|
7051
7198
|
content: result.content,
|
|
@@ -7059,6 +7206,21 @@ ${content}
|
|
|
7059
7206
|
}
|
|
7060
7207
|
return signal.aborted ? void 0 : outcomes;
|
|
7061
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
|
+
}
|
|
7062
7224
|
/**
|
|
7063
7225
|
* The ticket is spent the moment the resume leaves: the server answers these calls
|
|
7064
7226
|
* now, so a pause saved from here on — the conversation manager saves before it cuts a
|
|
@@ -7081,8 +7243,8 @@ ${content}
|
|
|
7081
7243
|
* Close the reply's calls that can no longer finish — paused, queued, or running on a
|
|
7082
7244
|
* server the page stopped listening to: each says it stopped, and forgets the pause.
|
|
7083
7245
|
* A cut connection carries none of their last events, so nothing else would: the rows
|
|
7084
|
-
*
|
|
7085
|
-
* 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
|
|
7086
7248
|
* the wait moves the head. A run in the page ends every call itself: nothing to do.
|
|
7087
7249
|
*/
|
|
7088
7250
|
_closeOpenCalls(targetElement, messageId) {
|
|
@@ -7327,10 +7489,9 @@ class AparteMessageRepository {
|
|
|
7327
7489
|
/**
|
|
7328
7490
|
* Every message the tree holds — every branch, not just the active path.
|
|
7329
7491
|
*
|
|
7330
|
-
* `getMessages()
|
|
7331
|
-
*
|
|
7332
|
-
*
|
|
7333
|
-
* 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
|
|
7334
7495
|
* insertion: a caller that needs the topology reads `export()`.
|
|
7335
7496
|
*/
|
|
7336
7497
|
getAllMessages() {
|
|
@@ -7399,9 +7560,7 @@ class AparteMessageRepository {
|
|
|
7399
7560
|
* Remove all descendants of `messageId` (inclusive) and, when the head was among
|
|
7400
7561
|
* them, set `head` to the parent of `messageId`. Used by edit/truncate flows.
|
|
7401
7562
|
*
|
|
7402
|
-
* A head in a branch this call did not touch stays where it is
|
|
7403
|
-
* relocated regardless, which took the active path out of the version the reader was
|
|
7404
|
-
* on for a truncate somewhere else entirely.
|
|
7563
|
+
* A head in a branch this call did not touch stays where it is.
|
|
7405
7564
|
*
|
|
7406
7565
|
* @param messageId The first message to remove
|
|
7407
7566
|
*/
|
|
@@ -7455,9 +7614,8 @@ class AparteMessageRepository {
|
|
|
7455
7614
|
/**
|
|
7456
7615
|
* Import a previously-exported snapshot, rebuilding the full tree and
|
|
7457
7616
|
* restoring the active head. Replaces any existing content (calls `clear()`
|
|
7458
|
-
* first).
|
|
7459
|
-
*
|
|
7460
|
-
* 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.
|
|
7461
7619
|
*/
|
|
7462
7620
|
import(exported) {
|
|
7463
7621
|
this.clear();
|
|
@@ -7485,11 +7643,8 @@ class AparteMessageRepository {
|
|
|
7485
7643
|
* Compatible with the assistant-ui ExportedMessageRepository format.
|
|
7486
7644
|
*
|
|
7487
7645
|
* Walked from the root through each node's `children`, so a parent is always
|
|
7488
|
-
* written before its children
|
|
7489
|
-
*
|
|
7490
|
-
* under one inserted after it (`addOrUpdateMessage` with a known id and a newer
|
|
7491
|
-
* parent). The snapshot then listed the child first, and `import()` threw "parent
|
|
7492
|
-
* 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.
|
|
7493
7648
|
*/
|
|
7494
7649
|
export() {
|
|
7495
7650
|
const messages = [];
|
|
@@ -7713,18 +7868,20 @@ export {
|
|
|
7713
7868
|
uuid as a8,
|
|
7714
7869
|
eventOrigin as a9,
|
|
7715
7870
|
activeElementIn as aa,
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
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,
|
|
7728
7885
|
APARTE_DEFAULT_BUBBLE_ACTIONS as b,
|
|
7729
7886
|
APARTE_DEFAULT_HOST_HANDLERS as c,
|
|
7730
7887
|
APARTE_DEFAULT_ICON_FALLBACKS as d,
|
|
@@ -7751,4 +7908,4 @@ export {
|
|
|
7751
7908
|
collectRendererStyles as y,
|
|
7752
7909
|
contentToText as z
|
|
7753
7910
|
};
|
|
7754
|
-
//# sourceMappingURL=dialog-triggers-
|
|
7911
|
+
//# sourceMappingURL=dialog-triggers-3Rk7PuZa.js.map
|