@aparte/core 0.16.10 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- 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 +33 -0
- 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 +10 -1
- 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 +82 -13
- 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 +6841 -6085
- package/dist/{dialog-triggers-cYGTlfZf.js → dialog-triggers-Bb-3EkT4.js} +591 -161
- package/dist/dialog-triggers-Bb-3EkT4.js.map +1 -0
- package/dist/generated/element-attributes.d.ts +10 -4
- 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 +550 -98
- 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/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 +2 -2
- 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/utils/event-target.d.ts +31 -0
- package/dist/utils/event-target.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,7 +1314,7 @@ 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();
|
|
@@ -1323,9 +1330,10 @@ class AparteConfig {
|
|
|
1323
1330
|
// Provider Setters (Dependency Injection)
|
|
1324
1331
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1325
1332
|
/**
|
|
1326
|
-
* Register a custom action button. `zones` places it in the
|
|
1327
|
-
*
|
|
1328
|
-
*
|
|
1333
|
+
* Register a custom action button. `zones` places it in the message (bubble)
|
|
1334
|
+
* toolbar. Re-registering the same id overwrites it. Notifies mounted elements
|
|
1335
|
+
* so they re-render. A button in the composer is an `<aparte-composer-action>`
|
|
1336
|
+
* element you write in your markup, not a registry entry.
|
|
1329
1337
|
*/
|
|
1330
1338
|
registerAction(action) {
|
|
1331
1339
|
const existing = this._actions.findIndex((a) => a.id === action.id);
|
|
@@ -1347,17 +1355,6 @@ class AparteConfig {
|
|
|
1347
1355
|
this._actions = this._actions.filter((a) => a.id !== id);
|
|
1348
1356
|
if (this._actions.length !== before) this._notify();
|
|
1349
1357
|
}
|
|
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
1358
|
/**
|
|
1362
1359
|
* Configure which action buttons appear in message bubbles.
|
|
1363
1360
|
* Unset keys keep their defaults — see {@link APARTE_DEFAULT_BUBBLE_ACTIONS}: `copy`
|
|
@@ -1418,8 +1415,11 @@ class AparteConfig {
|
|
|
1418
1415
|
* Set an incremental (streaming) Markdown renderer provider. Optional —
|
|
1419
1416
|
* when set, the chat bubble uses it to render the assistant message
|
|
1420
1417
|
* token-by-token DURING streaming (incremental parse + DOM append, O(n)),
|
|
1421
|
-
* instead of re-parsing the whole string on every token.
|
|
1422
|
-
* `setMarkdownProvider`
|
|
1418
|
+
* instead of re-parsing the whole string on every token. When the turn ends,
|
|
1419
|
+
* a registered one-shot `setMarkdownProvider` re-renders the finished message
|
|
1420
|
+
* at full fidelity; with no one-shot provider registered, what the streaming
|
|
1421
|
+
* renderer drew is kept (sanitised), rather than replaced by the built-in
|
|
1422
|
+
* escape-and-line-break fallback.
|
|
1423
1423
|
*/
|
|
1424
1424
|
setStreamingMarkdownProvider(fn) {
|
|
1425
1425
|
this._streamingMarkdownProvider = fn;
|
|
@@ -1676,11 +1676,57 @@ class AparteConfig {
|
|
|
1676
1676
|
this._keyProvider = provider;
|
|
1677
1677
|
}
|
|
1678
1678
|
/**
|
|
1679
|
-
*
|
|
1679
|
+
* Add a key source, without replacing the one `setKeyProvider` holds. Returns a
|
|
1680
|
+
* teardown.
|
|
1681
|
+
*
|
|
1682
|
+
* This is how `AparteClient({ keyResolver })` — the channel the docs teach as
|
|
1683
|
+
* primary — reaches everything that needs a key rather than the chat alone. It
|
|
1684
|
+
* used to reach the chat ONLY, so the model selector, whose one data path is
|
|
1685
|
+
* `refreshProviderModels`, showed an empty list on every cloud provider while
|
|
1686
|
+
* the chat worked: `fetchModels` returns `[]` without a key, so there was
|
|
1687
|
+
* nothing to see and nothing said.
|
|
1688
|
+
*
|
|
1689
|
+
* The direction matters (decision #9): the client registers a source ON the
|
|
1690
|
+
* config, so the capability is never hostage to the client — a page that only
|
|
1691
|
+
* ever calls `setKeyProvider` is unchanged, and a page that constructs no
|
|
1692
|
+
* client at all still lists models.
|
|
1693
|
+
*
|
|
1694
|
+
* Every call is its own source, even when the same function is handed in
|
|
1695
|
+
* twice: two panes sharing one module-level resolver are two mounts, and the
|
|
1696
|
+
* first one to unmount must not take the second one's key with it. A `Set`
|
|
1697
|
+
* keyed by the function itself made them one entry, which either teardown
|
|
1698
|
+
* removed.
|
|
1699
|
+
*/
|
|
1700
|
+
registerKeyProvider(provider) {
|
|
1701
|
+
const entry = (providerId) => provider(providerId);
|
|
1702
|
+
this._extraKeyProviders.add(entry);
|
|
1703
|
+
return () => {
|
|
1704
|
+
this._extraKeyProviders.delete(entry);
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* The credentials for a provider: a key, or the `{ apiKey, endpoint }` record
|
|
1709
|
+
* the transport and `fetchModels` both read.
|
|
1710
|
+
*
|
|
1711
|
+
* ONE resolution order, so the chat and the model list cannot disagree about
|
|
1712
|
+
* what an endpoint is: a registered resolver (`AparteClient`'s `keyResolver`)
|
|
1713
|
+
* first — which is the precedence the client has always documented — then
|
|
1714
|
+
* `setKeyProvider`.
|
|
1715
|
+
*
|
|
1716
|
+
* Registered sources answer NEWEST first. A page that mounts a second chat, or
|
|
1717
|
+
* remounts one to swap its options — the swap the wrappers document — leaves
|
|
1718
|
+
* more than one source here, and insertion order made the first resolver the
|
|
1719
|
+
* page ever registered answer for every later one, so the key could not be
|
|
1720
|
+
* changed. The snapshot is deliberate too: a source torn down while an earlier
|
|
1721
|
+
* one is being awaited must not shift the iteration underneath it.
|
|
1680
1722
|
*/
|
|
1681
1723
|
async getKey(providerId) {
|
|
1724
|
+
for (const provider of [...this._extraKeyProviders].reverse()) {
|
|
1725
|
+
const resolved = await provider(providerId);
|
|
1726
|
+
if (resolved) return resolved;
|
|
1727
|
+
}
|
|
1682
1728
|
if (this._keyProvider) {
|
|
1683
|
-
return await this._keyProvider(providerId);
|
|
1729
|
+
return await this._keyProvider(providerId) || void 0;
|
|
1684
1730
|
}
|
|
1685
1731
|
return void 0;
|
|
1686
1732
|
}
|
|
@@ -1693,8 +1739,8 @@ class AparteConfig {
|
|
|
1693
1739
|
const provider = this._aiProviders.get(providerId);
|
|
1694
1740
|
if (!provider || !provider.fetchModels) return [];
|
|
1695
1741
|
try {
|
|
1696
|
-
const
|
|
1697
|
-
const models = await provider.fetchModels(
|
|
1742
|
+
const auth = await this.getKey(providerId);
|
|
1743
|
+
const models = await provider.fetchModels(auth);
|
|
1698
1744
|
this._fetchedModels.set(providerId, models);
|
|
1699
1745
|
return models;
|
|
1700
1746
|
} catch (error) {
|
|
@@ -2064,6 +2110,13 @@ class AparteConfig {
|
|
|
2064
2110
|
* Controls what appears in the chat bubble when the AI calls this tool.
|
|
2065
2111
|
* Use this instead of the generic `tool_call` segment renderer for tool-specific UI.
|
|
2066
2112
|
*
|
|
2113
|
+
* A renderer that needs a popover (a `<aparte-select>`, a menu) cannot open one
|
|
2114
|
+
* inside the row: the bubble is `content-visibility: auto`, which makes it the
|
|
2115
|
+
* containing block for `position: fixed` descendants and clips a popover to its
|
|
2116
|
+
* own box. Mount it on `document.body` and position it from the trigger's rect
|
|
2117
|
+
* instead — see [Custom tool
|
|
2118
|
+
* renderer](https://apartejs.dev/guides/tools/#custom-tool-renderer).
|
|
2119
|
+
*
|
|
2067
2120
|
* @example
|
|
2068
2121
|
* // Hide the segment entirely (UI-only tool like ask_user)
|
|
2069
2122
|
* aparteGlobalConfig.registerToolRenderer('ask_user', { render: () => '' });
|
|
@@ -2257,17 +2310,32 @@ class AparteConfig {
|
|
|
2257
2310
|
* "installs itself — nothing to register", which is how that was found.
|
|
2258
2311
|
*/
|
|
2259
2312
|
requestUserInput(request) {
|
|
2260
|
-
const
|
|
2261
|
-
const
|
|
2313
|
+
const queue = this._elicitationQueueKey(request);
|
|
2314
|
+
const previous = this._elicitationQueue.get(queue);
|
|
2315
|
+
const settled = previous === void 0 ? this._present(request) : previous.then(
|
|
2262
2316
|
() => this._present(request),
|
|
2263
2317
|
() => this._present(request)
|
|
2264
2318
|
);
|
|
2265
2319
|
const mine = settled.then(NOOP, NOOP).then(() => {
|
|
2266
|
-
if (this._elicitationQueue === mine) this._elicitationQueue
|
|
2320
|
+
if (this._elicitationQueue.get(queue) === mine) this._elicitationQueue.delete(queue);
|
|
2267
2321
|
});
|
|
2268
|
-
this._elicitationQueue
|
|
2322
|
+
this._elicitationQueue.set(queue, mine);
|
|
2269
2323
|
return settled;
|
|
2270
2324
|
}
|
|
2325
|
+
/**
|
|
2326
|
+
* Which queue a request waits in: its own chat's.
|
|
2327
|
+
*
|
|
2328
|
+
* The wait exists so a second question cannot take the panel away from the one on
|
|
2329
|
+
* screen — a property of ONE panel, and `_present` routes by target, so a chat is
|
|
2330
|
+
* the right unit. It used to be one queue for the whole config: two chats on the
|
|
2331
|
+
* global config shared it, and an approval waiting for an answer in the first (which
|
|
2332
|
+
* is all an approval does) held the second's question behind it for ever, with
|
|
2333
|
+
* nothing on screen to answer. Requests that name no chat keep sharing the `null`
|
|
2334
|
+
* queue, because they all reach the same presenter — the top of the stack.
|
|
2335
|
+
*/
|
|
2336
|
+
_elicitationQueueKey(request) {
|
|
2337
|
+
return chatBoundaryOf(request.target);
|
|
2338
|
+
}
|
|
2271
2339
|
/** Hand ONE request to the presenter. Called only from the queue. */
|
|
2272
2340
|
_present(request) {
|
|
2273
2341
|
const presenter = this._presenterFor(request.target);
|
|
@@ -2306,7 +2374,7 @@ class AparteConfig {
|
|
|
2306
2374
|
this._keyProvider = void 0;
|
|
2307
2375
|
this._conversationManager = void 0;
|
|
2308
2376
|
this._elicitationPresenters = [];
|
|
2309
|
-
this._elicitationQueue
|
|
2377
|
+
this._elicitationQueue.clear();
|
|
2310
2378
|
this._locale = APARTE_DEFAULT_LOCALE;
|
|
2311
2379
|
this._actions = [];
|
|
2312
2380
|
this._sanitizer = defaultSanitizer;
|
|
@@ -2399,6 +2467,10 @@ function subscribeConfigChange(el2, handler) {
|
|
|
2399
2467
|
window.addEventListener(APARTE_CONFIG_CHANGE, listener);
|
|
2400
2468
|
return () => window.removeEventListener(APARTE_CONFIG_CHANGE, listener);
|
|
2401
2469
|
}
|
|
2470
|
+
const ONE_SHOT_PROBE = "<i>a</i>";
|
|
2471
|
+
function hasOneShotMarkdownProvider() {
|
|
2472
|
+
return contextConfig().renderMarkdown(ONE_SHOT_PROBE) !== escapeHtml(ONE_SHOT_PROBE);
|
|
2473
|
+
}
|
|
2402
2474
|
function writeStreamedMarkdown(host, contentEl, content, streaming) {
|
|
2403
2475
|
if (streaming) {
|
|
2404
2476
|
if (host._aparteSmd === void 0) {
|
|
@@ -2420,8 +2492,18 @@ function writeStreamedMarkdown(host, contentEl, content, streaming) {
|
|
|
2420
2492
|
return;
|
|
2421
2493
|
}
|
|
2422
2494
|
} else if (host._aparteSmd) {
|
|
2423
|
-
host._aparteSmd
|
|
2495
|
+
const smd = host._aparteSmd;
|
|
2496
|
+
const tail = content.slice(smd.written);
|
|
2497
|
+
if (tail) {
|
|
2498
|
+
smd.renderer.write(tail);
|
|
2499
|
+
smd.written = content.length;
|
|
2500
|
+
}
|
|
2501
|
+
smd.renderer.end();
|
|
2424
2502
|
host._aparteSmd = void 0;
|
|
2503
|
+
if (!hasOneShotMarkdownProvider()) {
|
|
2504
|
+
contentEl.innerHTML = contextConfig().sanitizeHtml(contentEl.innerHTML);
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2425
2507
|
}
|
|
2426
2508
|
contentEl.innerHTML = contextConfig().renderMarkdown(content);
|
|
2427
2509
|
}
|
|
@@ -3115,6 +3197,7 @@ function populateBubbleFromMessage(bubble, message, siblingInfo) {
|
|
|
3115
3197
|
}
|
|
3116
3198
|
}
|
|
3117
3199
|
const APARTE_CONVERSATION_SCHEMA_VERSION = 2;
|
|
3200
|
+
const APARTE_DERIVED_SEGMENTS = Symbol.for("aparte.derivedSegments");
|
|
3118
3201
|
function applyRetention(messages, tree, maxMessages) {
|
|
3119
3202
|
if (maxMessages <= 0 || messages.length <= maxMessages) return { messages, tree };
|
|
3120
3203
|
const trimmedFlat = messages.slice(messages.length - maxMessages);
|
|
@@ -3144,6 +3227,20 @@ function applyRetention(messages, tree, maxMessages) {
|
|
|
3144
3227
|
};
|
|
3145
3228
|
return { messages: trimmedFlat, tree: trimmedTree };
|
|
3146
3229
|
}
|
|
3230
|
+
function withoutDerived(message) {
|
|
3231
|
+
if (!message[APARTE_DERIVED_SEGMENTS]) return message;
|
|
3232
|
+
const { segments: _derived, ...rest } = message;
|
|
3233
|
+
return rest;
|
|
3234
|
+
}
|
|
3235
|
+
function withoutDerivedSegments(messages) {
|
|
3236
|
+
return messages.map(withoutDerived);
|
|
3237
|
+
}
|
|
3238
|
+
function withoutDerivedTree(tree) {
|
|
3239
|
+
return {
|
|
3240
|
+
...tree,
|
|
3241
|
+
messages: tree.messages.map((entry) => ({ ...entry, message: withoutDerived(entry.message) }))
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3147
3244
|
class AparteConversationManager {
|
|
3148
3245
|
_adapter;
|
|
3149
3246
|
_conversations = [];
|
|
@@ -3152,6 +3249,14 @@ class AparteConversationManager {
|
|
|
3152
3249
|
_initialized = false;
|
|
3153
3250
|
_retention = null;
|
|
3154
3251
|
_titleProvider = null;
|
|
3252
|
+
/** Ids whose messages are in memory. Every id, after a loadAll(); only fetched ones after loadMeta(). */
|
|
3253
|
+
_loaded = /* @__PURE__ */ new Set();
|
|
3254
|
+
/** One fetch per id at a time: a second ensureFull(id) while the first is in flight joins it. */
|
|
3255
|
+
_fetching = /* @__PURE__ */ new Map();
|
|
3256
|
+
/** One append per id at a time: the next addMessage(id) reads what the previous one wrote. */
|
|
3257
|
+
_appending = /* @__PURE__ */ new Map();
|
|
3258
|
+
/** The list on its way: a conversation created meanwhile waits for it, or the list would replace it. */
|
|
3259
|
+
_initPromise = null;
|
|
3155
3260
|
constructor(adapter, options) {
|
|
3156
3261
|
this._adapter = adapter;
|
|
3157
3262
|
this._retention = options?.retention ?? null;
|
|
@@ -3171,11 +3276,86 @@ class AparteConversationManager {
|
|
|
3171
3276
|
return this._titleProvider;
|
|
3172
3277
|
}
|
|
3173
3278
|
// ─── Initialisation ────────────────────────────────────────────────────
|
|
3174
|
-
/**
|
|
3279
|
+
/**
|
|
3280
|
+
* Load all conversations from the adapter. Call once at app startup.
|
|
3281
|
+
*
|
|
3282
|
+
* A later call re-reads the list from the store: rows it no longer lists are
|
|
3283
|
+
* dropped, and a conversation whose messages are already in memory keeps them —
|
|
3284
|
+
* so the one on screen goes on being persisted.
|
|
3285
|
+
*/
|
|
3175
3286
|
async init() {
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3287
|
+
const load = (async () => {
|
|
3288
|
+
const split = !!(this._adapter.loadMeta && this._adapter.loadFull);
|
|
3289
|
+
if (this._adapter.loadMeta && !this._adapter.loadFull) {
|
|
3290
|
+
console.warn("[AparteConversationManager] the adapter lists through loadMeta() but has no loadFull(): everything is loaded through loadAll() instead.");
|
|
3291
|
+
}
|
|
3292
|
+
const held = new Map(this._conversations.filter((c) => this._loaded.has(c.id)).map((c) => [c.id, c]));
|
|
3293
|
+
this._loaded.clear();
|
|
3294
|
+
const rows = split ? (await this._adapter.loadMeta()).map((m) => {
|
|
3295
|
+
const kept = held.get(m.id);
|
|
3296
|
+
return kept ? { ...m, messages: kept.messages, ...kept.tree ? { tree: kept.tree } : {} } : { ...m, messages: [] };
|
|
3297
|
+
}) : (await this._adapter.loadAll()).map((c) => ({ ...c, messages: c.messages ?? [] }));
|
|
3298
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3299
|
+
const unique = rows.filter((c) => !seen.has(c.id) && (seen.add(c.id), true));
|
|
3300
|
+
if (unique.length !== rows.length) {
|
|
3301
|
+
console.warn(`[AparteConversationManager] ${rows.length - unique.length} repeated id(s) in the list; the first row wins.`);
|
|
3302
|
+
}
|
|
3303
|
+
this._conversations = unique;
|
|
3304
|
+
for (const c of unique) if (!split || held.has(c.id)) this._loaded.add(c.id);
|
|
3305
|
+
this._initialized = true;
|
|
3306
|
+
this._notify();
|
|
3307
|
+
})();
|
|
3308
|
+
this._initPromise = load;
|
|
3309
|
+
try {
|
|
3310
|
+
await load;
|
|
3311
|
+
} finally {
|
|
3312
|
+
if (this._initPromise === load) this._initPromise = null;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
/** Whether a conversation's messages are in memory (always, without split storage). */
|
|
3316
|
+
isLoaded(id) {
|
|
3317
|
+
return this._loaded.has(id);
|
|
3318
|
+
}
|
|
3319
|
+
/**
|
|
3320
|
+
* Fetch a conversation's messages through the adapter's loadFull(), the first time
|
|
3321
|
+
* they are needed; later calls resolve at once. The row keeps its place and its meta;
|
|
3322
|
+
* the messages (and the tree) come from the full record, and a message written to
|
|
3323
|
+
* the row while the fetch ran goes after them. Resolves `false` when the store has no
|
|
3324
|
+
* such record (or answers with another id) — the row is then NOT loaded, and says so,
|
|
3325
|
+
* rather than counting as an empty conversation.
|
|
3326
|
+
*/
|
|
3327
|
+
async ensureFull(id) {
|
|
3328
|
+
if (this._loaded.has(id)) return true;
|
|
3329
|
+
if (!this._adapter.loadFull) return false;
|
|
3330
|
+
const inFlight = this._fetching.get(id);
|
|
3331
|
+
if (inFlight) return inFlight;
|
|
3332
|
+
const fetch2 = (async () => {
|
|
3333
|
+
const full = await this._adapter.loadFull(id);
|
|
3334
|
+
const row = this._find(id);
|
|
3335
|
+
if (!row || !full) return false;
|
|
3336
|
+
if (full.id !== id) {
|
|
3337
|
+
console.warn(`[AparteConversationManager] loadFull('${id}') answered with '${full.id}': ignored.`);
|
|
3338
|
+
return false;
|
|
3339
|
+
}
|
|
3340
|
+
this._replace({ ...row, ...full, id, messages: full.messages ?? [] });
|
|
3341
|
+
this._loaded.add(id);
|
|
3342
|
+
this._notify();
|
|
3343
|
+
return true;
|
|
3344
|
+
})().finally(() => this._fetching.delete(id));
|
|
3345
|
+
this._fetching.set(id, fetch2);
|
|
3346
|
+
return fetch2;
|
|
3347
|
+
}
|
|
3348
|
+
/**
|
|
3349
|
+
* The row with its messages in memory — fetched first when they are not, so a
|
|
3350
|
+
* metadata change never writes a row back without them. `null` when the id is
|
|
3351
|
+
* unknown or the store has no record to fetch.
|
|
3352
|
+
*/
|
|
3353
|
+
async _full(id) {
|
|
3354
|
+
if (!this._loaded.has(id) && this._adapter.loadFull) {
|
|
3355
|
+
const loaded = await this.ensureFull(id);
|
|
3356
|
+
if (!loaded) return null;
|
|
3357
|
+
}
|
|
3358
|
+
return this._find(id) ?? null;
|
|
3179
3359
|
}
|
|
3180
3360
|
/**
|
|
3181
3361
|
* Whether `init()` has completed at least once. Consumers use this to
|
|
@@ -3207,8 +3387,12 @@ class AparteConversationManager {
|
|
|
3207
3387
|
return this._conversations.filter((c) => !!c.archivedAt).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
3208
3388
|
}
|
|
3209
3389
|
// ─── Mutations ──────────────────────────────────────────────────────────
|
|
3210
|
-
/**
|
|
3211
|
-
|
|
3390
|
+
/**
|
|
3391
|
+
* Create a new empty conversation, persist it, and make it active. Untitled by
|
|
3392
|
+
* default: the list shows its locale's `newChat` word for an empty title, so the
|
|
3393
|
+
* store never carries an English string a French UI would then display.
|
|
3394
|
+
*/
|
|
3395
|
+
async createNew(title = "") {
|
|
3212
3396
|
const now = Date.now();
|
|
3213
3397
|
const conv = {
|
|
3214
3398
|
id: uuid(),
|
|
@@ -3218,9 +3402,16 @@ class AparteConversationManager {
|
|
|
3218
3402
|
messages: [],
|
|
3219
3403
|
schemaVersion: APARTE_CONVERSATION_SCHEMA_VERSION
|
|
3220
3404
|
};
|
|
3221
|
-
this._conversations = [conv, ...this._conversations];
|
|
3222
3405
|
this._activeId = conv.id;
|
|
3223
|
-
|
|
3406
|
+
this._loaded.add(conv.id);
|
|
3407
|
+
if (this._initPromise) await this._initPromise.catch(() => {
|
|
3408
|
+
});
|
|
3409
|
+
this._conversations = [conv, ...this._conversations];
|
|
3410
|
+
try {
|
|
3411
|
+
await this._adapter.save(conv);
|
|
3412
|
+
} catch (err) {
|
|
3413
|
+
console.warn("[AparteConversationManager] save() failed for a new conversation; it stays in memory and the next write retries.", err);
|
|
3414
|
+
}
|
|
3224
3415
|
this._notify();
|
|
3225
3416
|
return conv;
|
|
3226
3417
|
}
|
|
@@ -3235,17 +3426,41 @@ class AparteConversationManager {
|
|
|
3235
3426
|
this._activeId = null;
|
|
3236
3427
|
this._notify();
|
|
3237
3428
|
}
|
|
3238
|
-
/**
|
|
3429
|
+
/**
|
|
3430
|
+
* Append a message to a conversation. Auto-generates title from first user message.
|
|
3431
|
+
*
|
|
3432
|
+
* One append at a time per conversation: two sends that overlap (a suggestion and a
|
|
3433
|
+
* quick type, coalesced into one new conversation) both suspend before reading the
|
|
3434
|
+
* record — for the messages, then for the title provider — so both used to read a
|
|
3435
|
+
* conversation with no user message in it yet, and the second one's text became the
|
|
3436
|
+
* title.
|
|
3437
|
+
*/
|
|
3239
3438
|
async addMessage(convId, msg) {
|
|
3240
|
-
const
|
|
3439
|
+
const previous = this._appending.get(convId);
|
|
3440
|
+
const run = previous ? previous.then(() => this._append(convId, msg)) : this._append(convId, msg);
|
|
3441
|
+
const tail = run.catch(() => {
|
|
3442
|
+
});
|
|
3443
|
+
this._appending.set(convId, tail);
|
|
3444
|
+
void tail.then(() => {
|
|
3445
|
+
if (this._appending.get(convId) === tail) this._appending.delete(convId);
|
|
3446
|
+
});
|
|
3447
|
+
return run;
|
|
3448
|
+
}
|
|
3449
|
+
async _append(convId, msg) {
|
|
3450
|
+
const conv = await this._full(convId);
|
|
3241
3451
|
if (!conv) return;
|
|
3242
3452
|
const isFirstUserMsg = msg.role === "user" && conv.messages.every((m) => m.role !== "user");
|
|
3453
|
+
const title = isFirstUserMsg ? await this._title(msg) : null;
|
|
3454
|
+
const fresh = this._find(convId);
|
|
3455
|
+
if (!fresh) return;
|
|
3456
|
+
const present = fresh.messages.some((m) => m.id === msg.id);
|
|
3243
3457
|
const updated = {
|
|
3244
|
-
...
|
|
3245
|
-
messages: [...
|
|
3458
|
+
...fresh,
|
|
3459
|
+
messages: present ? fresh.messages : [...fresh.messages, msg],
|
|
3246
3460
|
updatedAt: Date.now(),
|
|
3247
|
-
title:
|
|
3461
|
+
title: title ?? fresh.title
|
|
3248
3462
|
};
|
|
3463
|
+
if (isFirstUserMsg) updated.autoTitle = true;
|
|
3249
3464
|
this._replace(updated);
|
|
3250
3465
|
await this._adapter.save(updated);
|
|
3251
3466
|
this._notify();
|
|
@@ -3258,20 +3473,44 @@ class AparteConversationManager {
|
|
|
3258
3473
|
async updateMessages(convId, messages, tree) {
|
|
3259
3474
|
const conv = this._find(convId);
|
|
3260
3475
|
if (!conv) return;
|
|
3476
|
+
if (!this._loaded.has(convId)) {
|
|
3477
|
+
console.warn(`[AparteConversationManager] updateMessages('${convId}') before its messages were loaded: ignored.`);
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3480
|
+
const firstUser = messages.find((m) => m.role === "user");
|
|
3261
3481
|
if (this._retention) {
|
|
3262
3482
|
({ messages, tree } = applyRetention(messages, tree, this._retention.maxMessages));
|
|
3263
3483
|
}
|
|
3484
|
+
const storedMessages = withoutDerivedSegments(messages);
|
|
3485
|
+
const storedTree = tree === void 0 ? void 0 : withoutDerivedTree(tree);
|
|
3264
3486
|
let contentChanged = true;
|
|
3487
|
+
let treeChanged = tree !== void 0;
|
|
3265
3488
|
try {
|
|
3266
|
-
contentChanged = JSON.stringify(
|
|
3489
|
+
contentChanged = JSON.stringify(storedMessages) !== JSON.stringify(withoutDerivedSegments(conv.messages));
|
|
3490
|
+
if (storedTree !== void 0) treeChanged = JSON.stringify(storedTree) !== JSON.stringify(conv.tree);
|
|
3267
3491
|
} catch {
|
|
3268
3492
|
}
|
|
3493
|
+
let title;
|
|
3494
|
+
if (conv.autoTitle) {
|
|
3495
|
+
const before = conv.messages.find((m) => m.role === "user");
|
|
3496
|
+
if (firstUser && (firstUser.content ?? "") !== (before?.content ?? "")) {
|
|
3497
|
+
title = await this._title(firstUser);
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
if (!contentChanged && !treeChanged && title === void 0) return;
|
|
3501
|
+
const fresh = this._find(convId);
|
|
3502
|
+
if (!fresh) return;
|
|
3269
3503
|
const updated = {
|
|
3270
|
-
...
|
|
3271
|
-
|
|
3272
|
-
|
|
3504
|
+
...fresh,
|
|
3505
|
+
// What is stored is what the caller sent: segments a viewport DERIVED on
|
|
3506
|
+
// display are not the app's content. Stored, they would fill the record with
|
|
3507
|
+
// core-generated ids and freeze the parse as it stood the day it was saved —
|
|
3508
|
+
// a block grammar registered later would never apply to that message.
|
|
3509
|
+
messages: storedMessages,
|
|
3510
|
+
updatedAt: contentChanged ? Date.now() : fresh.updatedAt
|
|
3273
3511
|
};
|
|
3274
|
-
if (
|
|
3512
|
+
if (storedTree !== void 0) updated.tree = storedTree;
|
|
3513
|
+
if (title !== void 0) updated.title = title;
|
|
3275
3514
|
this._replace(updated);
|
|
3276
3515
|
await this._adapter.save(updated);
|
|
3277
3516
|
this._notify();
|
|
@@ -3279,35 +3518,41 @@ class AparteConversationManager {
|
|
|
3279
3518
|
/** Permanently delete a conversation. */
|
|
3280
3519
|
async delete(id) {
|
|
3281
3520
|
this._conversations = this._conversations.filter((c) => c.id !== id);
|
|
3521
|
+
this._loaded.delete(id);
|
|
3282
3522
|
if (this._activeId === id) this._activeId = null;
|
|
3283
3523
|
await this._adapter.delete(id);
|
|
3284
3524
|
this._notify();
|
|
3285
3525
|
}
|
|
3286
3526
|
/** Archive a conversation (soft-delete). */
|
|
3287
3527
|
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
|
-
}
|
|
3528
|
+
if (!await this._meta(id, (conv) => ({ ...conv, archivedAt: Date.now() }), this._adapter.archive)) return;
|
|
3297
3529
|
if (this._activeId === id) this._activeId = null;
|
|
3298
3530
|
this._notify();
|
|
3299
3531
|
}
|
|
3532
|
+
/**
|
|
3533
|
+
* A metadata change, through the adapter's own hook when it has one (the row is
|
|
3534
|
+
* updated in memory, nothing else travels), else through `save()` — with the
|
|
3535
|
+
* messages fetched first, so a row the list came with (`messages: []`) is never
|
|
3536
|
+
* written back over the stored ones.
|
|
3537
|
+
*/
|
|
3538
|
+
async _meta(id, change, hook) {
|
|
3539
|
+
if (hook) {
|
|
3540
|
+
const conv2 = this._find(id);
|
|
3541
|
+
if (!conv2) return false;
|
|
3542
|
+
this._replace(change(conv2));
|
|
3543
|
+
await hook.call(this._adapter, id);
|
|
3544
|
+
return true;
|
|
3545
|
+
}
|
|
3546
|
+
const conv = await this._full(id);
|
|
3547
|
+
if (!conv) return false;
|
|
3548
|
+
const updated = change(conv);
|
|
3549
|
+
this._replace(updated);
|
|
3550
|
+
await this._adapter.save(updated);
|
|
3551
|
+
return true;
|
|
3552
|
+
}
|
|
3300
3553
|
/** Restore an archived conversation. */
|
|
3301
3554
|
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
|
-
}
|
|
3555
|
+
if (!await this._meta(id, (conv) => ({ ...conv, archivedAt: void 0 }), this._adapter.unarchive)) return;
|
|
3311
3556
|
this._notify();
|
|
3312
3557
|
}
|
|
3313
3558
|
/**
|
|
@@ -3317,28 +3562,12 @@ class AparteConversationManager {
|
|
|
3317
3562
|
* does not gets the whole record through `save()`.
|
|
3318
3563
|
*/
|
|
3319
3564
|
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
|
-
}
|
|
3565
|
+
if (!await this._meta(id, (conv) => ({ ...conv, pinnedAt: Date.now() }), this._adapter.pin)) return;
|
|
3329
3566
|
this._notify();
|
|
3330
3567
|
}
|
|
3331
3568
|
/** Unpin a conversation. Metadata only — see `pin()`. */
|
|
3332
3569
|
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
|
-
}
|
|
3570
|
+
if (!await this._meta(id, (conv) => ({ ...conv, pinnedAt: void 0 }), this._adapter.unpin)) return;
|
|
3342
3571
|
this._notify();
|
|
3343
3572
|
}
|
|
3344
3573
|
/** Update the title manually. The full input is preserved — UI surfaces
|
|
@@ -3346,12 +3575,17 @@ class AparteConversationManager {
|
|
|
3346
3575
|
* (`min-w-0` + `truncate`). Auto-titles produced internally by
|
|
3347
3576
|
* `_autoTitle()` remain capped at a sensible length on input. */
|
|
3348
3577
|
async updateTitle(id, title) {
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
this.
|
|
3578
|
+
if (!this._find(id)) {
|
|
3579
|
+
console.warn(`[AparteConversationManager] updateTitle('${id}'): unknown conversation.`);
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
const next = title.trim();
|
|
3583
|
+
const done = await this._meta(
|
|
3584
|
+
id,
|
|
3585
|
+
(conv) => ({ ...conv, title: next, autoTitle: false }),
|
|
3586
|
+
this._adapter.rename ? (convId) => this._adapter.rename(convId, next) : void 0
|
|
3587
|
+
);
|
|
3588
|
+
if (done) this._notify();
|
|
3355
3589
|
}
|
|
3356
3590
|
// ─── Observer ───────────────────────────────────────────────────────────
|
|
3357
3591
|
subscribe(listener) {
|
|
@@ -3378,8 +3612,9 @@ class AparteConversationManager {
|
|
|
3378
3612
|
});
|
|
3379
3613
|
}
|
|
3380
3614
|
/**
|
|
3381
|
-
* The title of a conversation, decided
|
|
3382
|
-
*
|
|
3615
|
+
* The title of a conversation, decided on its first user message — and again if
|
|
3616
|
+
* that message is edited while the title is still the manager's: the provider's
|
|
3617
|
+
* answer when one is registered and answers, else the default.
|
|
3383
3618
|
*/
|
|
3384
3619
|
async _title(msg) {
|
|
3385
3620
|
const fallback = this._autoTitle(msg);
|
|
@@ -3393,7 +3628,7 @@ class AparteConversationManager {
|
|
|
3393
3628
|
}
|
|
3394
3629
|
}
|
|
3395
3630
|
_autoTitle(msg) {
|
|
3396
|
-
return (msg.content ?? "").trim()
|
|
3631
|
+
return (msg.content ?? "").trim();
|
|
3397
3632
|
}
|
|
3398
3633
|
}
|
|
3399
3634
|
function filesToAttachments(files) {
|
|
@@ -3432,6 +3667,8 @@ class AparteConversationController {
|
|
|
3432
3667
|
_subscribedTo = null;
|
|
3433
3668
|
/** Between bind() and unbind(): the only time a manager subscription may be installed. */
|
|
3434
3669
|
_bound = false;
|
|
3670
|
+
/** The binding shows the wait this controller asked for — see `_showWait`. */
|
|
3671
|
+
_waitShown = false;
|
|
3435
3672
|
/**
|
|
3436
3673
|
* React to an external mutation of the manager (another component deleting or
|
|
3437
3674
|
* archiving the active conversation). Without this the binding would keep
|
|
@@ -3452,6 +3689,7 @@ class AparteConversationController {
|
|
|
3452
3689
|
}
|
|
3453
3690
|
this._activeId = null;
|
|
3454
3691
|
this._binding.clearMessages();
|
|
3692
|
+
this._showWait(false);
|
|
3455
3693
|
}
|
|
3456
3694
|
};
|
|
3457
3695
|
constructor(binding, options = {}) {
|
|
@@ -3558,7 +3796,7 @@ class AparteConversationController {
|
|
|
3558
3796
|
void this.setConversationId(evt.detail?.id ?? null);
|
|
3559
3797
|
};
|
|
3560
3798
|
if (typeof window !== "undefined") {
|
|
3561
|
-
window.addEventListener("aparte-select
|
|
3799
|
+
window.addEventListener("aparte-conversation-select", this._onSelectConversation);
|
|
3562
3800
|
}
|
|
3563
3801
|
this._bound = true;
|
|
3564
3802
|
this._manager();
|
|
@@ -3599,7 +3837,7 @@ class AparteConversationController {
|
|
|
3599
3837
|
this._onMessageAborted = null;
|
|
3600
3838
|
}
|
|
3601
3839
|
if (this._onSelectConversation && typeof window !== "undefined") {
|
|
3602
|
-
window.removeEventListener("aparte-select
|
|
3840
|
+
window.removeEventListener("aparte-conversation-select", this._onSelectConversation);
|
|
3603
3841
|
this._onSelectConversation = null;
|
|
3604
3842
|
}
|
|
3605
3843
|
if (this._unsubscribeManager) {
|
|
@@ -3645,6 +3883,7 @@ class AparteConversationController {
|
|
|
3645
3883
|
this._activeId = null;
|
|
3646
3884
|
manager?.clearActive();
|
|
3647
3885
|
if (isSwitch) this._binding.clearMessages();
|
|
3886
|
+
this._showWait(false);
|
|
3648
3887
|
return;
|
|
3649
3888
|
}
|
|
3650
3889
|
const conv = manager?.conversations.find((c) => c.id === id);
|
|
@@ -3660,29 +3899,68 @@ class AparteConversationController {
|
|
|
3660
3899
|
console.warn("[ConversationController] id still unknown after hydration — clearing. id:", id);
|
|
3661
3900
|
manager.clearActive();
|
|
3662
3901
|
this._binding.clearMessages();
|
|
3902
|
+
this._showWait(false);
|
|
3663
3903
|
}
|
|
3664
3904
|
});
|
|
3665
3905
|
this._stopHydrationRetry = stop;
|
|
3906
|
+
this._showWait(false);
|
|
3666
3907
|
return;
|
|
3667
3908
|
}
|
|
3668
3909
|
console.warn("[ConversationController] unknown id or no manager — clearing. manager:", !!manager, "conv:", !!conv);
|
|
3669
3910
|
this._activeId = null;
|
|
3670
3911
|
manager?.clearActive();
|
|
3671
3912
|
this._binding.clearMessages();
|
|
3913
|
+
this._showWait(false);
|
|
3672
3914
|
return;
|
|
3673
3915
|
}
|
|
3674
3916
|
this._activeId = id;
|
|
3675
3917
|
manager.select(id);
|
|
3918
|
+
let record = conv;
|
|
3919
|
+
const fetching = !manager.isLoaded(id);
|
|
3920
|
+
if (fetching) {
|
|
3921
|
+
this._showWait(true);
|
|
3922
|
+
this._binding.clearMessages();
|
|
3923
|
+
let loaded = false;
|
|
3924
|
+
try {
|
|
3925
|
+
loaded = await manager.ensureFull(id);
|
|
3926
|
+
} catch (err) {
|
|
3927
|
+
console.warn("[ConversationController] the conversation could not be loaded:", id, err);
|
|
3928
|
+
}
|
|
3929
|
+
if (this._activeId !== id) return;
|
|
3930
|
+
if (!loaded) {
|
|
3931
|
+
if (manager.conversations.some((c) => c.id === id)) {
|
|
3932
|
+
console.warn("[ConversationController] no messages came back for", id, "— leaving it.");
|
|
3933
|
+
}
|
|
3934
|
+
this._activeId = null;
|
|
3935
|
+
manager.clearActive();
|
|
3936
|
+
this._binding.clearMessages();
|
|
3937
|
+
this._showWait(false);
|
|
3938
|
+
return;
|
|
3939
|
+
}
|
|
3940
|
+
record = manager.conversations.find((c) => c.id === id) ?? conv;
|
|
3941
|
+
}
|
|
3676
3942
|
this._isLoadingConversation = true;
|
|
3677
3943
|
try {
|
|
3678
|
-
this._binding.setMessages([...
|
|
3679
|
-
if (
|
|
3680
|
-
this._binding.importTree(
|
|
3944
|
+
this._binding.setMessages([...record.messages ?? []]);
|
|
3945
|
+
if (record.tree && this._binding.importTree) {
|
|
3946
|
+
this._binding.importTree(record.tree);
|
|
3681
3947
|
}
|
|
3682
3948
|
} finally {
|
|
3683
3949
|
this._isLoadingConversation = false;
|
|
3950
|
+
this._showWait(false);
|
|
3684
3951
|
}
|
|
3685
3952
|
}
|
|
3953
|
+
/**
|
|
3954
|
+
* The binding's wait, owned here: whoever runs `setConversationId` last clears it
|
|
3955
|
+
* — a loaded conversation or a new chat that replaces a fetch, a delete of the
|
|
3956
|
+
* one being fetched — so a wait shown for one call never outlives the call that
|
|
3957
|
+
* superseded it.
|
|
3958
|
+
*/
|
|
3959
|
+
_showWait(on) {
|
|
3960
|
+
if (this._waitShown === on) return;
|
|
3961
|
+
this._waitShown = on;
|
|
3962
|
+
this._binding.setLoading?.(on);
|
|
3963
|
+
}
|
|
3686
3964
|
// ─────────────────────────────────────────────────────────────────────
|
|
3687
3965
|
// Internal
|
|
3688
3966
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -3700,6 +3978,7 @@ class AparteConversationController {
|
|
|
3700
3978
|
if (!this._activeId) return;
|
|
3701
3979
|
const manager = this._manager();
|
|
3702
3980
|
if (!manager) return;
|
|
3981
|
+
if (!manager.isLoaded(this._activeId)) return;
|
|
3703
3982
|
const msgs = this._binding.getMessages().map(
|
|
3704
3983
|
(m) => m.status === "streaming" || m.status === "pending" ? { ...m, status: "completed" } : m
|
|
3705
3984
|
);
|
|
@@ -3729,12 +4008,12 @@ class AparteConversationController {
|
|
|
3729
4008
|
}
|
|
3730
4009
|
if (!convId) {
|
|
3731
4010
|
if (!this._ensureInFlight) {
|
|
3732
|
-
const text = (userMsg.content ?? "").toString().trim()
|
|
4011
|
+
const text = (userMsg.content ?? "").toString().trim();
|
|
3733
4012
|
this._ensureInFlight = manager.createNew(text).then((conv) => conv.id);
|
|
3734
4013
|
}
|
|
3735
4014
|
convId = await this._ensureInFlight;
|
|
3736
4015
|
this._ensureInFlight = null;
|
|
3737
|
-
if (this._activeId !== wasActiveId) {
|
|
4016
|
+
if (this._activeId !== wasActiveId && this._activeId !== convId) {
|
|
3738
4017
|
void manager.delete(convId).catch(() => {
|
|
3739
4018
|
});
|
|
3740
4019
|
return;
|
|
@@ -3872,7 +4151,9 @@ class AparteChatHost {
|
|
|
3872
4151
|
setMessages: (msgs) => {
|
|
3873
4152
|
this._beginConversationSwap();
|
|
3874
4153
|
this._vp()?.resetSpacer?.();
|
|
3875
|
-
|
|
4154
|
+
const adopted = msgs.map(adoptMessageSegments);
|
|
4155
|
+
this.binding.setMessages(adopted);
|
|
4156
|
+
this._vp()?.setMessages?.(adopted);
|
|
3876
4157
|
},
|
|
3877
4158
|
appendMessage: (msg) => this.appendMessage(msg),
|
|
3878
4159
|
getMessages: () => this.binding.getMessages(),
|
|
@@ -3887,6 +4168,17 @@ class AparteChatHost {
|
|
|
3887
4168
|
},
|
|
3888
4169
|
importTree: (tree) => {
|
|
3889
4170
|
this._vp()?.importTree?.(tree);
|
|
4171
|
+
},
|
|
4172
|
+
// One writer for the wait. A wrapper that hears `onLoadingChange` draws the
|
|
4173
|
+
// wait itself and writes the viewport's attribute from its own
|
|
4174
|
+
// `loading || hostLoading` — so the host writing it too meant that, with the
|
|
4175
|
+
// consumer's own prop on, a `setLoading(false)` here removed the attribute
|
|
4176
|
+
// (and `aria-busy`) while the wrapper still drew the skeleton and still held
|
|
4177
|
+
// the composer shut. A binding that cannot hear it has no other way to know,
|
|
4178
|
+
// and there the attribute stays the host's to write.
|
|
4179
|
+
setLoading: (on) => {
|
|
4180
|
+
if (this.binding.onLoadingChange) this.binding.onLoadingChange(on);
|
|
4181
|
+
else this._vp()?.setLoading?.(on);
|
|
3890
4182
|
}
|
|
3891
4183
|
};
|
|
3892
4184
|
this._controller = new AparteConversationController(convBinding, {
|
|
@@ -3992,7 +4284,7 @@ class AparteChatHost {
|
|
|
3992
4284
|
// defaults rather than the page's.
|
|
3993
4285
|
resolveConfig(this.binding.host).getSegmentDefaults(segment.type)
|
|
3994
4286
|
) : segment;
|
|
3995
|
-
this.
|
|
4287
|
+
this._streamBubble(last?.id)?.addSegment?.(stamped);
|
|
3996
4288
|
if (!last) return;
|
|
3997
4289
|
const segments = [...last.segments || [], stamped];
|
|
3998
4290
|
const next = [...msgs.slice(0, -1), { ...last, segments }];
|
|
@@ -4007,7 +4299,7 @@ class AparteChatHost {
|
|
|
4007
4299
|
if (last && this._isOrphan(last.id)) return;
|
|
4008
4300
|
const current = last?.segments?.find((s) => s.id === segmentId);
|
|
4009
4301
|
const stamped = current ? stampSegmentOnUpdate(current, updates) : updates;
|
|
4010
|
-
this.
|
|
4302
|
+
this._streamBubble(last?.id)?.updateSegment?.(segmentId, stamped);
|
|
4011
4303
|
if (!last || !last.segments) return;
|
|
4012
4304
|
const segments = last.segments.map(
|
|
4013
4305
|
(s) => s.id === segmentId ? mergeSegmentUpdate(s, stamped) : s
|
|
@@ -4019,7 +4311,7 @@ class AparteChatHost {
|
|
|
4019
4311
|
/** Remove a transient segment (a status row the host added itself). */
|
|
4020
4312
|
removeSegment(segmentId) {
|
|
4021
4313
|
this._flushStreamState();
|
|
4022
|
-
this.
|
|
4314
|
+
this._streamBubble(this.binding.getMessages().at(-1)?.id)?.removeSegment?.(segmentId);
|
|
4023
4315
|
const msgs = this.binding.getMessages();
|
|
4024
4316
|
const last = msgs[msgs.length - 1];
|
|
4025
4317
|
if (!last?.segments) return;
|
|
@@ -4331,6 +4623,27 @@ class AparteChatHost {
|
|
|
4331
4623
|
`aparte-chat-bubble[message-id="${cssEscape(id)}"]`
|
|
4332
4624
|
);
|
|
4333
4625
|
}
|
|
4626
|
+
/**
|
|
4627
|
+
* The bubble the streaming writes below belong to: the one carrying that message's
|
|
4628
|
+
* id, and NOTHING when it is not on the page yet.
|
|
4629
|
+
*
|
|
4630
|
+
* It used to be "the last bubble in the viewport", which is a different message
|
|
4631
|
+
* whenever the DOM is a render behind the list — which is every framework here.
|
|
4632
|
+
* `appendMessage` puts the new reply in React's state and React paints it on its own
|
|
4633
|
+
* schedule, so a first chunk arriving in between went into the PREVIOUS reply's
|
|
4634
|
+
* bubble; `syncBubbles` then wrote the same segment onto the real one, and the same
|
|
4635
|
+
* `data-segment-id` was on two bubbles.
|
|
4636
|
+
*
|
|
4637
|
+
* Skipping the paint is safe because it is only ever a paint: the message list is
|
|
4638
|
+
* written either way, and `syncBubbles` — called by the framework after each render,
|
|
4639
|
+
* always re-syncing the last message — puts it on screen as soon as the bubble
|
|
4640
|
+
* exists. Without an id there is nothing to resolve, so the last bubble stands: that
|
|
4641
|
+
* is a host writing into a transcript it has not appended to.
|
|
4642
|
+
*/
|
|
4643
|
+
_streamBubble(messageId) {
|
|
4644
|
+
if (messageId) return this._bubbleById(messageId);
|
|
4645
|
+
return this._lastBubble();
|
|
4646
|
+
}
|
|
4334
4647
|
_lastBubble() {
|
|
4335
4648
|
const vp = this.binding.viewport;
|
|
4336
4649
|
if (!vp) return null;
|
|
@@ -4357,9 +4670,15 @@ class AparteChatHost {
|
|
|
4357
4670
|
const d = e?.detail;
|
|
4358
4671
|
if (d?.messageId) this._setStreamingId(d.messageId);
|
|
4359
4672
|
};
|
|
4673
|
+
const endsThisTurn = (e) => {
|
|
4674
|
+
const id = e?.detail?.messageId;
|
|
4675
|
+
return !id || this._streamingId === null || id === this._streamingId;
|
|
4676
|
+
};
|
|
4360
4677
|
const onDone = (e) => {
|
|
4361
|
-
|
|
4362
|
-
|
|
4678
|
+
if (endsThisTurn(e)) {
|
|
4679
|
+
this.binding.onTypingChange?.(false);
|
|
4680
|
+
this._setStreamingId(null);
|
|
4681
|
+
}
|
|
4363
4682
|
const d = e?.detail;
|
|
4364
4683
|
if (!d?.messageId || !d.usage) return;
|
|
4365
4684
|
const next = this.binding.getMessages().map(
|
|
@@ -4369,7 +4688,8 @@ class AparteChatHost {
|
|
|
4369
4688
|
this.binding.onMessagesChange?.(next);
|
|
4370
4689
|
this._bubbleById(d.messageId)?.setUsage?.(d.usage);
|
|
4371
4690
|
};
|
|
4372
|
-
const onEnd = () => {
|
|
4691
|
+
const onEnd = (e) => {
|
|
4692
|
+
if (!endsThisTurn(e)) return;
|
|
4373
4693
|
this.binding.onTypingChange?.(false);
|
|
4374
4694
|
this._setStreamingId(null);
|
|
4375
4695
|
};
|
|
@@ -4589,6 +4909,15 @@ function readableToAsyncIterable(stream, signal) {
|
|
|
4589
4909
|
}
|
|
4590
4910
|
};
|
|
4591
4911
|
}
|
|
4912
|
+
function eventOrigin(e) {
|
|
4913
|
+
return e.composedPath?.()[0] ?? e.target;
|
|
4914
|
+
}
|
|
4915
|
+
function rootOf(node) {
|
|
4916
|
+
return node.getRootNode();
|
|
4917
|
+
}
|
|
4918
|
+
function activeElementIn(node) {
|
|
4919
|
+
return node.getRootNode().activeElement ?? document.activeElement;
|
|
4920
|
+
}
|
|
4592
4921
|
let steppedPanelSeq = 0;
|
|
4593
4922
|
let _relabels = null;
|
|
4594
4923
|
function onRelabel(fn) {
|
|
@@ -5110,11 +5439,14 @@ function requestUserInput(request) {
|
|
|
5110
5439
|
return resolveConfig(request.target ?? null).requestUserInput(request);
|
|
5111
5440
|
}
|
|
5112
5441
|
const COMPACTION_PREAMBLE = "The earlier part of this conversation was compacted. This is a summary of what came before; continue from it.";
|
|
5442
|
+
const warnedUninlinedFiles = /* @__PURE__ */ new Set();
|
|
5443
|
+
const CHAT_SELECTOR = "aparte-chat, aparte-chat-viewport, [data-aparte-chat]";
|
|
5113
5444
|
class AparteClient {
|
|
5114
5445
|
_boundHandler = null;
|
|
5115
5446
|
_boundAbortHandler = null;
|
|
5116
5447
|
_boundRetryHandler = null;
|
|
5117
5448
|
_boundEditHandler = null;
|
|
5449
|
+
_releaseKeyResolver = null;
|
|
5118
5450
|
_isAborted = false;
|
|
5119
5451
|
/**
|
|
5120
5452
|
* Does this window event belong to this client?
|
|
@@ -5135,7 +5467,7 @@ class AparteClient {
|
|
|
5135
5467
|
const scope = this.options.scopeToTargetId;
|
|
5136
5468
|
if (scope) return detail?.targetId === scope;
|
|
5137
5469
|
if (this._config === aparteGlobalConfig) return true;
|
|
5138
|
-
const target = this._resolveTarget(detail?.targetId);
|
|
5470
|
+
const target = this._resolveTarget(detail?.targetId, e);
|
|
5139
5471
|
if (!target) return true;
|
|
5140
5472
|
const owner = resolveConfig(target);
|
|
5141
5473
|
return owner === this._config || owner === aparteGlobalConfig;
|
|
@@ -5151,6 +5483,7 @@ class AparteClient {
|
|
|
5151
5483
|
...options
|
|
5152
5484
|
};
|
|
5153
5485
|
this._config = options.config ?? aparteGlobalConfig;
|
|
5486
|
+
this._registerKeyResolver();
|
|
5154
5487
|
if (this.options.autoRegister) {
|
|
5155
5488
|
registerDefaultRenderers(this._config);
|
|
5156
5489
|
} else {
|
|
@@ -5176,6 +5509,20 @@ class AparteClient {
|
|
|
5176
5509
|
_beginUserTurn() {
|
|
5177
5510
|
this._isAborted = false;
|
|
5178
5511
|
}
|
|
5512
|
+
/**
|
|
5513
|
+
* Put this client's `keyResolver` on its config, and hold the teardown so
|
|
5514
|
+
* `stop()` can take it back off.
|
|
5515
|
+
*
|
|
5516
|
+
* The registration used to be fire-and-forget, so it outlived the client that
|
|
5517
|
+
* made it: a stopped client kept signing model-list requests, and a remount —
|
|
5518
|
+
* the way the wrappers document swapping options — added a source behind the
|
|
5519
|
+
* one it meant to replace instead of replacing it. Idempotent, so the usual
|
|
5520
|
+
* `new AparteClient({…}).start()` registers once.
|
|
5521
|
+
*/
|
|
5522
|
+
_registerKeyResolver() {
|
|
5523
|
+
if (this._releaseKeyResolver || !this.options.keyResolver) return;
|
|
5524
|
+
this._releaseKeyResolver = this._config.registerKeyProvider(this.options.keyResolver);
|
|
5525
|
+
}
|
|
5179
5526
|
/**
|
|
5180
5527
|
* Sets up the event listeners.
|
|
5181
5528
|
* This is called once in the constructor.
|
|
@@ -5194,6 +5541,7 @@ class AparteClient {
|
|
|
5194
5541
|
* Start listening for aparte-send events on the window.
|
|
5195
5542
|
*/
|
|
5196
5543
|
start() {
|
|
5544
|
+
this._registerKeyResolver();
|
|
5197
5545
|
if (!this._boundHandler) {
|
|
5198
5546
|
this._setupListeners();
|
|
5199
5547
|
}
|
|
@@ -5229,6 +5577,8 @@ class AparteClient {
|
|
|
5229
5577
|
*/
|
|
5230
5578
|
stop() {
|
|
5231
5579
|
this.abort();
|
|
5580
|
+
this._releaseKeyResolver?.();
|
|
5581
|
+
this._releaseKeyResolver = null;
|
|
5232
5582
|
if (!this._boundHandler) return;
|
|
5233
5583
|
window.removeEventListener("aparte-send", this._boundHandler);
|
|
5234
5584
|
this._boundHandler = null;
|
|
@@ -5429,7 +5779,7 @@ class AparteClient {
|
|
|
5429
5779
|
const { messageId, targetId } = event.detail ?? {};
|
|
5430
5780
|
this._beginUserTurn();
|
|
5431
5781
|
if (!messageId) return;
|
|
5432
|
-
const targetElement = this._resolveTarget(targetId);
|
|
5782
|
+
const targetElement = this._resolveTarget(targetId, event);
|
|
5433
5783
|
if (!targetElement) {
|
|
5434
5784
|
console.warn("[AparteClient] aparte-retry — no target found");
|
|
5435
5785
|
return;
|
|
@@ -5490,7 +5840,7 @@ class AparteClient {
|
|
|
5490
5840
|
const { messageId, content: newContent, targetId } = event.detail ?? {};
|
|
5491
5841
|
this._beginUserTurn();
|
|
5492
5842
|
if (!messageId || newContent === void 0) return;
|
|
5493
|
-
const targetElement = this._resolveTarget(targetId);
|
|
5843
|
+
const targetElement = this._resolveTarget(targetId, event);
|
|
5494
5844
|
if (!targetElement) {
|
|
5495
5845
|
console.warn("[AparteClient] aparte-edit — no target found");
|
|
5496
5846
|
return;
|
|
@@ -5532,24 +5882,83 @@ class AparteClient {
|
|
|
5532
5882
|
authConfig
|
|
5533
5883
|
);
|
|
5534
5884
|
}
|
|
5885
|
+
/**
|
|
5886
|
+
* The chain the gesture really came up, nearest node first.
|
|
5887
|
+
*
|
|
5888
|
+
* Read from a `window` listener, `event.target` is the shadow HOST when the event
|
|
5889
|
+
* left a tree of the consumer's own, and `parentElement` stops at that tree's root —
|
|
5890
|
+
* so a walk from either starts, or ends, outside the chat the person typed into.
|
|
5891
|
+
* `composedPath()` is the one chain that crosses. The fallback is for an event
|
|
5892
|
+
* dispatched by hand, which has no path.
|
|
5893
|
+
*/
|
|
5894
|
+
_originChain(event) {
|
|
5895
|
+
const path = event?.composedPath?.() ?? [];
|
|
5896
|
+
if (path.length > 0) return path;
|
|
5897
|
+
const chain = [];
|
|
5898
|
+
let node = event ? eventOrigin(event) : null;
|
|
5899
|
+
while (node) {
|
|
5900
|
+
chain.push(node);
|
|
5901
|
+
node = node.parentElement;
|
|
5902
|
+
}
|
|
5903
|
+
return chain;
|
|
5904
|
+
}
|
|
5905
|
+
/**
|
|
5906
|
+
* The trees a lookup must search, the one the gesture came from FIRST.
|
|
5907
|
+
*
|
|
5908
|
+
* `document.getElementById` and `document.querySelectorAll` cannot see into a shadow
|
|
5909
|
+
* root, so a chat mounted in one — the arrangement the theming guide endorses — was
|
|
5910
|
+
* invisible to both: alone, the send was dropped with a warning; on a page that also
|
|
5911
|
+
* holds a chat in the light DOM, the scan answered with the WRONG chat, so the
|
|
5912
|
+
* person's message AND the model's reply landed in a transcript they never typed
|
|
5913
|
+
* into. Nearest tree first is what keeps it from swapping the other way round.
|
|
5914
|
+
*/
|
|
5915
|
+
_rootsFor(event) {
|
|
5916
|
+
const roots = [];
|
|
5917
|
+
for (const node of this._originChain(event)) {
|
|
5918
|
+
if (typeof node.getRootNode !== "function") continue;
|
|
5919
|
+
const root = rootOf(node);
|
|
5920
|
+
if (!roots.includes(root)) roots.push(root);
|
|
5921
|
+
}
|
|
5922
|
+
if (!roots.includes(document)) roots.push(document);
|
|
5923
|
+
return roots;
|
|
5924
|
+
}
|
|
5925
|
+
/** The element an id names, looked up in the gesture's own tree before the document. */
|
|
5926
|
+
_byId(targetId, event) {
|
|
5927
|
+
for (const root of this._rootsFor(event)) {
|
|
5928
|
+
const el2 = root.getElementById?.(targetId);
|
|
5929
|
+
if (el2) return el2;
|
|
5930
|
+
}
|
|
5931
|
+
return null;
|
|
5932
|
+
}
|
|
5933
|
+
/** The first chat that can render, own tree first — the path when nothing names one. */
|
|
5934
|
+
_scanForTarget(event) {
|
|
5935
|
+
for (const root of this._rootsFor(event)) {
|
|
5936
|
+
for (const candidate of root.querySelectorAll(CHAT_SELECTOR)) {
|
|
5937
|
+
const target = this._asRenderTarget(candidate);
|
|
5938
|
+
if (target) return target;
|
|
5939
|
+
}
|
|
5940
|
+
}
|
|
5941
|
+
return null;
|
|
5942
|
+
}
|
|
5535
5943
|
/**
|
|
5536
5944
|
* Resolve a target element by id (from event detail.targetId) or via targetResolver / DOM scan.
|
|
5537
5945
|
*/
|
|
5538
|
-
_resolveTarget(targetId) {
|
|
5946
|
+
_resolveTarget(targetId, event) {
|
|
5539
5947
|
if (targetId) {
|
|
5540
|
-
const el2 =
|
|
5948
|
+
const el2 = this._byId(targetId, event);
|
|
5541
5949
|
if (el2) return this._asRenderTarget(el2) ?? el2;
|
|
5542
5950
|
}
|
|
5543
5951
|
if (this.options.targetResolver) {
|
|
5544
5952
|
const el2 = this.options.targetResolver();
|
|
5545
5953
|
if (el2) return this._asRenderTarget(el2) ?? el2;
|
|
5546
5954
|
}
|
|
5547
|
-
const
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5955
|
+
const scanned = this._scanForTarget(event);
|
|
5956
|
+
if (scanned) return scanned;
|
|
5957
|
+
for (const root of this._rootsFor(event)) {
|
|
5958
|
+
const first = root.querySelector(CHAT_SELECTOR);
|
|
5959
|
+
if (first) return first;
|
|
5551
5960
|
}
|
|
5552
|
-
return
|
|
5961
|
+
return null;
|
|
5553
5962
|
}
|
|
5554
5963
|
/**
|
|
5555
5964
|
* Resolve an element to a usable render target: itself when it exposes
|
|
@@ -5640,7 +6049,7 @@ class AparteClient {
|
|
|
5640
6049
|
let targetElement = null;
|
|
5641
6050
|
const targetId = event.detail?.targetId;
|
|
5642
6051
|
if (targetId) {
|
|
5643
|
-
const byId =
|
|
6052
|
+
const byId = this._byId(targetId, event);
|
|
5644
6053
|
const resolved = this._asRenderTarget(byId);
|
|
5645
6054
|
if (resolved) {
|
|
5646
6055
|
targetElement = resolved;
|
|
@@ -5649,32 +6058,22 @@ class AparteClient {
|
|
|
5649
6058
|
}
|
|
5650
6059
|
}
|
|
5651
6060
|
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
|
-
}
|
|
6061
|
+
targetElement = this._asRenderTarget(
|
|
6062
|
+
this.options.targetResolver()
|
|
6063
|
+
);
|
|
5665
6064
|
}
|
|
5666
6065
|
if (!targetElement) {
|
|
5667
|
-
const
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
const resolved = this._asRenderTarget(candidate);
|
|
5672
|
-
if (resolved) {
|
|
5673
|
-
targetElement = resolved;
|
|
6066
|
+
for (const node of this._originChain(event)) {
|
|
6067
|
+
const walker = node;
|
|
6068
|
+
if (typeof walker?.appendMessage === "function") {
|
|
6069
|
+
targetElement = walker;
|
|
5674
6070
|
break;
|
|
5675
6071
|
}
|
|
5676
6072
|
}
|
|
5677
6073
|
}
|
|
6074
|
+
if (!targetElement) {
|
|
6075
|
+
targetElement = this._scanForTarget(event);
|
|
6076
|
+
}
|
|
5678
6077
|
if (!targetElement) {
|
|
5679
6078
|
console.warn("[AparteClient] ⚠️ No target element found with appendMessage support. Provide a targetResolver in AparteClientOptions or ensure the composer has a `target` attribute.");
|
|
5680
6079
|
return;
|
|
@@ -5841,7 +6240,8 @@ ${text}` : text;
|
|
|
5841
6240
|
*
|
|
5842
6241
|
* `thinking` and `tool_call` stay out on purpose — the first is the model's own
|
|
5843
6242
|
* scratchpad, and most APIs neither want it back nor bill for it kindly; the
|
|
5844
|
-
* second
|
|
6243
|
+
* second was already carried, within the turn, as the assistant's calls and the
|
|
6244
|
+
* `tool` messages answering them.
|
|
5845
6245
|
*/
|
|
5846
6246
|
_segmentsToText(segments) {
|
|
5847
6247
|
if (!segments?.length) return "";
|
|
@@ -5885,7 +6285,8 @@ ${fence}`);
|
|
|
5885
6285
|
* - Images → AparteImagePart (base64 data URL)
|
|
5886
6286
|
* - Text files (txt, md, json, csv, xml, html, css, js, ts, …) → AparteTextPart
|
|
5887
6287
|
* injected as a fenced block so all models (including local) can read them.
|
|
5888
|
-
* - Other binary files →
|
|
6288
|
+
* - Other binary files → no content part, and a one-time `console.warn` naming the
|
|
6289
|
+
* file (see the branch below for why it is not silent).
|
|
5889
6290
|
*/
|
|
5890
6291
|
async _filesToContentParts(files) {
|
|
5891
6292
|
const TEXT_TYPES = /^(text\/|application\/(json|xml|javascript|typescript|x-yaml|yaml|toml|csv|markdown))/i;
|
|
@@ -5918,6 +6319,12 @@ ${content}
|
|
|
5918
6319
|
reader.readAsText(file);
|
|
5919
6320
|
});
|
|
5920
6321
|
}
|
|
6322
|
+
if (!warnedUninlinedFiles.has(file.name)) {
|
|
6323
|
+
warnedUninlinedFiles.add(file.name);
|
|
6324
|
+
console.warn(
|
|
6325
|
+
`[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.`
|
|
6326
|
+
);
|
|
6327
|
+
}
|
|
5921
6328
|
return Promise.resolve(null);
|
|
5922
6329
|
})
|
|
5923
6330
|
);
|
|
@@ -6107,6 +6514,20 @@ class AparteMessageRepository {
|
|
|
6107
6514
|
getMessageById(id) {
|
|
6108
6515
|
return this._nodes.get(id)?.current;
|
|
6109
6516
|
}
|
|
6517
|
+
/**
|
|
6518
|
+
* Every message the tree holds — every branch, not just the active path.
|
|
6519
|
+
*
|
|
6520
|
+
* `getMessages()` walks head → root, so a question about the whole conversation
|
|
6521
|
+
* ("is anything still streaming?") answered from it could not see a turn that a
|
|
6522
|
+
* retry had just taken off the path — and the viewport wrote that turn's remaining
|
|
6523
|
+
* segments into the bubble which replaced it. No order is promised beyond
|
|
6524
|
+
* insertion: a caller that needs the topology reads `export()`.
|
|
6525
|
+
*/
|
|
6526
|
+
getAllMessages() {
|
|
6527
|
+
const all = [];
|
|
6528
|
+
for (const node of this._nodes.values()) all.push(node.current);
|
|
6529
|
+
return all;
|
|
6530
|
+
}
|
|
6110
6531
|
/**
|
|
6111
6532
|
* Returns the IDs of all siblings of `messageId`
|
|
6112
6533
|
* (i.e. all children of its parent, including itself).
|
|
@@ -6165,15 +6586,19 @@ class AparteMessageRepository {
|
|
|
6165
6586
|
this._head = findHead(node);
|
|
6166
6587
|
}
|
|
6167
6588
|
/**
|
|
6168
|
-
* Remove all descendants of `messageId` (inclusive) and
|
|
6169
|
-
* the parent of `messageId`. Used by edit/truncate flows.
|
|
6589
|
+
* Remove all descendants of `messageId` (inclusive) and, when the head was among
|
|
6590
|
+
* them, set `head` to the parent of `messageId`. Used by edit/truncate flows.
|
|
6591
|
+
*
|
|
6592
|
+
* A head in a branch this call did not touch stays where it is: it used to be
|
|
6593
|
+
* relocated regardless, which took the active path out of the version the reader was
|
|
6594
|
+
* on for a truncate somewhere else entirely.
|
|
6170
6595
|
*
|
|
6171
6596
|
* @param messageId The first message to remove
|
|
6172
|
-
* @param newParentId Optional explicit parent to set head to
|
|
6173
6597
|
*/
|
|
6174
6598
|
resetHead(messageId) {
|
|
6175
6599
|
const node = this._nodes.get(messageId);
|
|
6176
6600
|
if (!node) return;
|
|
6601
|
+
const headBefore = this._head;
|
|
6177
6602
|
this._deleteDescendants(node);
|
|
6178
6603
|
const parentOrRoot = node.prev ?? this._root;
|
|
6179
6604
|
parentOrRoot.children = parentOrRoot.children.filter((id) => id !== messageId);
|
|
@@ -6181,7 +6606,8 @@ class AparteMessageRepository {
|
|
|
6181
6606
|
parentOrRoot.next = null;
|
|
6182
6607
|
}
|
|
6183
6608
|
this._nodes.delete(messageId);
|
|
6184
|
-
|
|
6609
|
+
const headSurvived = headBefore !== null && headBefore !== node && this._nodes.get(headBefore.current.id) === headBefore;
|
|
6610
|
+
if (!headSurvived) this._head = node.prev;
|
|
6185
6611
|
}
|
|
6186
6612
|
/**
|
|
6187
6613
|
* Update a message's data without changing its position in the tree.
|
|
@@ -6336,13 +6762,13 @@ const APARTE_DEFAULT_UI_EVENTS = [
|
|
|
6336
6762
|
"aparte-reset-done",
|
|
6337
6763
|
"aparte-path-changed",
|
|
6338
6764
|
// <aparte-conversation-list>
|
|
6339
|
-
"aparte-select
|
|
6340
|
-
"aparte-delete
|
|
6341
|
-
"aparte-archive
|
|
6342
|
-
"aparte-unarchive
|
|
6343
|
-
"aparte-rename
|
|
6344
|
-
"aparte-pin
|
|
6345
|
-
"aparte-unpin
|
|
6765
|
+
"aparte-conversation-select",
|
|
6766
|
+
"aparte-conversation-delete",
|
|
6767
|
+
"aparte-conversation-archive",
|
|
6768
|
+
"aparte-conversation-unarchive",
|
|
6769
|
+
"aparte-conversation-rename",
|
|
6770
|
+
"aparte-conversation-pin",
|
|
6771
|
+
"aparte-conversation-unpin",
|
|
6346
6772
|
// the select primitives
|
|
6347
6773
|
"aparte-select-change",
|
|
6348
6774
|
"aparte-select-open",
|
|
@@ -6470,15 +6896,19 @@ export {
|
|
|
6470
6896
|
subscribeConfigChange as a4,
|
|
6471
6897
|
unregisterSegmentRenderer as a5,
|
|
6472
6898
|
uuid as a6,
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6899
|
+
eventOrigin as a7,
|
|
6900
|
+
activeElementIn as a8,
|
|
6901
|
+
mergeSegmentUpdate as a9,
|
|
6902
|
+
writeStreamedMarkdown as aa,
|
|
6903
|
+
stampSegmentActivity as ab,
|
|
6904
|
+
stampSegmentOnInsert as ac,
|
|
6905
|
+
stampSegmentOnUpdate as ad,
|
|
6906
|
+
renumberSegments as ae,
|
|
6907
|
+
openSegmentIds as af,
|
|
6908
|
+
isTerminalStatus as ag,
|
|
6909
|
+
adoptMessageSegments as ah,
|
|
6910
|
+
APARTE_DERIVED_SEGMENTS as ai,
|
|
6911
|
+
rootOf as aj,
|
|
6482
6912
|
APARTE_DEFAULT_BUBBLE_ACTIONS as b,
|
|
6483
6913
|
APARTE_DEFAULT_HOST_HANDLERS as c,
|
|
6484
6914
|
APARTE_DEFAULT_ICON_FALLBACKS as d,
|
|
@@ -6505,4 +6935,4 @@ export {
|
|
|
6505
6935
|
collectRendererStyles as y,
|
|
6506
6936
|
contentToText as z
|
|
6507
6937
|
};
|
|
6508
|
-
//# sourceMappingURL=dialog-triggers-
|
|
6938
|
+
//# sourceMappingURL=dialog-triggers-Bb-3EkT4.js.map
|