@aparte/core 0.19.2 → 0.19.3

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.
Files changed (62) hide show
  1. package/README.md +6 -1
  2. package/dist/client/aparte-client.d.ts +10 -2
  3. package/dist/client/aparte-client.d.ts.map +1 -1
  4. package/dist/client/stream-adapter.d.ts.map +1 -1
  5. package/dist/components/bubble/aparte-chat-bubble.d.ts +1 -1
  6. package/dist/components/bubble/bubble-sync.d.ts +1 -1
  7. package/dist/components/composer/aparte-composer-action.d.ts +1 -1
  8. package/dist/components/composer/aparte-composer-add-attachment.d.ts +1 -1
  9. package/dist/components/composer/aparte-composer-cancel.d.ts +1 -1
  10. package/dist/components/composer/aparte-composer-input.d.ts +3 -2
  11. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  12. package/dist/components/composer/aparte-composer-send.d.ts +1 -1
  13. package/dist/components/composer/aparte-composer.d.ts +2 -0
  14. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  15. package/dist/components/context/aparte-context.d.ts +5 -4
  16. package/dist/components/context/aparte-context.d.ts.map +1 -1
  17. package/dist/components/conversation-list/aparte-conversation-list.d.ts +2 -0
  18. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  19. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  20. package/dist/components/status/aparte-chat-status.d.ts +5 -3
  21. package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
  22. package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -1
  23. package/dist/components/viewport/aparte-chat-viewport.d.ts +7 -0
  24. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  25. package/dist/config/aparte-config.d.ts +86 -79
  26. package/dist/config/aparte-config.d.ts.map +1 -1
  27. package/dist/config/locale.d.ts +4 -0
  28. package/dist/config/locale.d.ts.map +1 -1
  29. package/dist/conversations/conversation-manager.d.ts +3 -3
  30. package/dist/conversations/conversation-manager.d.ts.map +1 -1
  31. package/dist/conversations/index.d.ts +1 -1
  32. package/dist/conversations/index.d.ts.map +1 -1
  33. package/dist/custom-elements.json +12103 -11926
  34. package/dist/{dialog-triggers-CEazY76J.js → dialog-triggers-ChnPFG9M.js} +113 -90
  35. package/dist/dialog-triggers-ChnPFG9M.js.map +1 -0
  36. package/dist/elicitation/approval-panel.d.ts.map +1 -1
  37. package/dist/generated/element-attributes.d.ts +5 -5
  38. package/dist/generated/element-attributes.d.ts.map +1 -1
  39. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  40. package/dist/index.css +1 -1
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +96 -13
  44. package/dist/index.js.map +1 -1
  45. package/dist/index.node.d.ts +2 -2
  46. package/dist/index.node.d.ts.map +1 -1
  47. package/dist/index.node.js +1 -1
  48. package/dist/index.node.js.map +1 -1
  49. package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
  50. package/dist/renderers/segments/thinking.d.ts.map +1 -1
  51. package/dist/transport/agent-handler.d.ts +2 -0
  52. package/dist/transport/agent-handler.d.ts.map +1 -1
  53. package/dist/transport/backend-transport.d.ts +1 -1
  54. package/dist/types/imperative-api.d.ts +3 -1
  55. package/dist/types/imperative-api.d.ts.map +1 -1
  56. package/dist/types/index.d.ts +2 -2
  57. package/dist/types/index.d.ts.map +1 -1
  58. package/dist/types/segments.d.ts +8 -0
  59. package/dist/types/segments.d.ts.map +1 -1
  60. package/dist/types/tools.d.ts +1 -1
  61. package/package.json +3 -3
  62. package/dist/dialog-triggers-CEazY76J.js.map +0 -1
