@aparte/core 0.16.11 → 0.17.1
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 +11 -4
- package/dist/client/aparte-client.d.ts +42 -3
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts +22 -0
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/chat/aparte-chat.d.ts +55 -14
- package/dist/components/chat/aparte-chat.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-action.d.ts +5 -3
- package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-toolbar.d.ts +5 -2
- package/dist/components/composer/aparte-composer-toolbar.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer.d.ts +27 -0
- package/dist/components/composer/aparte-composer.d.ts.map +1 -1
- package/dist/components/context/aparte-context.d.ts +8 -0
- package/dist/components/context/aparte-context.d.ts.map +1 -1
- package/dist/components/conversation-list/aparte-conversation-list.d.ts +82 -17
- package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
- package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -1
- package/dist/components/sidebar/aparte-sidebar.d.ts.map +1 -1
- package/dist/components/split/aparte-split.d.ts.map +1 -1
- package/dist/components/suggestions/aparte-suggestions.d.ts +37 -3
- package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +55 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/action-provider.d.ts +14 -14
- package/dist/config/action-provider.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +109 -14
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/bubble-shell-renderer.d.ts +50 -12
- package/dist/config/bubble-shell-renderer.d.ts.map +1 -1
- package/dist/config/locale.d.ts +33 -1
- 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 +14 -0
- package/dist/conversations/conversation-controller.d.ts.map +1 -1
- package/dist/conversations/conversation-manager.d.ts +57 -19
- package/dist/conversations/conversation-manager.d.ts.map +1 -1
- package/dist/conversations/types.d.ts +18 -11
- package/dist/conversations/types.d.ts.map +1 -1
- package/dist/custom-elements.json +12007 -11027
- package/dist/{dialog-triggers-cYGTlfZf.js → dialog-triggers-CJWw0P3l.js} +652 -161
- package/dist/dialog-triggers-CJWw0P3l.js.map +1 -0
- package/dist/generated/element-attributes.d.ts +15 -9
- package/dist/generated/element-attributes.d.ts.map +1 -1
- package/dist/host/aparte-chat-host.d.ts +30 -0
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/index.css +1 -8393
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +644 -111
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +1 -1
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +1 -1
- package/dist/index.node.js.map +1 -1
- package/dist/interop/element-props.d.ts +1 -1
- package/dist/primitives/select/aparte-optgroup.d.ts +1 -0
- package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
- package/dist/primitives/select/aparte-select.d.ts +12 -1
- package/dist/primitives/select/aparte-select.d.ts.map +1 -1
- package/dist/renderers/markdown-stream.d.ts.map +1 -1
- package/dist/renderers/segments/tool-call.d.ts.map +1 -1
- package/dist/runtime/message-repository.d.ts +16 -3
- package/dist/runtime/message-repository.d.ts.map +1 -1
- package/dist/types/chat.d.ts +41 -46
- package/dist/types/chat.d.ts.map +1 -1
- package/dist/types/event-map.d.ts +7 -7
- package/dist/types/event-map.d.ts.map +1 -1
- package/dist/types/events.d.ts +7 -6
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/imperative-api.d.ts +16 -0
- package/dist/types/imperative-api.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/models.d.ts +9 -0
- package/dist/types/models.d.ts.map +1 -1
- package/dist/types/segments.d.ts +56 -13
- package/dist/types/segments.d.ts.map +1 -1
- package/dist/types/tools.d.ts +14 -0
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/utils/event-target.d.ts +31 -0
- package/dist/utils/event-target.d.ts.map +1 -0
- package/dist/utils/transcript.d.ts +13 -0
- package/dist/utils/transcript.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/dialog-triggers-cYGTlfZf.js.map +0 -1
|
@@ -147,6 +147,8 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
147
147
|
rejectTool: "Reject",
|
|
148
148
|
messageInfo: "Details",
|
|
149
149
|
newChat: "New Chat",
|
|
150
|
+
loadingConversations: "Loading conversations",
|
|
151
|
+
loadingConversation: "Loading the conversation",
|
|
150
152
|
deleteConversation: "Delete",
|
|
151
153
|
archiveConversation: "Archive",
|
|
152
154
|
unarchiveConversation: "Unarchive",
|
|
@@ -165,8 +167,7 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
165
167
|
scrollRailLabel: "Conversation outline",
|
|
166
168
|
sidebarLabel: "Conversations",
|
|
167
169
|
splitHandleLabel: "Resize the panes",
|
|
168
|
-
transcript: "Transcript"
|
|
169
|
-
direction: "ltr"
|
|
170
|
+
transcript: "Transcript"
|
|
170
171
|
};
|
|
171
172
|
function isFormatAdapter(p) {
|
|
172
173
|
return typeof p.buildRequest === "function" && typeof p.parseStream === "function" && typeof p.defaultEndpoint === "string" && (typeof p.authHeaders === "function" || typeof p.authQuery === "function");
|
|
@@ -982,7 +983,7 @@ const ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
|
982
983
|
"picture",
|
|
983
984
|
"source",
|
|
984
985
|
"input",
|
|
985
|
-
// GFM task-list checkboxes only
|
|
986
|
+
// GFM task-list checkboxes only — `copyAttributes` refuses every other `type`
|
|
986
987
|
"abbr",
|
|
987
988
|
"details",
|
|
988
989
|
"summary",
|
|
@@ -1086,6 +1087,7 @@ function copyAttributes(src, dest, tag) {
|
|
|
1086
1087
|
if (name.startsWith("on") || name.startsWith("formaction")) continue;
|
|
1087
1088
|
if (!name.startsWith("data-") && !GLOBAL_ATTRS.has(name) && !(extra && extra.has(name))) continue;
|
|
1088
1089
|
if (URL_ATTRS.has(name) && !isSafeUrl(value, tag)) continue;
|
|
1090
|
+
if (tag === "input" && name === "type" && value.toLowerCase() !== "checkbox") continue;
|
|
1089
1091
|
if (name === "srcset" && !isSafeSrcset(value, tag)) continue;
|
|
1090
1092
|
if (name === "style") {
|
|
1091
1093
|
const scrubbed = scrubStyle(value);
|
|
@@ -1233,6 +1235,11 @@ class AparteConfig {
|
|
|
1233
1235
|
_iconProvider;
|
|
1234
1236
|
_avatarProvider;
|
|
1235
1237
|
_keyProvider;
|
|
1238
|
+
/**
|
|
1239
|
+
* Sources added with `registerKeyProvider` — consulted before `_keyProvider`,
|
|
1240
|
+
* newest first (see `getKey`).
|
|
1241
|
+
*/
|
|
1242
|
+
_extraKeyProviders = /* @__PURE__ */ new Set();
|
|
1236
1243
|
_locale = APARTE_DEFAULT_LOCALE;
|
|
1237
1244
|
_actions = [];
|
|
1238
1245
|
_listeners = /* @__PURE__ */ new Set();
|
|
@@ -1307,10 +1314,11 @@ class AparteConfig {
|
|
|
1307
1314
|
* (soon) the tool-approval gate. A custom presenter registered by a consumer gets
|
|
1308
1315
|
* the same protection for free, which it previously had none of.
|
|
1309
1316
|
*/
|
|
1310
|
-
_elicitationQueue =
|
|
1317
|
+
_elicitationQueue = /* @__PURE__ */ new Map();
|
|
1311
1318
|
// Tool Registry
|
|
1312
1319
|
_tools = /* @__PURE__ */ new Map();
|
|
1313
1320
|
_toolRenderers = /* @__PURE__ */ new Map();
|
|
1321
|
+
_toolSummaries = /* @__PURE__ */ new Map();
|
|
1314
1322
|
/** Tagged blocks the stream parser recognises in the prose — see {@link registerStreamBlock}. */
|
|
1315
1323
|
_streamBlocks = /* @__PURE__ */ new Map();
|
|
1316
1324
|
_segmentDefaults = /* @__PURE__ */ new Map();
|
|
@@ -1323,9 +1331,10 @@ class AparteConfig {
|
|
|
1323
1331
|
// Provider Setters (Dependency Injection)
|
|
1324
1332
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1325
1333
|
/**
|
|
1326
|
-
* Register a custom action button. `zones` places it in the
|
|
1327
|
-
*
|
|
1328
|
-
*
|
|
1334
|
+
* Register a custom action button. `zones` places it in the message (bubble)
|
|
1335
|
+
* toolbar. Re-registering the same id overwrites it. Notifies mounted elements
|
|
1336
|
+
* so they re-render. A button in the composer is an `<aparte-composer-action>`
|
|
1337
|
+
* element you write in your markup, not a registry entry.
|
|
1329
1338
|
*/
|
|
1330
1339
|
registerAction(action) {
|
|
1331
1340
|
const existing = this._actions.findIndex((a) => a.id === action.id);
|
|
@@ -1347,17 +1356,6 @@ class AparteConfig {
|
|
|
1347
1356
|
this._actions = this._actions.filter((a) => a.id !== id);
|
|
1348
1357
|
if (this._actions.length !== before) this._notify();
|
|
1349
1358
|
}
|
|
1350
|
-
/**
|
|
1351
|
-
* Show or hide a composer action button by id.
|
|
1352
|
-
* Triggers a config update so all mounted composer elements react immediately.
|
|
1353
|
-
*/
|
|
1354
|
-
setActionHidden(id, hidden) {
|
|
1355
|
-
const action = this._actions.find((a) => a.id === id);
|
|
1356
|
-
if (action) {
|
|
1357
|
-
action.composer = { ...action.composer, hidden };
|
|
1358
|
-
this._notify();
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
1359
|
/**
|
|
1362
1360
|
* Configure which action buttons appear in message bubbles.
|
|
1363
1361
|
* Unset keys keep their defaults — see {@link APARTE_DEFAULT_BUBBLE_ACTIONS}: `copy`
|
|
@@ -1418,8 +1416,11 @@ class AparteConfig {
|
|
|
1418
1416
|
* Set an incremental (streaming) Markdown renderer provider. Optional —
|
|
1419
1417
|
* when set, the chat bubble uses it to render the assistant message
|
|
1420
1418
|
* token-by-token DURING streaming (incremental parse + DOM append, O(n)),
|
|
1421
|
-
* instead of re-parsing the whole string on every token.
|
|
1422
|
-
* `setMarkdownProvider`
|
|
1419
|
+
* instead of re-parsing the whole string on every token. When the turn ends,
|
|
1420
|
+
* a registered one-shot `setMarkdownProvider` re-renders the finished message
|
|
1421
|
+
* at full fidelity; with no one-shot provider registered, what the streaming
|
|
1422
|
+
* renderer drew is kept (sanitised), rather than replaced by the built-in
|
|
1423
|
+
* escape-and-line-break fallback.
|
|
1423
1424
|
*/
|
|
1424
1425
|
setStreamingMarkdownProvider(fn) {
|
|
1425
1426
|
this._streamingMarkdownProvider = fn;
|
|
@@ -1676,11 +1677,57 @@ class AparteConfig {
|
|
|
1676
1677
|
this._keyProvider = provider;
|
|
1677
1678
|
}
|
|
1678
1679
|
/**
|
|
1679
|
-
*
|
|
1680
|
+
* Add a key source, without replacing the one `setKeyProvider` holds. Returns a
|
|
1681
|
+
* teardown.
|
|
1682
|
+
*
|
|
1683
|
+
* This is how `AparteClient({ keyResolver })` — the channel the docs teach as
|
|
1684
|
+
* primary — reaches everything that needs a key rather than the chat alone. It
|
|
1685
|
+
* used to reach the chat ONLY, so the model selector, whose one data path is
|
|
1686
|
+
* `refreshProviderModels`, showed an empty list on every cloud provider while
|
|
1687
|
+
* the chat worked: `fetchModels` returns `[]` without a key, so there was
|
|
1688
|
+
* nothing to see and nothing said.
|
|
1689
|
+
*
|
|
1690
|
+
* The direction matters (decision #9): the client registers a source ON the
|
|
1691
|
+
* config, so the capability is never hostage to the client — a page that only
|
|
1692
|
+
* ever calls `setKeyProvider` is unchanged, and a page that constructs no
|
|
1693
|
+
* client at all still lists models.
|
|
1694
|
+
*
|
|
1695
|
+
* Every call is its own source, even when the same function is handed in
|
|
1696
|
+
* twice: two panes sharing one module-level resolver are two mounts, and the
|
|
1697
|
+
* first one to unmount must not take the second one's key with it. A `Set`
|
|
1698
|
+
* keyed by the function itself made them one entry, which either teardown
|
|
1699
|
+
* removed.
|
|
1700
|
+
*/
|
|
1701
|
+
registerKeyProvider(provider) {
|
|
1702
|
+
const entry = (providerId) => provider(providerId);
|
|
1703
|
+
this._extraKeyProviders.add(entry);
|
|
1704
|
+
return () => {
|
|
1705
|
+
this._extraKeyProviders.delete(entry);
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* The credentials for a provider: a key, or the `{ apiKey, endpoint }` record
|
|
1710
|
+
* the transport and `fetchModels` both read.
|
|
1711
|
+
*
|
|
1712
|
+
* ONE resolution order, so the chat and the model list cannot disagree about
|
|
1713
|
+
* what an endpoint is: a registered resolver (`AparteClient`'s `keyResolver`)
|
|
1714
|
+
* first — which is the precedence the client has always documented — then
|
|
1715
|
+
* `setKeyProvider`.
|
|
1716
|
+
*
|
|
1717
|
+
* Registered sources answer NEWEST first. A page that mounts a second chat, or
|
|
1718
|
+
* remounts one to swap its options — the swap the wrappers document — leaves
|
|
1719
|
+
* more than one source here, and insertion order made the first resolver the
|
|
1720
|
+
* page ever registered answer for every later one, so the key could not be
|
|
1721
|
+
* changed. The snapshot is deliberate too: a source torn down while an earlier
|
|
1722
|
+
* one is being awaited must not shift the iteration underneath it.
|
|
1680
1723
|
*/
|
|
1681
1724
|
async getKey(providerId) {
|
|
1725
|
+
for (const provider of [...this._extraKeyProviders].reverse()) {
|
|
1726
|
+
const resolved = await provider(providerId);
|
|
1727
|
+
if (resolved) return resolved;
|
|
1728
|
+
}
|
|
1682
1729
|
if (this._keyProvider) {
|
|
1683
|
-
return await this._keyProvider(providerId);
|
|
1730
|
+
return await this._keyProvider(providerId) || void 0;
|
|
1684
1731
|
}
|
|
1685
1732
|
return void 0;
|
|
1686
1733
|
}
|
|
@@ -1693,8 +1740,8 @@ class AparteConfig {
|
|
|
1693
1740
|
const provider = this._aiProviders.get(providerId);
|
|
1694
1741
|
if (!provider || !provider.fetchModels) return [];
|
|
1695
1742
|
try {
|
|
1696
|
-
const
|
|
1697
|
-
const models = await provider.fetchModels(
|
|
1743
|
+
const auth = await this.getKey(providerId);
|
|
1744
|
+
const models = await provider.fetchModels(auth);
|
|
1698
1745
|
this._fetchedModels.set(providerId, models);
|
|
1699
1746
|
return models;
|
|
1700
1747
|
} catch (error) {
|
|
@@ -2064,6 +2111,13 @@ class AparteConfig {
|
|
|
2064
2111
|
* Controls what appears in the chat bubble when the AI calls this tool.
|
|
2065
2112
|
* Use this instead of the generic `tool_call` segment renderer for tool-specific UI.
|
|
2066
2113
|
*
|
|
2114
|
+
* A renderer that needs a popover (a `<aparte-select>`, a menu) cannot open one
|
|
2115
|
+
* inside the row: the bubble is `content-visibility: auto`, which makes it the
|
|
2116
|
+
* containing block for `position: fixed` descendants and clips a popover to its
|
|
2117
|
+
* own box. Mount it on `document.body` and position it from the trigger's rect
|
|
2118
|
+
* instead — see [Custom tool
|
|
2119
|
+
* renderer](https://apartejs.dev/guides/tools/#custom-tool-renderer).
|
|
2120
|
+
*
|
|
2067
2121
|
* @example
|
|
2068
2122
|
* // Hide the segment entirely (UI-only tool like ask_user)
|
|
2069
2123
|
* aparteGlobalConfig.registerToolRenderer('ask_user', { render: () => '' });
|
|
@@ -2121,6 +2175,37 @@ class AparteConfig {
|
|
|
2121
2175
|
getToolRenderer(toolName) {
|
|
2122
2176
|
return this._toolRenderers.get(toolName);
|
|
2123
2177
|
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Say what a call targets, in the default row, after the tool's name.
|
|
2180
|
+
*
|
|
2181
|
+
* The default row names the tool and keeps its arguments behind a disclosure, so a
|
|
2182
|
+
* reader saw that a file was read, not which one. Replacing the row with
|
|
2183
|
+
* `registerToolRenderer` for that one line meant re-implementing its six states,
|
|
2184
|
+
* the spinner, the disclosure and the relabel contract. This keeps all of them and
|
|
2185
|
+
* adds the line. A tool with its own renderer draws its own row, so its summary is
|
|
2186
|
+
* never read.
|
|
2187
|
+
*
|
|
2188
|
+
* The text is written as text, never parsed as HTML. It is read when the row
|
|
2189
|
+
* renders, on every update to the call (its arguments may still be arriving), and
|
|
2190
|
+
* on every config change — so registering one after a row is on screen shows up on
|
|
2191
|
+
* that row's next update.
|
|
2192
|
+
*
|
|
2193
|
+
* @example
|
|
2194
|
+
* // read_file skills.json
|
|
2195
|
+
* aparteGlobalConfig.registerToolSummary('read_file', (segment) =>
|
|
2196
|
+
* String(segment.toolCall.input['path'] ?? ''));
|
|
2197
|
+
*/
|
|
2198
|
+
registerToolSummary(toolName, summary) {
|
|
2199
|
+
this._toolSummaries.set(toolName, summary);
|
|
2200
|
+
}
|
|
2201
|
+
/** Unregister a per-tool summary. */
|
|
2202
|
+
unregisterToolSummary(toolName) {
|
|
2203
|
+
this._toolSummaries.delete(toolName);
|
|
2204
|
+
}
|
|
2205
|
+
/** Get the summary for a specific tool name. Returns undefined if none registered. */
|
|
2206
|
+
getToolSummary(toolName) {
|
|
2207
|
+
return this._toolSummaries.get(toolName);
|
|
2208
|
+
}
|
|
2124
2209
|
/**
|
|
2125
2210
|
* Teach the stream parser a tagged block: `<tag attr="…">…</tag>` in the model's
|
|
2126
2211
|
* prose becomes the segment `toSegment` builds, streamed delta by delta.
|
|
@@ -2257,17 +2342,32 @@ class AparteConfig {
|
|
|
2257
2342
|
* "installs itself — nothing to register", which is how that was found.
|
|
2258
2343
|
*/
|
|
2259
2344
|
requestUserInput(request) {
|
|
2260
|
-
const
|
|
2261
|
-
const
|
|
2345
|
+
const queue = this._elicitationQueueKey(request);
|
|
2346
|
+
const previous = this._elicitationQueue.get(queue);
|
|
2347
|
+
const settled = previous === void 0 ? this._present(request) : previous.then(
|
|
2262
2348
|
() => this._present(request),
|
|
2263
2349
|
() => this._present(request)
|
|
2264
2350
|
);
|
|
2265
2351
|
const mine = settled.then(NOOP, NOOP).then(() => {
|
|
2266
|
-
if (this._elicitationQueue === mine) this._elicitationQueue
|
|
2352
|
+
if (this._elicitationQueue.get(queue) === mine) this._elicitationQueue.delete(queue);
|
|
2267
2353
|
});
|
|
2268
|
-
this._elicitationQueue
|
|
2354
|
+
this._elicitationQueue.set(queue, mine);
|
|
2269
2355
|
return settled;
|
|
2270
2356
|
}
|
|
2357
|
+
/**
|
|
2358
|
+
* Which queue a request waits in: its own chat's.
|
|
2359
|
+
*
|
|
2360
|
+
* The wait exists so a second question cannot take the panel away from the one on
|
|
2361
|
+
* screen — a property of ONE panel, and `_present` routes by target, so a chat is
|
|
2362
|
+
* the right unit. It used to be one queue for the whole config: two chats on the
|
|
2363
|
+
* global config shared it, and an approval waiting for an answer in the first (which
|
|
2364
|
+
* is all an approval does) held the second's question behind it for ever, with
|
|
2365
|
+
* nothing on screen to answer. Requests that name no chat keep sharing the `null`
|
|
2366
|
+
* queue, because they all reach the same presenter — the top of the stack.
|
|
2367
|
+
*/
|
|
2368
|
+
_elicitationQueueKey(request) {
|
|
2369
|
+
return chatBoundaryOf(request.target);
|
|
2370
|
+
}
|
|
2271
2371
|
/** Hand ONE request to the presenter. Called only from the queue. */
|
|
2272
2372
|
_present(request) {
|
|
2273
2373
|
const presenter = this._presenterFor(request.target);
|
|
@@ -2306,7 +2406,7 @@ class AparteConfig {
|
|
|
2306
2406
|
this._keyProvider = void 0;
|
|
2307
2407
|
this._conversationManager = void 0;
|
|
2308
2408
|
this._elicitationPresenters = [];
|
|
2309
|
-
this._elicitationQueue
|
|
2409
|
+
this._elicitationQueue.clear();
|
|
2310
2410
|
this._locale = APARTE_DEFAULT_LOCALE;
|
|
2311
2411
|
this._actions = [];
|
|
2312
2412
|
this._sanitizer = defaultSanitizer;
|
|
@@ -2314,6 +2414,7 @@ class AparteConfig {
|
|
|
2314
2414
|
this._fetchedModels.clear();
|
|
2315
2415
|
this._tools.clear();
|
|
2316
2416
|
this._toolRenderers.clear();
|
|
2417
|
+
this._toolSummaries.clear();
|
|
2317
2418
|
this._streamBlocks.clear();
|
|
2318
2419
|
this._modelConfig = {};
|
|
2319
2420
|
this._requireModelSelection = false;
|
|
@@ -2399,9 +2500,15 @@ function subscribeConfigChange(el2, handler) {
|
|
|
2399
2500
|
window.addEventListener(APARTE_CONFIG_CHANGE, listener);
|
|
2400
2501
|
return () => window.removeEventListener(APARTE_CONFIG_CHANGE, listener);
|
|
2401
2502
|
}
|
|
2503
|
+
const settledFromParser = /* @__PURE__ */ new WeakMap();
|
|
2504
|
+
const ONE_SHOT_PROBE = "<i>a</i>";
|
|
2505
|
+
function hasOneShotMarkdownProvider() {
|
|
2506
|
+
return contextConfig().renderMarkdown(ONE_SHOT_PROBE) !== escapeHtml(ONE_SHOT_PROBE);
|
|
2507
|
+
}
|
|
2402
2508
|
function writeStreamedMarkdown(host, contentEl, content, streaming) {
|
|
2403
2509
|
if (streaming) {
|
|
2404
2510
|
if (host._aparteSmd === void 0) {
|
|
2511
|
+
settledFromParser.delete(contentEl);
|
|
2405
2512
|
const renderer = contextConfig().createStreamingMarkdownRenderer(contentEl);
|
|
2406
2513
|
if (renderer) {
|
|
2407
2514
|
contentEl.textContent = "";
|
|
@@ -2420,9 +2527,23 @@ function writeStreamedMarkdown(host, contentEl, content, streaming) {
|
|
|
2420
2527
|
return;
|
|
2421
2528
|
}
|
|
2422
2529
|
} else if (host._aparteSmd) {
|
|
2423
|
-
host._aparteSmd
|
|
2530
|
+
const smd = host._aparteSmd;
|
|
2531
|
+
const tail = content.slice(smd.written);
|
|
2532
|
+
if (tail) {
|
|
2533
|
+
smd.renderer.write(tail);
|
|
2534
|
+
smd.written = content.length;
|
|
2535
|
+
}
|
|
2536
|
+
smd.renderer.end();
|
|
2424
2537
|
host._aparteSmd = void 0;
|
|
2538
|
+
if (!hasOneShotMarkdownProvider()) {
|
|
2539
|
+
contentEl.innerHTML = contextConfig().sanitizeHtml(contentEl.innerHTML);
|
|
2540
|
+
settledFromParser.set(contentEl, content);
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
} else if (settledFromParser.get(contentEl) === content && !hasOneShotMarkdownProvider()) {
|
|
2544
|
+
return;
|
|
2425
2545
|
}
|
|
2546
|
+
settledFromParser.delete(contentEl);
|
|
2426
2547
|
contentEl.innerHTML = contextConfig().renderMarkdown(content);
|
|
2427
2548
|
}
|
|
2428
2549
|
function highlightMarkdownFences(contentEl) {
|
|
@@ -2761,12 +2882,31 @@ function highlightParts(element) {
|
|
|
2761
2882
|
});
|
|
2762
2883
|
}
|
|
2763
2884
|
}
|
|
2885
|
+
function toolTarget(segment) {
|
|
2886
|
+
const summary = contextConfig().getToolSummary(segment.toolCall?.name ?? "");
|
|
2887
|
+
if (!summary) return "";
|
|
2888
|
+
try {
|
|
2889
|
+
const text = summary(segment);
|
|
2890
|
+
return typeof text === "string" ? text.trim() : "";
|
|
2891
|
+
} catch {
|
|
2892
|
+
return "";
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
function setTarget(element, segment) {
|
|
2896
|
+
const el2 = element.querySelector(".aparte-tool-target");
|
|
2897
|
+
if (!el2) return;
|
|
2898
|
+
const text = toolTarget(segment);
|
|
2899
|
+
if (el2.textContent !== text) el2.textContent = text;
|
|
2900
|
+
el2.toggleAttribute("hidden", !text);
|
|
2901
|
+
}
|
|
2764
2902
|
function pillMarkup(segment, name, status) {
|
|
2765
2903
|
const badge = stateBadge(segment);
|
|
2904
|
+
const target = toolTarget(segment);
|
|
2766
2905
|
return `
|
|
2767
2906
|
<span class="aparte-tool-label">
|
|
2768
2907
|
<span class="aparte-tool-icon">${contextConfig().getIcon("tool")}</span>
|
|
2769
2908
|
<span class="aparte-tool-name">${escapeHtml(name)}</span>
|
|
2909
|
+
<span class="aparte-tool-target"${target ? "" : " hidden"}>${escapeHtml(target)}</span>
|
|
2770
2910
|
</span>
|
|
2771
2911
|
<span class="aparte-spinner aparte-tool-spinner" aria-hidden="true"${status === "pending" ? "" : " hidden"}></span>
|
|
2772
2912
|
<span class="aparte-tool-state">${badge}</span>`;
|
|
@@ -2831,6 +2971,7 @@ const toolCallRenderer = {
|
|
|
2831
2971
|
}
|
|
2832
2972
|
const icon = element.querySelector(".aparte-tool-icon");
|
|
2833
2973
|
if (icon) icon.innerHTML = cfg.getIcon("tool");
|
|
2974
|
+
setTarget(element, segment);
|
|
2834
2975
|
const statusEl = element.querySelector(".aparte-tool-state");
|
|
2835
2976
|
if (statusEl) statusEl.innerHTML = stateBadge(segment);
|
|
2836
2977
|
},
|
|
@@ -2871,6 +3012,7 @@ const toolCallRenderer = {
|
|
|
2871
3012
|
const badge = element.querySelector(".aparte-tool-state");
|
|
2872
3013
|
if (badge) badge.innerHTML = stateBadge(segment);
|
|
2873
3014
|
element.querySelector(".aparte-tool-spinner")?.toggleAttribute("hidden", status !== "pending");
|
|
3015
|
+
setTarget(element, segment);
|
|
2874
3016
|
setPart(element, "input", input);
|
|
2875
3017
|
setPart(element, "output", segment.result ?? "");
|
|
2876
3018
|
highlightParts(element);
|
|
@@ -3115,6 +3257,7 @@ function populateBubbleFromMessage(bubble, message, siblingInfo) {
|
|
|
3115
3257
|
}
|
|
3116
3258
|
}
|
|
3117
3259
|
const APARTE_CONVERSATION_SCHEMA_VERSION = 2;
|
|
3260
|
+
const APARTE_DERIVED_SEGMENTS = Symbol.for("aparte.derivedSegments");
|
|
3118
3261
|
function applyRetention(messages, tree, maxMessages) {
|
|
3119
3262
|
if (maxMessages <= 0 || messages.length <= maxMessages) return { messages, tree };
|
|
3120
3263
|
const trimmedFlat = messages.slice(messages.length - maxMessages);
|
|
@@ -3144,6 +3287,20 @@ function applyRetention(messages, tree, maxMessages) {
|
|
|
3144
3287
|
};
|
|
3145
3288
|
return { messages: trimmedFlat, tree: trimmedTree };
|
|
3146
3289
|
}
|
|
3290
|
+
function withoutDerived(message) {
|
|
3291
|
+
if (!message[APARTE_DERIVED_SEGMENTS]) return message;
|
|
3292
|
+
const { segments: _derived, ...rest } = message;
|
|
3293
|
+
return rest;
|
|
3294
|
+
}
|
|
3295
|
+
function withoutDerivedSegments(messages) {
|
|
3296
|
+
return messages.map(withoutDerived);
|
|
3297
|
+
}
|
|
3298
|
+
function withoutDerivedTree(tree) {
|
|
3299
|
+
return {
|
|
3300
|
+
...tree,
|
|
3301
|
+
messages: tree.messages.map((entry) => ({ ...entry, message: withoutDerived(entry.message) }))
|
|
3302
|
+
};
|
|
3303
|
+
}
|
|
3147
3304
|
class AparteConversationManager {
|
|
3148
3305
|
_adapter;
|
|
3149
3306
|
_conversations = [];
|
|
@@ -3152,6 +3309,14 @@ class AparteConversationManager {
|
|
|
3152
3309
|
_initialized = false;
|
|
3153
3310
|
_retention = null;
|
|
3154
3311
|
_titleProvider = null;
|
|
3312
|
+
/** Ids whose messages are in memory. Every id, after a loadAll(); only fetched ones after loadMeta(). */
|
|
3313
|
+
_loaded = /* @__PURE__ */ new Set();
|
|
3314
|
+
/** One fetch per id at a time: a second ensureFull(id) while the first is in flight joins it. */
|
|
3315
|
+
_fetching = /* @__PURE__ */ new Map();
|
|
3316
|
+
/** One append per id at a time: the next addMessage(id) reads what the previous one wrote. */
|
|
3317
|
+
_appending = /* @__PURE__ */ new Map();
|
|
3318
|
+
/** The list on its way: a conversation created meanwhile waits for it, or the list would replace it. */
|
|
3319
|
+
_initPromise = null;
|
|
3155
3320
|
constructor(adapter, options) {
|
|
3156
3321
|
this._adapter = adapter;
|
|
3157
3322
|
this._retention = options?.retention ?? null;
|
|
@@ -3171,11 +3336,86 @@ class AparteConversationManager {
|
|
|
3171
3336
|
return this._titleProvider;
|
|
3172
3337
|
}
|
|
3173
3338
|
// ─── Initialisation ────────────────────────────────────────────────────
|
|
3174
|
-
/**
|
|
3339
|
+
/**
|
|
3340
|
+
* Load all conversations from the adapter. Call once at app startup.
|
|
3341
|
+
*
|
|
3342
|
+
* A later call re-reads the list from the store: rows it no longer lists are
|
|
3343
|
+
* dropped, and a conversation whose messages are already in memory keeps them —
|
|
3344
|
+
* so the one on screen goes on being persisted.
|
|
3345
|
+
*/
|
|
3175
3346
|
async init() {
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3347
|
+
const load = (async () => {
|
|
3348
|
+
const split = !!(this._adapter.loadMeta && this._adapter.loadFull);
|
|
3349
|
+
if (this._adapter.loadMeta && !this._adapter.loadFull) {
|
|
3350
|
+
console.warn("[AparteConversationManager] the adapter lists through loadMeta() but has no loadFull(): everything is loaded through loadAll() instead.");
|
|
3351
|
+
}
|
|
3352
|
+
const held = new Map(this._conversations.filter((c) => this._loaded.has(c.id)).map((c) => [c.id, c]));
|
|
3353
|
+
this._loaded.clear();
|
|
3354
|
+
const rows = split ? (await this._adapter.loadMeta()).map((m) => {
|
|
3355
|
+
const kept = held.get(m.id);
|
|
3356
|
+
return kept ? { ...m, messages: kept.messages, ...kept.tree ? { tree: kept.tree } : {} } : { ...m, messages: [] };
|
|
3357
|
+
}) : (await this._adapter.loadAll()).map((c) => ({ ...c, messages: c.messages ?? [] }));
|
|
3358
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3359
|
+
const unique = rows.filter((c) => !seen.has(c.id) && (seen.add(c.id), true));
|
|
3360
|
+
if (unique.length !== rows.length) {
|
|
3361
|
+
console.warn(`[AparteConversationManager] ${rows.length - unique.length} repeated id(s) in the list; the first row wins.`);
|
|
3362
|
+
}
|
|
3363
|
+
this._conversations = unique;
|
|
3364
|
+
for (const c of unique) if (!split || held.has(c.id)) this._loaded.add(c.id);
|
|
3365
|
+
this._initialized = true;
|
|
3366
|
+
this._notify();
|
|
3367
|
+
})();
|
|
3368
|
+
this._initPromise = load;
|
|
3369
|
+
try {
|
|
3370
|
+
await load;
|
|
3371
|
+
} finally {
|
|
3372
|
+
if (this._initPromise === load) this._initPromise = null;
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
/** Whether a conversation's messages are in memory (always, without split storage). */
|
|
3376
|
+
isLoaded(id) {
|
|
3377
|
+
return this._loaded.has(id);
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Fetch a conversation's messages through the adapter's loadFull(), the first time
|
|
3381
|
+
* they are needed; later calls resolve at once. The row keeps its place and its meta;
|
|
3382
|
+
* the messages (and the tree) come from the full record, and a message written to
|
|
3383
|
+
* the row while the fetch ran goes after them. Resolves `false` when the store has no
|
|
3384
|
+
* such record (or answers with another id) — the row is then NOT loaded, and says so,
|
|
3385
|
+
* rather than counting as an empty conversation.
|
|
3386
|
+
*/
|
|
3387
|
+
async ensureFull(id) {
|
|
3388
|
+
if (this._loaded.has(id)) return true;
|
|
3389
|
+
if (!this._adapter.loadFull) return false;
|
|
3390
|
+
const inFlight = this._fetching.get(id);
|
|
3391
|
+
if (inFlight) return inFlight;
|
|
3392
|
+
const fetch2 = (async () => {
|
|
3393
|
+
const full = await this._adapter.loadFull(id);
|
|
3394
|
+
const row = this._find(id);
|
|
3395
|
+
if (!row || !full) return false;
|
|
3396
|
+
if (full.id !== id) {
|
|
3397
|
+
console.warn(`[AparteConversationManager] loadFull('${id}') answered with '${full.id}': ignored.`);
|
|
3398
|
+
return false;
|
|
3399
|
+
}
|
|
3400
|
+
this._replace({ ...row, ...full, id, messages: full.messages ?? [] });
|
|
3401
|
+
this._loaded.add(id);
|
|
3402
|
+
this._notify();
|
|
3403
|
+
return true;
|
|
3404
|
+
})().finally(() => this._fetching.delete(id));
|
|
3405
|
+
this._fetching.set(id, fetch2);
|
|
3406
|
+
return fetch2;
|
|
3407
|
+
}
|
|
3408
|
+
/**
|
|
3409
|
+
* The row with its messages in memory — fetched first when they are not, so a
|
|
3410
|
+
* metadata change never writes a row back without them. `null` when the id is
|
|
3411
|
+
* unknown or the store has no record to fetch.
|
|
3412
|
+
*/
|
|
3413
|
+
async _full(id) {
|
|
3414
|
+
if (!this._loaded.has(id) && this._adapter.loadFull) {
|
|
3415
|
+
const loaded = await this.ensureFull(id);
|
|
3416
|
+
if (!loaded) return null;
|
|
3417
|
+
}
|
|
3418
|
+
return this._find(id) ?? null;
|
|
3179
3419
|
}
|
|
3180
3420
|
/**
|
|
3181
3421
|
* Whether `init()` has completed at least once. Consumers use this to
|
|
@@ -3207,8 +3447,12 @@ class AparteConversationManager {
|
|
|
3207
3447
|
return this._conversations.filter((c) => !!c.archivedAt).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
3208
3448
|
}
|
|
3209
3449
|
// ─── Mutations ──────────────────────────────────────────────────────────
|
|
3210
|
-
/**
|
|
3211
|
-
|
|
3450
|
+
/**
|
|
3451
|
+
* Create a new empty conversation, persist it, and make it active. Untitled by
|
|
3452
|
+
* default: the list shows its locale's `newChat` word for an empty title, so the
|
|
3453
|
+
* store never carries an English string a French UI would then display.
|
|
3454
|
+
*/
|
|
3455
|
+
async createNew(title = "") {
|
|
3212
3456
|
const now = Date.now();
|
|
3213
3457
|
const conv = {
|
|
3214
3458
|
id: uuid(),
|
|
@@ -3218,9 +3462,16 @@ class AparteConversationManager {
|
|
|
3218
3462
|
messages: [],
|
|
3219
3463
|
schemaVersion: APARTE_CONVERSATION_SCHEMA_VERSION
|
|
3220
3464
|
};
|
|
3221
|
-
this._conversations = [conv, ...this._conversations];
|
|
3222
3465
|
this._activeId = conv.id;
|
|
3223
|
-
|
|
3466
|
+
this._loaded.add(conv.id);
|
|
3467
|
+
if (this._initPromise) await this._initPromise.catch(() => {
|
|
3468
|
+
});
|
|
3469
|
+
this._conversations = [conv, ...this._conversations];
|
|
3470
|
+
try {
|
|
3471
|
+
await this._adapter.save(conv);
|
|
3472
|
+
} catch (err) {
|
|
3473
|
+
console.warn("[AparteConversationManager] save() failed for a new conversation; it stays in memory and the next write retries.", err);
|
|
3474
|
+
}
|
|
3224
3475
|
this._notify();
|
|
3225
3476
|
return conv;
|
|
3226
3477
|
}
|
|
@@ -3235,17 +3486,41 @@ class AparteConversationManager {
|
|
|
3235
3486
|
this._activeId = null;
|
|
3236
3487
|
this._notify();
|
|
3237
3488
|
}
|
|
3238
|
-
/**
|
|
3489
|
+
/**
|
|
3490
|
+
* Append a message to a conversation. Auto-generates title from first user message.
|
|
3491
|
+
*
|
|
3492
|
+
* One append at a time per conversation: two sends that overlap (a suggestion and a
|
|
3493
|
+
* quick type, coalesced into one new conversation) both suspend before reading the
|
|
3494
|
+
* record — for the messages, then for the title provider — so both used to read a
|
|
3495
|
+
* conversation with no user message in it yet, and the second one's text became the
|
|
3496
|
+
* title.
|
|
3497
|
+
*/
|
|
3239
3498
|
async addMessage(convId, msg) {
|
|
3240
|
-
const
|
|
3499
|
+
const previous = this._appending.get(convId);
|
|
3500
|
+
const run = previous ? previous.then(() => this._append(convId, msg)) : this._append(convId, msg);
|
|
3501
|
+
const tail = run.catch(() => {
|
|
3502
|
+
});
|
|
3503
|
+
this._appending.set(convId, tail);
|
|
3504
|
+
void tail.then(() => {
|
|
3505
|
+
if (this._appending.get(convId) === tail) this._appending.delete(convId);
|
|
3506
|
+
});
|
|
3507
|
+
return run;
|
|
3508
|
+
}
|
|
3509
|
+
async _append(convId, msg) {
|
|
3510
|
+
const conv = await this._full(convId);
|
|
3241
3511
|
if (!conv) return;
|
|
3242
3512
|
const isFirstUserMsg = msg.role === "user" && conv.messages.every((m) => m.role !== "user");
|
|
3513
|
+
const title = isFirstUserMsg ? await this._title(msg) : null;
|
|
3514
|
+
const fresh = this._find(convId);
|
|
3515
|
+
if (!fresh) return;
|
|
3516
|
+
const present = fresh.messages.some((m) => m.id === msg.id);
|
|
3243
3517
|
const updated = {
|
|
3244
|
-
...
|
|
3245
|
-
messages: [...
|
|
3518
|
+
...fresh,
|
|
3519
|
+
messages: present ? fresh.messages : [...fresh.messages, msg],
|
|
3246
3520
|
updatedAt: Date.now(),
|
|
3247
|
-
title:
|
|
3521
|
+
title: title ?? fresh.title
|
|
3248
3522
|
};
|
|
3523
|
+
if (isFirstUserMsg) updated.autoTitle = true;
|
|
3249
3524
|
this._replace(updated);
|
|
3250
3525
|
await this._adapter.save(updated);
|
|
3251
3526
|
this._notify();
|
|
@@ -3258,20 +3533,44 @@ class AparteConversationManager {
|
|
|
3258
3533
|
async updateMessages(convId, messages, tree) {
|
|
3259
3534
|
const conv = this._find(convId);
|
|
3260
3535
|
if (!conv) return;
|
|
3536
|
+
if (!this._loaded.has(convId)) {
|
|
3537
|
+
console.warn(`[AparteConversationManager] updateMessages('${convId}') before its messages were loaded: ignored.`);
|
|
3538
|
+
return;
|
|
3539
|
+
}
|
|
3540
|
+
const firstUser = messages.find((m) => m.role === "user");
|
|
3261
3541
|
if (this._retention) {
|
|
3262
3542
|
({ messages, tree } = applyRetention(messages, tree, this._retention.maxMessages));
|
|
3263
3543
|
}
|
|
3544
|
+
const storedMessages = withoutDerivedSegments(messages);
|
|
3545
|
+
const storedTree = tree === void 0 ? void 0 : withoutDerivedTree(tree);
|
|
3264
3546
|
let contentChanged = true;
|
|
3547
|
+
let treeChanged = tree !== void 0;
|
|
3265
3548
|
try {
|
|
3266
|
-
contentChanged = JSON.stringify(
|
|
3549
|
+
contentChanged = JSON.stringify(storedMessages) !== JSON.stringify(withoutDerivedSegments(conv.messages));
|
|
3550
|
+
if (storedTree !== void 0) treeChanged = JSON.stringify(storedTree) !== JSON.stringify(conv.tree);
|
|
3267
3551
|
} catch {
|
|
3268
3552
|
}
|
|
3553
|
+
let title;
|
|
3554
|
+
if (conv.autoTitle) {
|
|
3555
|
+
const before = conv.messages.find((m) => m.role === "user");
|
|
3556
|
+
if (firstUser && (firstUser.content ?? "") !== (before?.content ?? "")) {
|
|
3557
|
+
title = await this._title(firstUser);
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
if (!contentChanged && !treeChanged && title === void 0) return;
|
|
3561
|
+
const fresh = this._find(convId);
|
|
3562
|
+
if (!fresh) return;
|
|
3269
3563
|
const updated = {
|
|
3270
|
-
...
|
|
3271
|
-
|
|
3272
|
-
|
|
3564
|
+
...fresh,
|
|
3565
|
+
// What is stored is what the caller sent: segments a viewport DERIVED on
|
|
3566
|
+
// display are not the app's content. Stored, they would fill the record with
|
|
3567
|
+
// core-generated ids and freeze the parse as it stood the day it was saved —
|
|
3568
|
+
// a block grammar registered later would never apply to that message.
|
|
3569
|
+
messages: storedMessages,
|
|
3570
|
+
updatedAt: contentChanged ? Date.now() : fresh.updatedAt
|
|
3273
3571
|
};
|
|
3274
|
-
if (
|
|
3572
|
+
if (storedTree !== void 0) updated.tree = storedTree;
|
|
3573
|
+
if (title !== void 0) updated.title = title;
|
|
3275
3574
|
this._replace(updated);
|
|
3276
3575
|
await this._adapter.save(updated);
|
|
3277
3576
|
this._notify();
|
|
@@ -3279,35 +3578,41 @@ class AparteConversationManager {
|
|
|
3279
3578
|
/** Permanently delete a conversation. */
|
|
3280
3579
|
async delete(id) {
|
|
3281
3580
|
this._conversations = this._conversations.filter((c) => c.id !== id);
|
|
3581
|
+
this._loaded.delete(id);
|
|
3282
3582
|
if (this._activeId === id) this._activeId = null;
|
|
3283
3583
|
await this._adapter.delete(id);
|
|
3284
3584
|
this._notify();
|
|
3285
3585
|
}
|
|
3286
3586
|
/** Archive a conversation (soft-delete). */
|
|
3287
3587
|
async archive(id) {
|
|
3288
|
-
|
|
3289
|
-
if (!conv) return;
|
|
3290
|
-
const updated = { ...conv, archivedAt: Date.now() };
|
|
3291
|
-
this._replace(updated);
|
|
3292
|
-
if (this._adapter.archive) {
|
|
3293
|
-
await this._adapter.archive(id);
|
|
3294
|
-
} else {
|
|
3295
|
-
await this._adapter.save(updated);
|
|
3296
|
-
}
|
|
3588
|
+
if (!await this._meta(id, (conv) => ({ ...conv, archivedAt: Date.now() }), this._adapter.archive)) return;
|
|
3297
3589
|
if (this._activeId === id) this._activeId = null;
|
|
3298
3590
|
this._notify();
|
|
3299
3591
|
}
|
|
3592
|
+
/**
|
|
3593
|
+
* A metadata change, through the adapter's own hook when it has one (the row is
|
|
3594
|
+
* updated in memory, nothing else travels), else through `save()` — with the
|
|
3595
|
+
* messages fetched first, so a row the list came with (`messages: []`) is never
|
|
3596
|
+
* written back over the stored ones.
|
|
3597
|
+
*/
|
|
3598
|
+
async _meta(id, change, hook) {
|
|
3599
|
+
if (hook) {
|
|
3600
|
+
const conv2 = this._find(id);
|
|
3601
|
+
if (!conv2) return false;
|
|
3602
|
+
this._replace(change(conv2));
|
|
3603
|
+
await hook.call(this._adapter, id);
|
|
3604
|
+
return true;
|
|
3605
|
+
}
|
|
3606
|
+
const conv = await this._full(id);
|
|
3607
|
+
if (!conv) return false;
|
|
3608
|
+
const updated = change(conv);
|
|
3609
|
+
this._replace(updated);
|
|
3610
|
+
await this._adapter.save(updated);
|
|
3611
|
+
return true;
|
|
3612
|
+
}
|
|
3300
3613
|
/** Restore an archived conversation. */
|
|
3301
3614
|
async unarchive(id) {
|
|
3302
|
-
|
|
3303
|
-
if (!conv) return;
|
|
3304
|
-
const updated = { ...conv, archivedAt: void 0 };
|
|
3305
|
-
this._replace(updated);
|
|
3306
|
-
if (this._adapter.unarchive) {
|
|
3307
|
-
await this._adapter.unarchive(id);
|
|
3308
|
-
} else {
|
|
3309
|
-
await this._adapter.save(updated);
|
|
3310
|
-
}
|
|
3615
|
+
if (!await this._meta(id, (conv) => ({ ...conv, archivedAt: void 0 }), this._adapter.unarchive)) return;
|
|
3311
3616
|
this._notify();
|
|
3312
3617
|
}
|
|
3313
3618
|
/**
|
|
@@ -3317,28 +3622,12 @@ class AparteConversationManager {
|
|
|
3317
3622
|
* does not gets the whole record through `save()`.
|
|
3318
3623
|
*/
|
|
3319
3624
|
async pin(id) {
|
|
3320
|
-
|
|
3321
|
-
if (!conv) return;
|
|
3322
|
-
const updated = { ...conv, pinnedAt: Date.now() };
|
|
3323
|
-
this._replace(updated);
|
|
3324
|
-
if (this._adapter.pin) {
|
|
3325
|
-
await this._adapter.pin(id);
|
|
3326
|
-
} else {
|
|
3327
|
-
await this._adapter.save(updated);
|
|
3328
|
-
}
|
|
3625
|
+
if (!await this._meta(id, (conv) => ({ ...conv, pinnedAt: Date.now() }), this._adapter.pin)) return;
|
|
3329
3626
|
this._notify();
|
|
3330
3627
|
}
|
|
3331
3628
|
/** Unpin a conversation. Metadata only — see `pin()`. */
|
|
3332
3629
|
async unpin(id) {
|
|
3333
|
-
|
|
3334
|
-
if (!conv) return;
|
|
3335
|
-
const updated = { ...conv, pinnedAt: void 0 };
|
|
3336
|
-
this._replace(updated);
|
|
3337
|
-
if (this._adapter.unpin) {
|
|
3338
|
-
await this._adapter.unpin(id);
|
|
3339
|
-
} else {
|
|
3340
|
-
await this._adapter.save(updated);
|
|
3341
|
-
}
|
|
3630
|
+
if (!await this._meta(id, (conv) => ({ ...conv, pinnedAt: void 0 }), this._adapter.unpin)) return;
|
|
3342
3631
|
this._notify();
|
|
3343
3632
|
}
|
|
3344
3633
|
/** Update the title manually. The full input is preserved — UI surfaces
|
|
@@ -3346,12 +3635,17 @@ class AparteConversationManager {
|
|
|
3346
3635
|
* (`min-w-0` + `truncate`). Auto-titles produced internally by
|
|
3347
3636
|
* `_autoTitle()` remain capped at a sensible length on input. */
|
|
3348
3637
|
async updateTitle(id, title) {
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
this.
|
|
3638
|
+
if (!this._find(id)) {
|
|
3639
|
+
console.warn(`[AparteConversationManager] updateTitle('${id}'): unknown conversation.`);
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3642
|
+
const next = title.trim();
|
|
3643
|
+
const done = await this._meta(
|
|
3644
|
+
id,
|
|
3645
|
+
(conv) => ({ ...conv, title: next, autoTitle: false }),
|
|
3646
|
+
this._adapter.rename ? (convId) => this._adapter.rename(convId, next) : void 0
|
|
3647
|
+
);
|
|
3648
|
+
if (done) this._notify();
|
|
3355
3649
|
}
|
|
3356
3650
|
// ─── Observer ───────────────────────────────────────────────────────────
|
|
3357
3651
|
subscribe(listener) {
|
|
@@ -3378,8 +3672,9 @@ class AparteConversationManager {
|
|
|
3378
3672
|
});
|
|
3379
3673
|
}
|
|
3380
3674
|
/**
|
|
3381
|
-
* The title of a conversation, decided
|
|
3382
|
-
*
|
|
3675
|
+
* The title of a conversation, decided on its first user message — and again if
|
|
3676
|
+
* that message is edited while the title is still the manager's: the provider's
|
|
3677
|
+
* answer when one is registered and answers, else the default.
|
|
3383
3678
|
*/
|
|
3384
3679
|
async _title(msg) {
|
|
3385
3680
|
const fallback = this._autoTitle(msg);
|
|
@@ -3393,7 +3688,7 @@ class AparteConversationManager {
|
|
|
3393
3688
|
}
|
|
3394
3689
|
}
|
|
3395
3690
|
_autoTitle(msg) {
|
|
3396
|
-
return (msg.content ?? "").trim()
|
|
3691
|
+
return (msg.content ?? "").trim();
|
|
3397
3692
|
}
|
|
3398
3693
|
}
|
|
3399
3694
|
function filesToAttachments(files) {
|
|
@@ -3432,6 +3727,8 @@ class AparteConversationController {
|
|
|
3432
3727
|
_subscribedTo = null;
|
|
3433
3728
|
/** Between bind() and unbind(): the only time a manager subscription may be installed. */
|
|
3434
3729
|
_bound = false;
|
|
3730
|
+
/** The binding shows the wait this controller asked for — see `_showWait`. */
|
|
3731
|
+
_waitShown = false;
|
|
3435
3732
|
/**
|
|
3436
3733
|
* React to an external mutation of the manager (another component deleting or
|
|
3437
3734
|
* archiving the active conversation). Without this the binding would keep
|
|
@@ -3452,6 +3749,7 @@ class AparteConversationController {
|
|
|
3452
3749
|
}
|
|
3453
3750
|
this._activeId = null;
|
|
3454
3751
|
this._binding.clearMessages();
|
|
3752
|
+
this._showWait(false);
|
|
3455
3753
|
}
|
|
3456
3754
|
};
|
|
3457
3755
|
constructor(binding, options = {}) {
|
|
@@ -3558,7 +3856,7 @@ class AparteConversationController {
|
|
|
3558
3856
|
void this.setConversationId(evt.detail?.id ?? null);
|
|
3559
3857
|
};
|
|
3560
3858
|
if (typeof window !== "undefined") {
|
|
3561
|
-
window.addEventListener("aparte-select
|
|
3859
|
+
window.addEventListener("aparte-conversation-select", this._onSelectConversation);
|
|
3562
3860
|
}
|
|
3563
3861
|
this._bound = true;
|
|
3564
3862
|
this._manager();
|
|
@@ -3599,7 +3897,7 @@ class AparteConversationController {
|
|
|
3599
3897
|
this._onMessageAborted = null;
|
|
3600
3898
|
}
|
|
3601
3899
|
if (this._onSelectConversation && typeof window !== "undefined") {
|
|
3602
|
-
window.removeEventListener("aparte-select
|
|
3900
|
+
window.removeEventListener("aparte-conversation-select", this._onSelectConversation);
|
|
3603
3901
|
this._onSelectConversation = null;
|
|
3604
3902
|
}
|
|
3605
3903
|
if (this._unsubscribeManager) {
|
|
@@ -3645,6 +3943,7 @@ class AparteConversationController {
|
|
|
3645
3943
|
this._activeId = null;
|
|
3646
3944
|
manager?.clearActive();
|
|
3647
3945
|
if (isSwitch) this._binding.clearMessages();
|
|
3946
|
+
this._showWait(false);
|
|
3648
3947
|
return;
|
|
3649
3948
|
}
|
|
3650
3949
|
const conv = manager?.conversations.find((c) => c.id === id);
|
|
@@ -3660,29 +3959,68 @@ class AparteConversationController {
|
|
|
3660
3959
|
console.warn("[ConversationController] id still unknown after hydration — clearing. id:", id);
|
|
3661
3960
|
manager.clearActive();
|
|
3662
3961
|
this._binding.clearMessages();
|
|
3962
|
+
this._showWait(false);
|
|
3663
3963
|
}
|
|
3664
3964
|
});
|
|
3665
3965
|
this._stopHydrationRetry = stop;
|
|
3966
|
+
this._showWait(false);
|
|
3666
3967
|
return;
|
|
3667
3968
|
}
|
|
3668
3969
|
console.warn("[ConversationController] unknown id or no manager — clearing. manager:", !!manager, "conv:", !!conv);
|
|
3669
3970
|
this._activeId = null;
|
|
3670
3971
|
manager?.clearActive();
|
|
3671
3972
|
this._binding.clearMessages();
|
|
3973
|
+
this._showWait(false);
|
|
3672
3974
|
return;
|
|
3673
3975
|
}
|
|
3674
3976
|
this._activeId = id;
|
|
3675
3977
|
manager.select(id);
|
|
3978
|
+
let record = conv;
|
|
3979
|
+
const fetching = !manager.isLoaded(id);
|
|
3980
|
+
if (fetching) {
|
|
3981
|
+
this._showWait(true);
|
|
3982
|
+
this._binding.clearMessages();
|
|
3983
|
+
let loaded = false;
|
|
3984
|
+
try {
|
|
3985
|
+
loaded = await manager.ensureFull(id);
|
|
3986
|
+
} catch (err) {
|
|
3987
|
+
console.warn("[ConversationController] the conversation could not be loaded:", id, err);
|
|
3988
|
+
}
|
|
3989
|
+
if (this._activeId !== id) return;
|
|
3990
|
+
if (!loaded) {
|
|
3991
|
+
if (manager.conversations.some((c) => c.id === id)) {
|
|
3992
|
+
console.warn("[ConversationController] no messages came back for", id, "— leaving it.");
|
|
3993
|
+
}
|
|
3994
|
+
this._activeId = null;
|
|
3995
|
+
manager.clearActive();
|
|
3996
|
+
this._binding.clearMessages();
|
|
3997
|
+
this._showWait(false);
|
|
3998
|
+
return;
|
|
3999
|
+
}
|
|
4000
|
+
record = manager.conversations.find((c) => c.id === id) ?? conv;
|
|
4001
|
+
}
|
|
3676
4002
|
this._isLoadingConversation = true;
|
|
3677
4003
|
try {
|
|
3678
|
-
this._binding.setMessages([...
|
|
3679
|
-
if (
|
|
3680
|
-
this._binding.importTree(
|
|
4004
|
+
this._binding.setMessages([...record.messages ?? []]);
|
|
4005
|
+
if (record.tree && this._binding.importTree) {
|
|
4006
|
+
this._binding.importTree(record.tree);
|
|
3681
4007
|
}
|
|
3682
4008
|
} finally {
|
|
3683
4009
|
this._isLoadingConversation = false;
|
|
4010
|
+
this._showWait(false);
|
|
3684
4011
|
}
|
|
3685
4012
|
}
|
|
4013
|
+
/**
|
|
4014
|
+
* The binding's wait, owned here: whoever runs `setConversationId` last clears it
|
|
4015
|
+
* — a loaded conversation or a new chat that replaces a fetch, a delete of the
|
|
4016
|
+
* one being fetched — so a wait shown for one call never outlives the call that
|
|
4017
|
+
* superseded it.
|
|
4018
|
+
*/
|
|
4019
|
+
_showWait(on) {
|
|
4020
|
+
if (this._waitShown === on) return;
|
|
4021
|
+
this._waitShown = on;
|
|
4022
|
+
this._binding.setLoading?.(on);
|
|
4023
|
+
}
|
|
3686
4024
|
// ─────────────────────────────────────────────────────────────────────
|
|
3687
4025
|
// Internal
|
|
3688
4026
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -3700,6 +4038,7 @@ class AparteConversationController {
|
|
|
3700
4038
|
if (!this._activeId) return;
|
|
3701
4039
|
const manager = this._manager();
|
|
3702
4040
|
if (!manager) return;
|
|
4041
|
+
if (!manager.isLoaded(this._activeId)) return;
|
|
3703
4042
|
const msgs = this._binding.getMessages().map(
|
|
3704
4043
|
(m) => m.status === "streaming" || m.status === "pending" ? { ...m, status: "completed" } : m
|
|
3705
4044
|
);
|
|
@@ -3729,12 +4068,12 @@ class AparteConversationController {
|
|
|
3729
4068
|
}
|
|
3730
4069
|
if (!convId) {
|
|
3731
4070
|
if (!this._ensureInFlight) {
|
|
3732
|
-
const text = (userMsg.content ?? "").toString().trim()
|
|
4071
|
+
const text = (userMsg.content ?? "").toString().trim();
|
|
3733
4072
|
this._ensureInFlight = manager.createNew(text).then((conv) => conv.id);
|
|
3734
4073
|
}
|
|
3735
4074
|
convId = await this._ensureInFlight;
|
|
3736
4075
|
this._ensureInFlight = null;
|
|
3737
|
-
if (this._activeId !== wasActiveId) {
|
|
4076
|
+
if (this._activeId !== wasActiveId && this._activeId !== convId) {
|
|
3738
4077
|
void manager.delete(convId).catch(() => {
|
|
3739
4078
|
});
|
|
3740
4079
|
return;
|
|
@@ -3872,7 +4211,9 @@ class AparteChatHost {
|
|
|
3872
4211
|
setMessages: (msgs) => {
|
|
3873
4212
|
this._beginConversationSwap();
|
|
3874
4213
|
this._vp()?.resetSpacer?.();
|
|
3875
|
-
|
|
4214
|
+
const adopted = msgs.map(adoptMessageSegments);
|
|
4215
|
+
this.binding.setMessages(adopted);
|
|
4216
|
+
this._vp()?.setMessages?.(adopted);
|
|
3876
4217
|
},
|
|
3877
4218
|
appendMessage: (msg) => this.appendMessage(msg),
|
|
3878
4219
|
getMessages: () => this.binding.getMessages(),
|
|
@@ -3887,6 +4228,17 @@ class AparteChatHost {
|
|
|
3887
4228
|
},
|
|
3888
4229
|
importTree: (tree) => {
|
|
3889
4230
|
this._vp()?.importTree?.(tree);
|
|
4231
|
+
},
|
|
4232
|
+
// One writer for the wait. A wrapper that hears `onLoadingChange` draws the
|
|
4233
|
+
// wait itself and writes the viewport's attribute from its own
|
|
4234
|
+
// `loading || hostLoading` — so the host writing it too meant that, with the
|
|
4235
|
+
// consumer's own prop on, a `setLoading(false)` here removed the attribute
|
|
4236
|
+
// (and `aria-busy`) while the wrapper still drew the skeleton and still held
|
|
4237
|
+
// the composer shut. A binding that cannot hear it has no other way to know,
|
|
4238
|
+
// and there the attribute stays the host's to write.
|
|
4239
|
+
setLoading: (on) => {
|
|
4240
|
+
if (this.binding.onLoadingChange) this.binding.onLoadingChange(on);
|
|
4241
|
+
else this._vp()?.setLoading?.(on);
|
|
3890
4242
|
}
|
|
3891
4243
|
};
|
|
3892
4244
|
this._controller = new AparteConversationController(convBinding, {
|
|
@@ -3992,7 +4344,7 @@ class AparteChatHost {
|
|
|
3992
4344
|
// defaults rather than the page's.
|
|
3993
4345
|
resolveConfig(this.binding.host).getSegmentDefaults(segment.type)
|
|
3994
4346
|
) : segment;
|
|
3995
|
-
this.
|
|
4347
|
+
this._streamBubble(last?.id)?.addSegment?.(stamped);
|
|
3996
4348
|
if (!last) return;
|
|
3997
4349
|
const segments = [...last.segments || [], stamped];
|
|
3998
4350
|
const next = [...msgs.slice(0, -1), { ...last, segments }];
|
|
@@ -4007,7 +4359,7 @@ class AparteChatHost {
|
|
|
4007
4359
|
if (last && this._isOrphan(last.id)) return;
|
|
4008
4360
|
const current = last?.segments?.find((s) => s.id === segmentId);
|
|
4009
4361
|
const stamped = current ? stampSegmentOnUpdate(current, updates) : updates;
|
|
4010
|
-
this.
|
|
4362
|
+
this._streamBubble(last?.id)?.updateSegment?.(segmentId, stamped);
|
|
4011
4363
|
if (!last || !last.segments) return;
|
|
4012
4364
|
const segments = last.segments.map(
|
|
4013
4365
|
(s) => s.id === segmentId ? mergeSegmentUpdate(s, stamped) : s
|
|
@@ -4019,7 +4371,7 @@ class AparteChatHost {
|
|
|
4019
4371
|
/** Remove a transient segment (a status row the host added itself). */
|
|
4020
4372
|
removeSegment(segmentId) {
|
|
4021
4373
|
this._flushStreamState();
|
|
4022
|
-
this.
|
|
4374
|
+
this._streamBubble(this.binding.getMessages().at(-1)?.id)?.removeSegment?.(segmentId);
|
|
4023
4375
|
const msgs = this.binding.getMessages();
|
|
4024
4376
|
const last = msgs[msgs.length - 1];
|
|
4025
4377
|
if (!last?.segments) return;
|
|
@@ -4331,6 +4683,27 @@ class AparteChatHost {
|
|
|
4331
4683
|
`aparte-chat-bubble[message-id="${cssEscape(id)}"]`
|
|
4332
4684
|
);
|
|
4333
4685
|
}
|
|
4686
|
+
/**
|
|
4687
|
+
* The bubble the streaming writes below belong to: the one carrying that message's
|
|
4688
|
+
* id, and NOTHING when it is not on the page yet.
|
|
4689
|
+
*
|
|
4690
|
+
* It used to be "the last bubble in the viewport", which is a different message
|
|
4691
|
+
* whenever the DOM is a render behind the list — which is every framework here.
|
|
4692
|
+
* `appendMessage` puts the new reply in React's state and React paints it on its own
|
|
4693
|
+
* schedule, so a first chunk arriving in between went into the PREVIOUS reply's
|
|
4694
|
+
* bubble; `syncBubbles` then wrote the same segment onto the real one, and the same
|
|
4695
|
+
* `data-segment-id` was on two bubbles.
|
|
4696
|
+
*
|
|
4697
|
+
* Skipping the paint is safe because it is only ever a paint: the message list is
|
|
4698
|
+
* written either way, and `syncBubbles` — called by the framework after each render,
|
|
4699
|
+
* always re-syncing the last message — puts it on screen as soon as the bubble
|
|
4700
|
+
* exists. Without an id there is nothing to resolve, so the last bubble stands: that
|
|
4701
|
+
* is a host writing into a transcript it has not appended to.
|
|
4702
|
+
*/
|
|
4703
|
+
_streamBubble(messageId) {
|
|
4704
|
+
if (messageId) return this._bubbleById(messageId);
|
|
4705
|
+
return this._lastBubble();
|
|
4706
|
+
}
|
|
4334
4707
|
_lastBubble() {
|
|
4335
4708
|
const vp = this.binding.viewport;
|
|
4336
4709
|
if (!vp) return null;
|
|
@@ -4357,9 +4730,15 @@ class AparteChatHost {
|
|
|
4357
4730
|
const d = e?.detail;
|
|
4358
4731
|
if (d?.messageId) this._setStreamingId(d.messageId);
|
|
4359
4732
|
};
|
|
4733
|
+
const endsThisTurn = (e) => {
|
|
4734
|
+
const id = e?.detail?.messageId;
|
|
4735
|
+
return !id || this._streamingId === null || id === this._streamingId;
|
|
4736
|
+
};
|
|
4360
4737
|
const onDone = (e) => {
|
|
4361
|
-
|
|
4362
|
-
|
|
4738
|
+
if (endsThisTurn(e)) {
|
|
4739
|
+
this.binding.onTypingChange?.(false);
|
|
4740
|
+
this._setStreamingId(null);
|
|
4741
|
+
}
|
|
4363
4742
|
const d = e?.detail;
|
|
4364
4743
|
if (!d?.messageId || !d.usage) return;
|
|
4365
4744
|
const next = this.binding.getMessages().map(
|
|
@@ -4369,7 +4748,8 @@ class AparteChatHost {
|
|
|
4369
4748
|
this.binding.onMessagesChange?.(next);
|
|
4370
4749
|
this._bubbleById(d.messageId)?.setUsage?.(d.usage);
|
|
4371
4750
|
};
|
|
4372
|
-
const onEnd = () => {
|
|
4751
|
+
const onEnd = (e) => {
|
|
4752
|
+
if (!endsThisTurn(e)) return;
|
|
4373
4753
|
this.binding.onTypingChange?.(false);
|
|
4374
4754
|
this._setStreamingId(null);
|
|
4375
4755
|
};
|
|
@@ -4589,6 +4969,15 @@ function readableToAsyncIterable(stream, signal) {
|
|
|
4589
4969
|
}
|
|
4590
4970
|
};
|
|
4591
4971
|
}
|
|
4972
|
+
function eventOrigin(e) {
|
|
4973
|
+
return e.composedPath?.()[0] ?? e.target;
|
|
4974
|
+
}
|
|
4975
|
+
function rootOf(node) {
|
|
4976
|
+
return node.getRootNode();
|
|
4977
|
+
}
|
|
4978
|
+
function activeElementIn(node) {
|
|
4979
|
+
return node.getRootNode().activeElement ?? document.activeElement;
|
|
4980
|
+
}
|
|
4592
4981
|
let steppedPanelSeq = 0;
|
|
4593
4982
|
let _relabels = null;
|
|
4594
4983
|
function onRelabel(fn) {
|
|
@@ -5110,11 +5499,14 @@ function requestUserInput(request) {
|
|
|
5110
5499
|
return resolveConfig(request.target ?? null).requestUserInput(request);
|
|
5111
5500
|
}
|
|
5112
5501
|
const COMPACTION_PREAMBLE = "The earlier part of this conversation was compacted. This is a summary of what came before; continue from it.";
|
|
5502
|
+
const warnedUninlinedFiles = /* @__PURE__ */ new Set();
|
|
5503
|
+
const CHAT_SELECTOR = "aparte-chat, aparte-chat-viewport, [data-aparte-chat]";
|
|
5113
5504
|
class AparteClient {
|
|
5114
5505
|
_boundHandler = null;
|
|
5115
5506
|
_boundAbortHandler = null;
|
|
5116
5507
|
_boundRetryHandler = null;
|
|
5117
5508
|
_boundEditHandler = null;
|
|
5509
|
+
_releaseKeyResolver = null;
|
|
5118
5510
|
_isAborted = false;
|
|
5119
5511
|
/**
|
|
5120
5512
|
* Does this window event belong to this client?
|
|
@@ -5135,7 +5527,7 @@ class AparteClient {
|
|
|
5135
5527
|
const scope = this.options.scopeToTargetId;
|
|
5136
5528
|
if (scope) return detail?.targetId === scope;
|
|
5137
5529
|
if (this._config === aparteGlobalConfig) return true;
|
|
5138
|
-
const target = this._resolveTarget(detail?.targetId);
|
|
5530
|
+
const target = this._resolveTarget(detail?.targetId, e);
|
|
5139
5531
|
if (!target) return true;
|
|
5140
5532
|
const owner = resolveConfig(target);
|
|
5141
5533
|
return owner === this._config || owner === aparteGlobalConfig;
|
|
@@ -5151,6 +5543,7 @@ class AparteClient {
|
|
|
5151
5543
|
...options
|
|
5152
5544
|
};
|
|
5153
5545
|
this._config = options.config ?? aparteGlobalConfig;
|
|
5546
|
+
this._registerKeyResolver();
|
|
5154
5547
|
if (this.options.autoRegister) {
|
|
5155
5548
|
registerDefaultRenderers(this._config);
|
|
5156
5549
|
} else {
|
|
@@ -5176,6 +5569,20 @@ class AparteClient {
|
|
|
5176
5569
|
_beginUserTurn() {
|
|
5177
5570
|
this._isAborted = false;
|
|
5178
5571
|
}
|
|
5572
|
+
/**
|
|
5573
|
+
* Put this client's `keyResolver` on its config, and hold the teardown so
|
|
5574
|
+
* `stop()` can take it back off.
|
|
5575
|
+
*
|
|
5576
|
+
* The registration used to be fire-and-forget, so it outlived the client that
|
|
5577
|
+
* made it: a stopped client kept signing model-list requests, and a remount —
|
|
5578
|
+
* the way the wrappers document swapping options — added a source behind the
|
|
5579
|
+
* one it meant to replace instead of replacing it. Idempotent, so the usual
|
|
5580
|
+
* `new AparteClient({…}).start()` registers once.
|
|
5581
|
+
*/
|
|
5582
|
+
_registerKeyResolver() {
|
|
5583
|
+
if (this._releaseKeyResolver || !this.options.keyResolver) return;
|
|
5584
|
+
this._releaseKeyResolver = this._config.registerKeyProvider(this.options.keyResolver);
|
|
5585
|
+
}
|
|
5179
5586
|
/**
|
|
5180
5587
|
* Sets up the event listeners.
|
|
5181
5588
|
* This is called once in the constructor.
|
|
@@ -5194,6 +5601,7 @@ class AparteClient {
|
|
|
5194
5601
|
* Start listening for aparte-send events on the window.
|
|
5195
5602
|
*/
|
|
5196
5603
|
start() {
|
|
5604
|
+
this._registerKeyResolver();
|
|
5197
5605
|
if (!this._boundHandler) {
|
|
5198
5606
|
this._setupListeners();
|
|
5199
5607
|
}
|
|
@@ -5229,6 +5637,8 @@ class AparteClient {
|
|
|
5229
5637
|
*/
|
|
5230
5638
|
stop() {
|
|
5231
5639
|
this.abort();
|
|
5640
|
+
this._releaseKeyResolver?.();
|
|
5641
|
+
this._releaseKeyResolver = null;
|
|
5232
5642
|
if (!this._boundHandler) return;
|
|
5233
5643
|
window.removeEventListener("aparte-send", this._boundHandler);
|
|
5234
5644
|
this._boundHandler = null;
|
|
@@ -5429,7 +5839,7 @@ class AparteClient {
|
|
|
5429
5839
|
const { messageId, targetId } = event.detail ?? {};
|
|
5430
5840
|
this._beginUserTurn();
|
|
5431
5841
|
if (!messageId) return;
|
|
5432
|
-
const targetElement = this._resolveTarget(targetId);
|
|
5842
|
+
const targetElement = this._resolveTarget(targetId, event);
|
|
5433
5843
|
if (!targetElement) {
|
|
5434
5844
|
console.warn("[AparteClient] aparte-retry — no target found");
|
|
5435
5845
|
return;
|
|
@@ -5490,7 +5900,7 @@ class AparteClient {
|
|
|
5490
5900
|
const { messageId, content: newContent, targetId } = event.detail ?? {};
|
|
5491
5901
|
this._beginUserTurn();
|
|
5492
5902
|
if (!messageId || newContent === void 0) return;
|
|
5493
|
-
const targetElement = this._resolveTarget(targetId);
|
|
5903
|
+
const targetElement = this._resolveTarget(targetId, event);
|
|
5494
5904
|
if (!targetElement) {
|
|
5495
5905
|
console.warn("[AparteClient] aparte-edit — no target found");
|
|
5496
5906
|
return;
|
|
@@ -5532,24 +5942,83 @@ class AparteClient {
|
|
|
5532
5942
|
authConfig
|
|
5533
5943
|
);
|
|
5534
5944
|
}
|
|
5945
|
+
/**
|
|
5946
|
+
* The chain the gesture really came up, nearest node first.
|
|
5947
|
+
*
|
|
5948
|
+
* Read from a `window` listener, `event.target` is the shadow HOST when the event
|
|
5949
|
+
* left a tree of the consumer's own, and `parentElement` stops at that tree's root —
|
|
5950
|
+
* so a walk from either starts, or ends, outside the chat the person typed into.
|
|
5951
|
+
* `composedPath()` is the one chain that crosses. The fallback is for an event
|
|
5952
|
+
* dispatched by hand, which has no path.
|
|
5953
|
+
*/
|
|
5954
|
+
_originChain(event) {
|
|
5955
|
+
const path = event?.composedPath?.() ?? [];
|
|
5956
|
+
if (path.length > 0) return path;
|
|
5957
|
+
const chain = [];
|
|
5958
|
+
let node = event ? eventOrigin(event) : null;
|
|
5959
|
+
while (node) {
|
|
5960
|
+
chain.push(node);
|
|
5961
|
+
node = node.parentElement;
|
|
5962
|
+
}
|
|
5963
|
+
return chain;
|
|
5964
|
+
}
|
|
5965
|
+
/**
|
|
5966
|
+
* The trees a lookup must search, the one the gesture came from FIRST.
|
|
5967
|
+
*
|
|
5968
|
+
* `document.getElementById` and `document.querySelectorAll` cannot see into a shadow
|
|
5969
|
+
* root, so a chat mounted in one — the arrangement the theming guide endorses — was
|
|
5970
|
+
* invisible to both: alone, the send was dropped with a warning; on a page that also
|
|
5971
|
+
* holds a chat in the light DOM, the scan answered with the WRONG chat, so the
|
|
5972
|
+
* person's message AND the model's reply landed in a transcript they never typed
|
|
5973
|
+
* into. Nearest tree first is what keeps it from swapping the other way round.
|
|
5974
|
+
*/
|
|
5975
|
+
_rootsFor(event) {
|
|
5976
|
+
const roots = [];
|
|
5977
|
+
for (const node of this._originChain(event)) {
|
|
5978
|
+
if (typeof node.getRootNode !== "function") continue;
|
|
5979
|
+
const root = rootOf(node);
|
|
5980
|
+
if (!roots.includes(root)) roots.push(root);
|
|
5981
|
+
}
|
|
5982
|
+
if (!roots.includes(document)) roots.push(document);
|
|
5983
|
+
return roots;
|
|
5984
|
+
}
|
|
5985
|
+
/** The element an id names, looked up in the gesture's own tree before the document. */
|
|
5986
|
+
_byId(targetId, event) {
|
|
5987
|
+
for (const root of this._rootsFor(event)) {
|
|
5988
|
+
const el2 = root.getElementById?.(targetId);
|
|
5989
|
+
if (el2) return el2;
|
|
5990
|
+
}
|
|
5991
|
+
return null;
|
|
5992
|
+
}
|
|
5993
|
+
/** The first chat that can render, own tree first — the path when nothing names one. */
|
|
5994
|
+
_scanForTarget(event) {
|
|
5995
|
+
for (const root of this._rootsFor(event)) {
|
|
5996
|
+
for (const candidate of root.querySelectorAll(CHAT_SELECTOR)) {
|
|
5997
|
+
const target = this._asRenderTarget(candidate);
|
|
5998
|
+
if (target) return target;
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
return null;
|
|
6002
|
+
}
|
|
5535
6003
|
/**
|
|
5536
6004
|
* Resolve a target element by id (from event detail.targetId) or via targetResolver / DOM scan.
|
|
5537
6005
|
*/
|
|
5538
|
-
_resolveTarget(targetId) {
|
|
6006
|
+
_resolveTarget(targetId, event) {
|
|
5539
6007
|
if (targetId) {
|
|
5540
|
-
const el2 =
|
|
6008
|
+
const el2 = this._byId(targetId, event);
|
|
5541
6009
|
if (el2) return this._asRenderTarget(el2) ?? el2;
|
|
5542
6010
|
}
|
|
5543
6011
|
if (this.options.targetResolver) {
|
|
5544
6012
|
const el2 = this.options.targetResolver();
|
|
5545
6013
|
if (el2) return this._asRenderTarget(el2) ?? el2;
|
|
5546
6014
|
}
|
|
5547
|
-
const
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
6015
|
+
const scanned = this._scanForTarget(event);
|
|
6016
|
+
if (scanned) return scanned;
|
|
6017
|
+
for (const root of this._rootsFor(event)) {
|
|
6018
|
+
const first = root.querySelector(CHAT_SELECTOR);
|
|
6019
|
+
if (first) return first;
|
|
5551
6020
|
}
|
|
5552
|
-
return
|
|
6021
|
+
return null;
|
|
5553
6022
|
}
|
|
5554
6023
|
/**
|
|
5555
6024
|
* Resolve an element to a usable render target: itself when it exposes
|
|
@@ -5640,7 +6109,7 @@ class AparteClient {
|
|
|
5640
6109
|
let targetElement = null;
|
|
5641
6110
|
const targetId = event.detail?.targetId;
|
|
5642
6111
|
if (targetId) {
|
|
5643
|
-
const byId =
|
|
6112
|
+
const byId = this._byId(targetId, event);
|
|
5644
6113
|
const resolved = this._asRenderTarget(byId);
|
|
5645
6114
|
if (resolved) {
|
|
5646
6115
|
targetElement = resolved;
|
|
@@ -5649,32 +6118,22 @@ class AparteClient {
|
|
|
5649
6118
|
}
|
|
5650
6119
|
}
|
|
5651
6120
|
if (!targetElement && this.options.targetResolver) {
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
}
|
|
5656
|
-
}
|
|
5657
|
-
if (!targetElement) {
|
|
5658
|
-
let walker = event.target;
|
|
5659
|
-
while (walker && typeof walker.appendMessage !== "function") {
|
|
5660
|
-
walker = walker.parentElement;
|
|
5661
|
-
}
|
|
5662
|
-
if (walker) {
|
|
5663
|
-
targetElement = walker;
|
|
5664
|
-
}
|
|
6121
|
+
targetElement = this._asRenderTarget(
|
|
6122
|
+
this.options.targetResolver()
|
|
6123
|
+
);
|
|
5665
6124
|
}
|
|
5666
6125
|
if (!targetElement) {
|
|
5667
|
-
const
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
const resolved = this._asRenderTarget(candidate);
|
|
5672
|
-
if (resolved) {
|
|
5673
|
-
targetElement = resolved;
|
|
6126
|
+
for (const node of this._originChain(event)) {
|
|
6127
|
+
const walker = node;
|
|
6128
|
+
if (typeof walker?.appendMessage === "function") {
|
|
6129
|
+
targetElement = walker;
|
|
5674
6130
|
break;
|
|
5675
6131
|
}
|
|
5676
6132
|
}
|
|
5677
6133
|
}
|
|
6134
|
+
if (!targetElement) {
|
|
6135
|
+
targetElement = this._scanForTarget(event);
|
|
6136
|
+
}
|
|
5678
6137
|
if (!targetElement) {
|
|
5679
6138
|
console.warn("[AparteClient] ⚠️ No target element found with appendMessage support. Provide a targetResolver in AparteClientOptions or ensure the composer has a `target` attribute.");
|
|
5680
6139
|
return;
|
|
@@ -5841,7 +6300,8 @@ ${text}` : text;
|
|
|
5841
6300
|
*
|
|
5842
6301
|
* `thinking` and `tool_call` stay out on purpose — the first is the model's own
|
|
5843
6302
|
* scratchpad, and most APIs neither want it back nor bill for it kindly; the
|
|
5844
|
-
* second
|
|
6303
|
+
* second was already carried, within the turn, as the assistant's calls and the
|
|
6304
|
+
* `tool` messages answering them.
|
|
5845
6305
|
*/
|
|
5846
6306
|
_segmentsToText(segments) {
|
|
5847
6307
|
if (!segments?.length) return "";
|
|
@@ -5885,7 +6345,8 @@ ${fence}`);
|
|
|
5885
6345
|
* - Images → AparteImagePart (base64 data URL)
|
|
5886
6346
|
* - Text files (txt, md, json, csv, xml, html, css, js, ts, …) → AparteTextPart
|
|
5887
6347
|
* injected as a fenced block so all models (including local) can read them.
|
|
5888
|
-
* - Other binary files →
|
|
6348
|
+
* - Other binary files → no content part, and a one-time `console.warn` naming the
|
|
6349
|
+
* file (see the branch below for why it is not silent).
|
|
5889
6350
|
*/
|
|
5890
6351
|
async _filesToContentParts(files) {
|
|
5891
6352
|
const TEXT_TYPES = /^(text\/|application\/(json|xml|javascript|typescript|x-yaml|yaml|toml|csv|markdown))/i;
|
|
@@ -5918,6 +6379,12 @@ ${content}
|
|
|
5918
6379
|
reader.readAsText(file);
|
|
5919
6380
|
});
|
|
5920
6381
|
}
|
|
6382
|
+
if (!warnedUninlinedFiles.has(file.name)) {
|
|
6383
|
+
warnedUninlinedFiles.add(file.name);
|
|
6384
|
+
console.warn(
|
|
6385
|
+
`[aparte] ${file.name} (${file.type || "unknown type"}) is attached to the message but is not sent to the model: aparté inlines images and text files only. Handle it in your app (RAG, an upload) — the chip stays visible either way.`
|
|
6386
|
+
);
|
|
6387
|
+
}
|
|
5921
6388
|
return Promise.resolve(null);
|
|
5922
6389
|
})
|
|
5923
6390
|
);
|
|
@@ -6107,6 +6574,20 @@ class AparteMessageRepository {
|
|
|
6107
6574
|
getMessageById(id) {
|
|
6108
6575
|
return this._nodes.get(id)?.current;
|
|
6109
6576
|
}
|
|
6577
|
+
/**
|
|
6578
|
+
* Every message the tree holds — every branch, not just the active path.
|
|
6579
|
+
*
|
|
6580
|
+
* `getMessages()` walks head → root, so a question about the whole conversation
|
|
6581
|
+
* ("is anything still streaming?") answered from it could not see a turn that a
|
|
6582
|
+
* retry had just taken off the path — and the viewport wrote that turn's remaining
|
|
6583
|
+
* segments into the bubble which replaced it. No order is promised beyond
|
|
6584
|
+
* insertion: a caller that needs the topology reads `export()`.
|
|
6585
|
+
*/
|
|
6586
|
+
getAllMessages() {
|
|
6587
|
+
const all = [];
|
|
6588
|
+
for (const node of this._nodes.values()) all.push(node.current);
|
|
6589
|
+
return all;
|
|
6590
|
+
}
|
|
6110
6591
|
/**
|
|
6111
6592
|
* Returns the IDs of all siblings of `messageId`
|
|
6112
6593
|
* (i.e. all children of its parent, including itself).
|
|
@@ -6165,15 +6646,19 @@ class AparteMessageRepository {
|
|
|
6165
6646
|
this._head = findHead(node);
|
|
6166
6647
|
}
|
|
6167
6648
|
/**
|
|
6168
|
-
* Remove all descendants of `messageId` (inclusive) and
|
|
6169
|
-
* the parent of `messageId`. Used by edit/truncate flows.
|
|
6649
|
+
* Remove all descendants of `messageId` (inclusive) and, when the head was among
|
|
6650
|
+
* them, set `head` to the parent of `messageId`. Used by edit/truncate flows.
|
|
6651
|
+
*
|
|
6652
|
+
* A head in a branch this call did not touch stays where it is: it used to be
|
|
6653
|
+
* relocated regardless, which took the active path out of the version the reader was
|
|
6654
|
+
* on for a truncate somewhere else entirely.
|
|
6170
6655
|
*
|
|
6171
6656
|
* @param messageId The first message to remove
|
|
6172
|
-
* @param newParentId Optional explicit parent to set head to
|
|
6173
6657
|
*/
|
|
6174
6658
|
resetHead(messageId) {
|
|
6175
6659
|
const node = this._nodes.get(messageId);
|
|
6176
6660
|
if (!node) return;
|
|
6661
|
+
const headBefore = this._head;
|
|
6177
6662
|
this._deleteDescendants(node);
|
|
6178
6663
|
const parentOrRoot = node.prev ?? this._root;
|
|
6179
6664
|
parentOrRoot.children = parentOrRoot.children.filter((id) => id !== messageId);
|
|
@@ -6181,7 +6666,8 @@ class AparteMessageRepository {
|
|
|
6181
6666
|
parentOrRoot.next = null;
|
|
6182
6667
|
}
|
|
6183
6668
|
this._nodes.delete(messageId);
|
|
6184
|
-
|
|
6669
|
+
const headSurvived = headBefore !== null && headBefore !== node && this._nodes.get(headBefore.current.id) === headBefore;
|
|
6670
|
+
if (!headSurvived) this._head = node.prev;
|
|
6185
6671
|
}
|
|
6186
6672
|
/**
|
|
6187
6673
|
* Update a message's data without changing its position in the tree.
|
|
@@ -6336,13 +6822,13 @@ const APARTE_DEFAULT_UI_EVENTS = [
|
|
|
6336
6822
|
"aparte-reset-done",
|
|
6337
6823
|
"aparte-path-changed",
|
|
6338
6824
|
// <aparte-conversation-list>
|
|
6339
|
-
"aparte-select
|
|
6340
|
-
"aparte-delete
|
|
6341
|
-
"aparte-archive
|
|
6342
|
-
"aparte-unarchive
|
|
6343
|
-
"aparte-rename
|
|
6344
|
-
"aparte-pin
|
|
6345
|
-
"aparte-unpin
|
|
6825
|
+
"aparte-conversation-select",
|
|
6826
|
+
"aparte-conversation-delete",
|
|
6827
|
+
"aparte-conversation-archive",
|
|
6828
|
+
"aparte-conversation-unarchive",
|
|
6829
|
+
"aparte-conversation-rename",
|
|
6830
|
+
"aparte-conversation-pin",
|
|
6831
|
+
"aparte-conversation-unpin",
|
|
6346
6832
|
// the select primitives
|
|
6347
6833
|
"aparte-select-change",
|
|
6348
6834
|
"aparte-select-open",
|
|
@@ -6470,15 +6956,20 @@ export {
|
|
|
6470
6956
|
subscribeConfigChange as a4,
|
|
6471
6957
|
unregisterSegmentRenderer as a5,
|
|
6472
6958
|
uuid as a6,
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6959
|
+
eventOrigin as a7,
|
|
6960
|
+
activeElementIn as a8,
|
|
6961
|
+
mergeSegmentUpdate as a9,
|
|
6962
|
+
writeStreamedMarkdown as aa,
|
|
6963
|
+
stampSegmentActivity as ab,
|
|
6964
|
+
stampSegmentOnInsert as ac,
|
|
6965
|
+
stampSegmentOnUpdate as ad,
|
|
6966
|
+
renumberSegments as ae,
|
|
6967
|
+
openSegmentIds as af,
|
|
6968
|
+
isTerminalStatus as ag,
|
|
6969
|
+
adoptMessageSegments as ah,
|
|
6970
|
+
APARTE_DERIVED_SEGMENTS as ai,
|
|
6971
|
+
chatBoundaryOf as aj,
|
|
6972
|
+
rootOf as ak,
|
|
6482
6973
|
APARTE_DEFAULT_BUBBLE_ACTIONS as b,
|
|
6483
6974
|
APARTE_DEFAULT_HOST_HANDLERS as c,
|
|
6484
6975
|
APARTE_DEFAULT_ICON_FALLBACKS as d,
|
|
@@ -6505,4 +6996,4 @@ export {
|
|
|
6505
6996
|
collectRendererStyles as y,
|
|
6506
6997
|
contentToText as z
|
|
6507
6998
|
};
|
|
6508
|
-
//# sourceMappingURL=dialog-triggers-
|
|
6999
|
+
//# sourceMappingURL=dialog-triggers-CJWw0P3l.js.map
|