@@ -84,7 +84,7 @@ const APARTE_DEFAULT_LOCALE = {
84
84
  copy: "Copy",
85
85
  copied: "Copied!",
86
86
  retry: "Retry",
87
- thinking: "Thinking...",
87
+ thinking: "Thinking",
88
88
  elicitationOther: "Other…",
89
89
  elicitationOtherPlaceholder: "Type your answer…",
90
90
  elicitationOtherLabel: "Custom answer",
@@ -124,7 +124,7 @@ const APARTE_DEFAULT_LOCALE = {
124
124
  code: "Code",
125
125
  generating: "Generating…",
126
126
  rebuildingPreview: "Rebuilding preview…",
127
- typing: "Typing...",
127
+ typing: "Typing",
128
128
  error: "Error",
129
129
  running: "Running...",
130
130
  run: "Run",
@@ -148,6 +148,7 @@ const APARTE_DEFAULT_LOCALE = {
148
148
  messageInfo: "Details",
149
149
  newChat: "New Chat",
150
150
  loadingConversations: "Loading conversations",
151
+ conversationList: "Conversations",
151
152
  loadingConversation: "Loading the conversation",
152
153
  deleteConversation: "Delete",
153
154
  archiveConversation: "Archive",
@@ -698,8 +699,7 @@ class AparteStreamParser {
698
699
  return {
699
700
  id: this._options.autoGenerateIds ? this._generateId() : "",
700
701
  type: "thinking",
701
- content,
702
- label: "Thinking"
702
+ content
703
703
  };
704
704
  }
705
705
  _isPartialMatch(buffer, pattern) {
@@ -1062,8 +1062,7 @@ function createStreamAdapter(opts) {
1062
1062
  id: `think-${uuid()}`,
1063
1063
  type: "thinking",
1064
1064
  content: thinkingContent,
1065
- collapsed: true,
1066
- label: "Thinking"
1065
+ collapsed: true
1067
1066
  };
1068
1067
  thinkingId = seg.id;
1069
1068
  streaming.add(seg.id);
@@ -2054,7 +2053,7 @@ class AparteConfig {
2054
2053
  * @param sanitizer A sanitizer function (e.g. a DOMPurify wrapper) for
2055
2054
  * hardened coverage, or `null` to DISABLE sanitization. Disabling exposes
2056
2055
  * you to XSS from LLM-authored content — only do so for content you fully
2057
- * trust and have already sanitized upstream.
2056
+ * trust and have already sanitized upstream. `null` prints a console warning, once.
2058
2057
  * @example aparteGlobalConfig.setHtmlSanitizer((html) => DOMPurify.sanitize(html));
2059
2058
  */
2060
2059
  setHtmlSanitizer(sanitizer) {
@@ -2275,26 +2274,16 @@ class AparteConfig {
2275
2274
  this._keyProvider = provider;
2276
2275
  }
2277
2276
  /**
2278
- * Add a key source, without replacing the one `setKeyProvider` holds. Returns a
2279
- * teardown.
2280
- *
2281
- * This is how `AparteClient({ keyResolver })` — the channel the docs teach as
2282
- * primary — reaches everything that needs a key rather than the chat alone. It
2283
- * used to reach the chat ONLY, so the model selector, whose one data path is
2284
- * `refreshProviderModels`, showed an empty list on every cloud provider while
2285
- * the chat worked: `fetchModels` returns `[]` without a key, so there was
2286
- * nothing to see and nothing said.
2287
- *
2288
- * The direction matters (decision #9): the client registers a source ON the
2289
- * config, so the capability is never hostage to the client — a page that only
2290
- * ever calls `setKeyProvider` is unchanged, and a page that constructs no
2291
- * client at all still lists models.
2292
- *
2293
- * Every call is its own source, even when the same function is handed in
2294
- * twice: two panes sharing one module-level resolver are two mounts, and the
2295
- * first one to unmount must not take the second one's key with it. A `Set`
2296
- * keyed by the function itself made them one entry, which either teardown
2297
- * removed.
2277
+ * Add a key source beside the one {@link AparteConfig.setKeyProvider | setKeyProvider()}
2278
+ * holds, and return the function that removes it. The sources are asked in the order they
2279
+ * were added, then `setKeyProvider`'s, and the first answer wins. `AparteClient`'s
2280
+ * `keyResolver` is registered this way, so the model selector can list a cloud provider's
2281
+ * models too. Each call is a source of its own, even with the same function.
2282
+ * @example
2283
+ * const release = aparteGlobalConfig.registerKeyProvider((providerId) =>
2284
+ * providerId === 'openrouter' ? localStorage.getItem('openrouter-key') : undefined);
2285
+ * // when what owns the key goes away:
2286
+ * release();
2298
2287
  */
2299
2288
  registerKeyProvider(provider) {
2300
2289
  const entry = (providerId) => provider(providerId);
@@ -2330,9 +2319,10 @@ class AparteConfig {
2330
2319
  return void 0;
2331
2320
  }
2332
2321
  /**
2333
- * Refresh models for a specific provider
2334
- * Orchestrates: Key Retrieval -> Fetch -> Return
2335
- * This keeps UI components unaware of keys.
2322
+ * Fetch a provider's models, with the key and endpoint its key sources answer, so a
2323
+ * component that lists models never handles a key. The list is kept, and
2324
+ * `getCurrentModel()` reads the selected model's capabilities from it. Answers `[]` for an
2325
+ * unknown provider, one with no `fetchModels`, or a fetch that fails (with a console warning).
2336
2326
  */
2337
2327
  async refreshProviderModels(providerId) {
2338
2328
  const provider = this._aiProviders.get(providerId);
@@ -2351,7 +2341,13 @@ class AparteConfig {
2351
2341
  // AI Provider Management (BYORK - Bring Your Own Key)
2352
2342
  // ─────────────────────────────────────────────────────────────────────────
2353
2343
  /**
2354
- * Register one or more AI providers (e.g., OpenRouter, Gemini, Anthropic)
2344
+ * Register one or more AI providers (e.g., OpenRouter, Gemini, Anthropic).
2345
+ *
2346
+ * When this leaves exactly one provider registered, offering exactly one model it knows
2347
+ * without a request (a scripted or an in-browser provider), and no model is selected,
2348
+ * that model is selected. A provider that lists its models over the network selects
2349
+ * nothing: name one with {@link AparteConfig.setModelConfig | setModelConfig()}, or let the
2350
+ * person pick in `<aparte-model-selector>`.
2355
2351
  * @param providers AparteAIProvider implementations
2356
2352
  * @example
2357
2353
  * aparteGlobalConfig.registerAIProvider(OpenRouterProvider);
@@ -2469,7 +2465,8 @@ class AparteConfig {
2469
2465
  /**
2470
2466
  * Restore previously saved model preference via the registered provider.
2471
2467
  * Should be called once at app startup, before any component mounts.
2472
- * No-op if no provider is registered or nothing was saved.
2468
+ * No-op if no provider is registered or nothing was saved. The saved selection is
2469
+ * applied without being saved again.
2473
2470
  */
2474
2471
  restoreModelPreference() {
2475
2472
  if (!this._modelPreferenceProvider) return null;
@@ -2482,7 +2479,8 @@ class AparteConfig {
2482
2479
  return null;
2483
2480
  }
2484
2481
  /**
2485
- * Set model selection configuration
2482
+ * Select the default provider and model, merged over the current selection. With a
2483
+ * model-preference provider registered and both set, the selection is saved through it.
2486
2484
  */
2487
2485
  setModelConfig(config) {
2488
2486
  this._modelConfig = { ...this._modelConfig, ...config };
@@ -2526,12 +2524,6 @@ class AparteConfig {
2526
2524
  hasSelectedModel() {
2527
2525
  return !!(this._modelConfig.defaultProvider && this._modelConfig.defaultModel);
2528
2526
  }
2529
- /**
2530
- * Opt-in UX: when enabled, `<aparte-composer>` blocks sending and greys out
2531
- * until {@link hasSelectedModel} is true (e.g. while the model selector is
2532
- * still fetching its list). Off by default so single-model / backend setups
2533
- * that never select a model are unaffected.
2534
- */
2535
2527
  /**
2536
2528
  * Elicitation policy: whether a choice offers a free-text "Other…" escape.
2537
2529
  *
@@ -2590,6 +2582,12 @@ class AparteConfig {
2590
2582
  getElicitationFieldRenderer() {
2591
2583
  return this._elicitationFieldRenderer;
2592
2584
  }
2585
+ /**
2586
+ * Opt-in UX: when enabled, `<aparte-composer>` blocks sending and greys out
2587
+ * until {@link hasSelectedModel} is true (e.g. while the model selector is
2588
+ * still fetching its list). Off by default so single-model / backend setups
2589
+ * that never select a model are unaffected.
2590
+ */
2593
2591
  setRequireModelSelection(required) {
2594
2592
  if (this._requireModelSelection === required) return;
2595
2593
  this._requireModelSelection = required;
@@ -2625,7 +2623,8 @@ class AparteConfig {
2625
2623
  // Public Rendering Methods (with Fallbacks)
2626
2624
  // ─────────────────────────────────────────────────────────────────────────
2627
2625
  /**
2628
- * Get translated string by key
2626
+ * The locale's string for `key`, or the default (English) locale's when the locale
2627
+ * leaves it out; `''` for a key neither declares.
2629
2628
  */
2630
2629
  t(key) {
2631
2630
  const val = this._locale[key] || APARTE_DEFAULT_LOCALE[key];
@@ -2665,7 +2664,7 @@ class AparteConfig {
2665
2664
  }
2666
2665
  /**
2667
2666
  * Highlight code block
2668
- * Fallback: Returns raw code wrapped in <pre><code>
2667
+ * Fallback: Returns raw code wrapped in `<pre><code>`
2669
2668
  * Supports Promise if provider is async/streaming
2670
2669
  */
2671
2670
  async highlightCode(code, lang) {
@@ -2701,18 +2700,11 @@ class AparteConfig {
2701
2700
  return Array.from(this._tools.values()).map((e) => e.tool);
2702
2701
  }
2703
2702
  /**
2704
- * The registered tools' own `systemPrompt`s, joined, or `null` if none set one.
2705
- *
2706
- * `AparteTool.systemPrompt` has always been documented as "injected automatically when
2707
- * this tool is registered", on the type and in the tools guide — and nothing read it.
2708
- * A field that is documented and dead is worse than one that does not exist: a caller
2709
- * writes it, sees the tool work (the model has the schema either way), and never learns
2710
- * that the sentence explaining WHEN to reach for it was dropped. `@aparte/plugin-ask-user`
2711
- * sets one, so a shipped plugin was losing its instructions.
2712
- *
2713
- * Registration order, blank line between, because two tools' instructions are two
2714
- * paragraphs and not one run-on sentence. The app's own template stays a separate
2715
- * message — see `resolveSystemPrompt`, which is about the app and not about the tools.
2703
+ * The registered tools' own `systemPrompt`s, in registration order with a blank line
2704
+ * between, or `null` if none sets one. `AparteClient` sends them with every request, as a
2705
+ * system message after the app's own ({@link AparteConfig.resolveSystemPrompt | resolveSystemPrompt()});
2706
+ * a request you assemble yourself should send them the same way, or the model loses the
2707
+ * tools' instructions.
2716
2708
  */
2717
2709
  resolveToolSystemPrompts() {
2718
2710
  const prompts = Array.from(this._tools.values()).map((entry) => entry.tool.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
@@ -2876,6 +2868,12 @@ class AparteConfig {
2876
2868
  * confirmation, a text field, a form) and resolves with the user's answer.
2877
2869
  * The `<aparte-elicitation>` Web Component registers itself here; an app can
2878
2870
  * override with its own framework-native presenter. Pass `null` to clear.
2871
+ *
2872
+ * Several can be registered, one per chat: `owner` is an element of the chat the
2873
+ * presenter belongs to (`<aparte-elicitation>` passes itself). A request whose `target`
2874
+ * is inside that chat reaches its presenter; a request with no `target`, or from a chat
2875
+ * with no presenter of its own, reaches the most recently registered one. Registering a
2876
+ * presenter again makes it the most recent.
2879
2877
  */
2880
2878
  setElicitationPresenter(presenter, owner) {
2881
2879
  if (presenter === null) {
@@ -2927,35 +2925,21 @@ class AparteConfig {
2927
2925
  return stack[stack.length - 1]?.fn;
2928
2926
  }
2929
2927
  /**
2930
- * Ask the user for typed input mid-run and await their response. This is the
2931
- * generic primitive behind `ask_user` and tool approval — the KIND of
2932
- * question is the schema, not a bespoke tool.
2933
- *
2934
- * **Resolves** `accept` with the value, or `decline` when the user declines.
2935
- *
2936
- * **Rejects** — it does not resolve — when the request ends without an answer, with
2937
- * an {@link AparteElicitationAbortError} whose `name` is `'AbortError'` and whose
2938
- * `reason` is `'aborted'` (the turn was stopped, the signal fired, or another request
2939
- * took the question away) or `'no-presenter'` (nothing was mounted to ask it). So
2940
- * `await` it inside a `try`, or attach a `.catch()`.
2928
+ * Ask the person for typed input in the middle of a run, and wait for the answer. The
2929
+ * primitive behind `ask_user` and tool approval: the kind of question is its schema.
2941
2930
  *
2942
- * That is a change, and this block described the old behaviour for a whole release:
2943
- * "resolves `cancel` when the turn is cancelled", and "with NO presenter registered it
2944
- * resolves `cancel` rather than hanging". Both were false the moment `cancel` was
2945
- * removed — and the second is the one that bites, because a developer who reads
2946
- * "rather than hanging" writes no `.catch()` and gets an unhandled rejection. It ships
2947
- * in the `.d.ts` and in the custom-elements manifest, which is the API surface the
2948
- * docs site renders, so it was the most-read wrong sentence in the library.
2931
+ * **Resolves** `accept` with the value, or `decline` when the person declines.
2949
2932
  *
2950
- * A value was removed rather than renamed on purpose: `cancel` was easy to handle as
2951
- * though it were an answer, and the approval gate did exactly that — it read a stopped
2952
- * turn as the user refusing a tool they were never shown.
2933
+ * **Rejects**, with no answer, with an {@link AparteElicitationAbortError} (its `name` is
2934
+ * `'AbortError'`) whose `reason` is `'aborted'` — the turn was stopped, the signal fired,
2935
+ * or another request took the question away — or `'no-presenter'`, when nothing is mounted
2936
+ * to ask it (with a console warning, once). `await` it in a `try`, or attach a `.catch()`.
2953
2937
  *
2954
- * With no presenter it rejects `'no-presenter'` and warns once, because a request
2955
- * nobody could see is a setup mistake only the developer can fix. The default
2956
- * presenter is `<aparte-elicitation>`, which registers itself from its
2957
- * `connectedCallback` — so it has to be IN THE DOM. A docs page of ours claimed it
2958
- * "installs itself — nothing to register", which is how that was found.
2938
+ * The question goes to the presenter of the chat that holds `request.target` (see
2939
+ * {@link AparteConfig.setElicitationPresenter | setElicitationPresenter()});
2940
+ * `<aparte-elicitation>` is the default one, and registers itself once it is in the DOM.
2941
+ * A chat shows one question at a time: a second request in the same chat waits until the
2942
+ * first is answered or ends, and requests with no `target` share one queue.
2959
2943
  */
2960
2944
  requestUserInput(request) {
2961
2945
  const queue = this._elicitationQueueKey(request);
@@ -3000,10 +2984,13 @@ class AparteConfig {
3000
2984
  return presenter(request);
3001
2985
  }
3002
2986
  /**
3003
- * Reset ALL configuration back to defaults — providers, registries, model
3004
- * selection and bubble actions. Previously left `_aiProviders` / `_tools` /
3005
- * `_toolRenderers` / `_modelConfig` behind, which leaked across SPA
3006
- * navigations (registries only ever grew). Now a full reset.
2987
+ * Put the configuration back to its defaults: the providers and the selected model,
2988
+ * the tools with their renderers and summaries, the renderers and render hooks, the
2989
+ * Markdown and highlight providers, the sanitizer, the system prompt, the locale, icons,
2990
+ * avatars, actions and host handlers, the conversation manager, the question presenters
2991
+ * and their queue. Three things stay: the transport, the key sources added with
2992
+ * {@link AparteConfig.registerKeyProvider | registerKeyProvider()} (an `AparteClient`'s
2993
+ * `keyResolver` among them), and the {@link AparteConfig.subscribe | subscribe()} listeners.
3007
2994
  */
3008
2995
  reset() {
3009
2996
  this._markdownProvider = void 0;
@@ -3192,6 +3179,9 @@ const textRenderer = {
3192
3179
  getStyles: () => ``
3193
3180
  };
3194
3181
  const THINKING_ANCHOR_SLACK = 24;
3182
+ function labelOf(segment) {
3183
+ return segment.label && segment.label !== "Thinking" ? segment.label : contextConfig().t("thinking");
3184
+ }
3195
3185
  const thinkingRenderer = {
3196
3186
  type: "thinking",
3197
3187
  /*
@@ -3222,13 +3212,13 @@ const thinkingRenderer = {
3222
3212
  * only part that is genuinely about reasoning rather than about disclosure.
3223
3213
  * `.aparte-thinking-label` stays too: `relabel` below queries it.
3224
3214
  */
3225
- render: (segment) => `<details class="aparte-segment aparte-segment-thinking aparte-accordion__item" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed === false ? "open" : ""}><summary class="aparte-accordion__header aparte-thinking-header"><span class="aparte-thinking-label">${escapeHtml(segment.label || contextConfig().t("thinking"))}</span><span class="aparte-accordion__icon">${contextConfig().getIcon("expand")}</span></summary><div class="aparte-accordion__panel aparte-thinking-content" role="region" tabindex="0" aria-label="${escapeAttr(segment.label || contextConfig().t("thinking"))}">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
3215
+ render: (segment) => `<details class="aparte-segment aparte-segment-thinking aparte-accordion__item" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed === false ? "open" : ""}><summary class="aparte-accordion__header aparte-thinking-header"><span class="aparte-thinking-label">${escapeHtml(labelOf(segment))}</span><span class="aparte-accordion__icon">${contextConfig().getIcon("expand")}</span></summary><div class="aparte-accordion__panel aparte-thinking-content" role="region" tabindex="0" aria-label="${escapeAttr(labelOf(segment))}">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
3226
3216
  /**
3227
3217
  * The default label is the only config-derived text here — and `segment.label`
3228
3218
  * still wins, exactly as in `render`, because that string is the app's.
3229
3219
  */
3230
3220
  relabel: (el2, segment) => {
3231
- const text = segment.label || contextConfig().t("thinking");
3221
+ const text = labelOf(segment);
3232
3222
  const label = el2.querySelector(".aparte-thinking-label");
3233
3223
  if (label) label.textContent = text;
3234
3224
  el2.querySelector(".aparte-thinking-content")?.setAttribute("aria-label", text);
@@ -5063,6 +5053,14 @@ class AparteChatHost {
5063
5053
  }
5064
5054
  ac.signal.addEventListener("abort", () => resolve("aborted"), { once: true });
5065
5055
  });
5056
+ const { host, hostId } = this.binding;
5057
+ const onAbort = (e) => {
5058
+ const target = e.detail?.targetId;
5059
+ if ((!target || target === hostId) && this._streamAbort === ac) this.stopTokenStream();
5060
+ };
5061
+ window.addEventListener("aparte-abort", onAbort);
5062
+ const replaced = () => this._streamAbort !== void 0 && this._streamAbort !== ac;
5063
+ dispatchLifecycleEvent(host, "aparte-message-start", { messageId, role: "assistant" }, hostId);
5066
5064
  try {
5067
5065
  for (; ; ) {
5068
5066
  const res = await Promise.race([iterator.next(), aborted]);
@@ -5072,12 +5070,24 @@ class AparteChatHost {
5072
5070
  }
5073
5071
  this._flushStreamState();
5074
5072
  this._vp()?.completeMessage?.(messageId);
5075
- this._setStreamingId(null);
5073
+ if (!replaced()) {
5074
+ this._setStreamingId(null);
5075
+ dispatchLifecycleEvent(
5076
+ host,
5077
+ ac.signal.aborted ? "aparte-message-aborted" : "aparte-message-done",
5078
+ ac.signal.aborted ? { messageId } : { messageId, role: "assistant" },
5079
+ hostId
5080
+ );
5081
+ }
5076
5082
  } catch (err) {
5077
5083
  this._flushStreamState();
5078
- this._setStreamingId(null);
5084
+ if (!replaced()) {
5085
+ this._setStreamingId(null);
5086
+ dispatchLifecycleEvent(host, "aparte-message-error", { messageId, error: err }, hostId);
5087
+ }
5079
5088
  throw err;
5080
5089
  } finally {
5090
+ window.removeEventListener("aparte-abort", onAbort);
5081
5091
  if (ac.signal.aborted) {
5082
5092
  try {
5083
5093
  await iterator.return?.();
@@ -5814,6 +5824,7 @@ function el(tag, className, text) {
5814
5824
  if (text != null) node.textContent = text;
5815
5825
  return node;
5816
5826
  }
5827
+ let askSeq = 0;
5817
5828
  function buildApprovalPanel(message, options, onChange, details) {
5818
5829
  const cfg = contextConfig();
5819
5830
  const panel = el("div", "aparte-elic-panel aparte-approval-panel");
@@ -5843,6 +5854,10 @@ function buildApprovalPanel(message, options, onChange, details) {
5843
5854
  const row = el("div", "aparte-approval-options");
5844
5855
  row.setAttribute("role", "group");
5845
5856
  row.setAttribute("aria-label", cfg.t("approvalOptionsLabel"));
5857
+ if (ask) {
5858
+ ask.id ||= `aparte-approval-ask-${++askSeq}`;
5859
+ row.setAttribute("aria-describedby", ask.id);
5860
+ }
5846
5861
  const labelled = [];
5847
5862
  const textOf = (option) => typeof option.label === "function" ? option.label() : option.label;
5848
5863
  const descriptionOf = (option) => typeof option.description === "function" ? option.description() : option.description;
@@ -5943,6 +5958,11 @@ class AparteClient {
5943
5958
  options;
5944
5959
  /** Config read by this client — an instance config, or the global default. */
5945
5960
  _config;
5961
+ /**
5962
+ * A `keyResolver` becomes a key source on the client's config at once, before `start()`,
5963
+ * so everything that needs a key can use it — the model selector's list included.
5964
+ * `autoRegister` (on by default) installs core's built-in renderers on that config.
5965
+ */
5946
5966
  constructor(options = {}) {
5947
5967
  this.options = {
5948
5968
  autoRegister: true,
@@ -6004,7 +6024,9 @@ class AparteClient {
6004
6024
  };
6005
6025
  }
6006
6026
  /**
6007
- * Start listening for aparte-send events on the window.
6027
+ * Answer the chats: listen on `window` for `aparte-send`, `aparte-retry`, `aparte-edit`,
6028
+ * `aparte-abort` and `aparte-conversation-loaded`. Nothing streams before this is
6029
+ * called. Starting a stopped client puts its `keyResolver` back on the config.
6008
6030
  */
6009
6031
  start() {
6010
6032
  this._registerKeyResolver();
@@ -6047,7 +6069,8 @@ class AparteClient {
6047
6069
  window.addEventListener("aparte-conversation-loaded", this._boundLoadedHandler);
6048
6070
  }
6049
6071
  /**
6050
- * Stop listening.
6072
+ * Stop answering: abort the reply in flight, take this client's `keyResolver` off the
6073
+ * config, and remove the listeners.
6051
6074
  */
6052
6075
  stop() {
6053
6076
  this.abort();
@@ -7728,4 +7751,4 @@ export {
7728
7751
  collectRendererStyles as y,
7729
7752
  contentToText as z
7730
7753
  };
7731
- //# sourceMappingURL=dialog-triggers-CEazY76J.js.map
7754
+ //# sourceMappingURL=dialog-triggers-ChnPFG9M.js.map