@aparte/core 0.12.1 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/client/aparte-client.d.ts +25 -0
  2. package/dist/client/aparte-client.d.ts.map +1 -1
  3. package/dist/client/stream-adapter.d.ts.map +1 -1
  4. package/dist/components/bubble/aparte-chat-bubble.d.ts +117 -0
  5. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  6. package/dist/components/chat/aparte-chat.d.ts +70 -15
  7. package/dist/components/chat/aparte-chat.d.ts.map +1 -1
  8. package/dist/components/composer/aparte-composer-action.d.ts +48 -7
  9. package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
  10. package/dist/components/composer/aparte-composer-add-attachment.d.ts +37 -4
  11. package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
  12. package/dist/components/composer/aparte-composer-attachments.d.ts +35 -3
  13. package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
  14. package/dist/components/composer/aparte-composer-cancel.d.ts +25 -2
  15. package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
  16. package/dist/components/composer/aparte-composer-input.d.ts +54 -5
  17. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  18. package/dist/components/composer/aparte-composer-send.d.ts +47 -4
  19. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  20. package/dist/components/composer/aparte-composer-toolbar.d.ts +15 -4
  21. package/dist/components/composer/aparte-composer-toolbar.d.ts.map +1 -1
  22. package/dist/components/composer/aparte-composer.d.ts +139 -7
  23. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  24. package/dist/components/composer/index.d.ts +1 -1
  25. package/dist/components/composer/index.d.ts.map +1 -1
  26. package/dist/components/conversation-list/aparte-conversation-list.d.ts +50 -8
  27. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  28. package/dist/components/elicitation/aparte-elicitation.d.ts +45 -0
  29. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  30. package/dist/components/index.d.ts +1 -1
  31. package/dist/components/index.d.ts.map +1 -1
  32. package/dist/components/status/aparte-chat-status.d.ts +47 -5
  33. package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
  34. package/dist/components/viewport/aparte-chat-viewport.d.ts +80 -8
  35. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  36. package/dist/config/aparte-config.d.ts +22 -0
  37. package/dist/config/aparte-config.d.ts.map +1 -1
  38. package/dist/config/icon-provider.d.ts +17 -15
  39. package/dist/config/icon-provider.d.ts.map +1 -1
  40. package/dist/custom-elements.json +7372 -5219
  41. package/dist/elicitation/approval-panel.d.ts.map +1 -1
  42. package/dist/elicitation/panel.d.ts +23 -2
  43. package/dist/elicitation/panel.d.ts.map +1 -1
  44. package/dist/generated/element-attributes.d.ts +21 -15
  45. package/dist/generated/element-attributes.d.ts.map +1 -1
  46. package/dist/glyphs-QXfbL3n_.js +81 -0
  47. package/dist/glyphs-QXfbL3n_.js.map +1 -0
  48. package/dist/icons/extended.d.ts +153 -0
  49. package/dist/icons/extended.d.ts.map +1 -0
  50. package/dist/icons/glyphs.d.ts +98 -0
  51. package/dist/icons/glyphs.d.ts.map +1 -0
  52. package/dist/icons/index.d.ts +24 -0
  53. package/dist/icons/index.d.ts.map +1 -0
  54. package/dist/icons.d.ts +14 -0
  55. package/dist/icons.d.ts.map +1 -0
  56. package/dist/icons.js +157 -0
  57. package/dist/icons.js.map +1 -0
  58. package/dist/index.css +5109 -2121
  59. package/dist/index.d.ts +37 -3
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +184 -68
  62. package/dist/index.js.map +1 -1
  63. package/dist/index.node.d.ts +2 -2
  64. package/dist/index.node.d.ts.map +1 -1
  65. package/dist/index.node.js +1 -1
  66. package/dist/index.node.js.map +1 -1
  67. package/dist/{is-awaiting-reply-BWTDvQ__.js → is-awaiting-reply-CWp1ffHh.js} +347 -171
  68. package/dist/is-awaiting-reply-CWp1ffHh.js.map +1 -0
  69. package/dist/primitives/icon/aparte-icon.d.ts +49 -0
  70. package/dist/primitives/icon/aparte-icon.d.ts.map +1 -0
  71. package/dist/primitives/icon/index.d.ts +2 -0
  72. package/dist/primitives/icon/index.d.ts.map +1 -0
  73. package/dist/primitives/index.d.ts +1 -0
  74. package/dist/primitives/index.d.ts.map +1 -1
  75. package/dist/primitives/progress-spinner/aparte-progress-spinner.d.ts +20 -5
  76. package/dist/primitives/progress-spinner/aparte-progress-spinner.d.ts.map +1 -1
  77. package/dist/primitives/select/aparte-optgroup.d.ts +30 -7
  78. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  79. package/dist/primitives/select/aparte-option.d.ts +29 -3
  80. package/dist/primitives/select/aparte-option.d.ts.map +1 -1
  81. package/dist/primitives/select/aparte-select.d.ts +36 -11
  82. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  83. package/dist/renderers/segment-renderers.d.ts +18 -0
  84. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  85. package/dist/renderers/segments/artifact/card.d.ts.map +1 -1
  86. package/dist/renderers/segments/thinking.d.ts.map +1 -1
  87. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  88. package/dist/types/element-map.d.ts +2 -0
  89. package/dist/types/element-map.d.ts.map +1 -1
  90. package/dist/types/index.d.ts +1 -1
  91. package/dist/types/index.d.ts.map +1 -1
  92. package/dist/types/segments.d.ts +92 -7
  93. package/dist/types/segments.d.ts.map +1 -1
  94. package/dist/types/tools.d.ts +2 -0
  95. package/dist/types/tools.d.ts.map +1 -1
  96. package/package.json +7 -1
  97. package/dist/is-awaiting-reply-BWTDvQ__.js.map +0 -1
@@ -1,3 +1,4 @@
1
+ import { A as APARTE_ICON_GLYPHS } from "./glyphs-QXfbL3n_.js";
1
2
  function contentToText(content) {
2
3
  if (typeof content === "string") return content;
3
4
  return content.filter((p) => p.type === "text").map((p) => p.text).join("");
@@ -35,29 +36,7 @@ class AparteError extends Error {
35
36
  return new AparteError(message, defaultCode, void 0, error, status);
36
37
  }
37
38
  }
38
- const APARTE_DEFAULT_ICON_FALLBACKS = {
39
- copy: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
40
- check: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
41
- send: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>`,
42
- loading: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="aparte-icon-spin"><circle cx="12" cy="12" r="10" stroke-opacity="0.25"/><path d="M12 2a10 10 0 0 1 10 10"/></svg>`,
43
- error: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`,
44
- expand: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>`,
45
- collapse: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>`,
46
- terminal: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>`,
47
- paperclip: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>`,
48
- image: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`,
49
- file: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>`,
50
- scrollDown: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>`,
51
- retry: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>`,
52
- edit: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
53
- thumbUp: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>`,
54
- thumbDown: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>`,
55
- prevBranch: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>`,
56
- nextBranch: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>`,
57
- tool: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>`,
58
- close: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
59
- stop: `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><rect x="2" y="2" width="12" height="12" rx="2"/></svg>`
60
- };
39
+ const APARTE_DEFAULT_ICON_FALLBACKS = APARTE_ICON_GLYPHS;
61
40
  const APARTE_DEFAULT_LOCALE = {
62
41
  inputPlaceholder: "Type a message...",
63
42
  sendButton: "Send",
@@ -122,6 +101,14 @@ const APARTE_DEFAULT_LOCALE = {
122
101
  unarchiveConversation: "Unarchive conversation",
123
102
  direction: "ltr"
124
103
  };
104
+ const APARTE_DEFAULT_SKELETON_FALLBACKS = {
105
+ message: '<div class="aparte-skeleton-fallback">Loading...</div>',
106
+ code: '<div class="aparte-skeleton-fallback aparte-skeleton-fallback--code">Loading code...</div>',
107
+ thinking: '<div class="aparte-skeleton-fallback aparte-skeleton-fallback--tight">Thinking...</div>',
108
+ input: '<div class="aparte-skeleton-fallback aparte-skeleton-fallback--snug">...</div>',
109
+ list: '<div class="aparte-skeleton-fallback">Loading items...</div>',
110
+ text: '<div class="aparte-skeleton-fallback aparte-skeleton-fallback--tight">...</div>'
111
+ };
125
112
  function isFormatAdapter(p) {
126
113
  return typeof p.buildRequest === "function" && typeof p.parseStream === "function" && typeof p.defaultEndpoint === "string" && (typeof p.authHeaders === "function" || typeof p.authQuery === "function");
127
114
  }
@@ -1874,6 +1861,24 @@ class AparteConfig {
1874
1861
  getTools() {
1875
1862
  return Array.from(this._tools.values()).map((e) => e.tool);
1876
1863
  }
1864
+ /**
1865
+ * The registered tools' own `systemPrompt`s, joined, or `null` if none set one.
1866
+ *
1867
+ * `AparteTool.systemPrompt` has always been documented as "injected automatically when
1868
+ * this tool is registered", on the type and in the tools guide — and nothing read it.
1869
+ * A field that is documented and dead is worse than one that does not exist: a caller
1870
+ * writes it, sees the tool work (the model has the schema either way), and never learns
1871
+ * that the sentence explaining WHEN to reach for it was dropped. `@aparte/plugin-ask-user`
1872
+ * sets one, so a shipped plugin was losing its instructions.
1873
+ *
1874
+ * Registration order, blank line between, because two tools' instructions are two
1875
+ * paragraphs and not one run-on sentence. The app's own template stays a separate
1876
+ * message — see `resolveSystemPrompt`, which is about the app and not about the tools.
1877
+ */
1878
+ resolveToolSystemPrompts() {
1879
+ const prompts = Array.from(this._tools.values()).map((entry) => entry.tool.systemPrompt?.trim()).filter((prompt) => Boolean(prompt));
1880
+ return prompts.length ? prompts.join("\n\n") : null;
1881
+ }
1877
1882
  /** Get the handler for a tool by name */
1878
1883
  getToolHandler(name) {
1879
1884
  return this._tools.get(name)?.handler;
@@ -2123,16 +2128,15 @@ class AparteConfig {
2123
2128
  _defaultHighlightRenderer(code) {
2124
2129
  return `<pre><code>${escapeHtml(code)}</code></pre>`;
2125
2130
  }
2131
+ /**
2132
+ * ONE owner. This used to hold a second, hand-written copy of
2133
+ * `APARTE_DEFAULT_SKELETON_FALLBACKS`, and the two had already drifted — `message`
2134
+ * said "Loading message…" here and "Loading…" there, `text` the reverse. A duplicate
2135
+ * that disagrees with its original is worse than either: whichever one you read, you
2136
+ * cannot tell which one ships.
2137
+ */
2126
2138
  _defaultSkeletonRenderer(type) {
2127
- const fallbacks = {
2128
- message: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading message...</div>',
2129
- code: '<div class="aparte-skeleton-fallback" style="padding:16px;background:#1e293b;color:#64748b;border-radius:8px;">Loading code...</div>',
2130
- text: '<div class="aparte-skeleton-fallback" style="padding:8px;color:#9ca3af;">Loading...</div>',
2131
- thinking: '<div class="aparte-skeleton-fallback" style="padding:8px;color:#9ca3af;">Thinking...</div>',
2132
- input: '<div class="aparte-skeleton-fallback" style="padding:12px;color:#9ca3af;">...</div>',
2133
- list: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading items...</div>'
2134
- };
2135
- return fallbacks[type] || fallbacks.text;
2139
+ return APARTE_DEFAULT_SKELETON_FALLBACKS[type] ?? APARTE_DEFAULT_SKELETON_FALLBACKS.text;
2136
2140
  }
2137
2141
  }
2138
2142
  const GLOBAL_CONFIG_KEY = Symbol.for("aparte.globalConfig.1");
@@ -2196,14 +2200,6 @@ function subscribeConfigChange(el2, handler) {
2196
2200
  window.addEventListener(APARTE_CONFIG_CHANGE, listener);
2197
2201
  return () => window.removeEventListener(APARTE_CONFIG_CHANGE, listener);
2198
2202
  }
2199
- const APARTE_DEFAULT_SKELETON_FALLBACKS = {
2200
- message: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading...</div>',
2201
- code: '<div class="aparte-skeleton-fallback" style="padding:16px;background:#1e293b;color:#64748b;border-radius:8px;">Loading code...</div>',
2202
- thinking: '<div class="aparte-skeleton-fallback" style="padding:8px;color:#9ca3af;">Thinking...</div>',
2203
- input: '<div class="aparte-skeleton-fallback" style="padding:12px;color:#9ca3af;">...</div>',
2204
- list: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading items...</div>',
2205
- text: '<div class="aparte-skeleton-fallback" style="padding:8px;color:#9ca3af;">...</div>'
2206
- };
2207
2203
  function writeStreamedMarkdown(host, contentEl, content, streaming) {
2208
2204
  if (streaming) {
2209
2205
  if (host._aparteSmd === void 0) {
@@ -2276,7 +2272,21 @@ const thinkingRenderer = {
2276
2272
  * new class of risk. With no Markdown provider registered it degrades to the
2277
2273
  * zero-dependency default (escape + `<br>`), which is what it used to be.
2278
2274
  */
2279
- render: (segment) => `<details class="aparte-segment aparte-segment-thinking" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed === false ? "open" : ""}><summary class="aparte-thinking-header"><span class="aparte-thinking-label">${escapeHtml(segment.label || contextConfig().t("thinking"))}</span><span class="aparte-thinking-toggle"></span></summary><div class="aparte-thinking-content">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
2275
+ /*
2276
+ * WEARS THE ACCORDION RECIPE. A reasoning block is a disclosure — `<details>`, a
2277
+ * `<summary>` you press, a panel, a chevron that turns — which is exactly what
2278
+ * `surface/accordion.css` already draws, and this renderer used to draw a second
2279
+ * time under four classes of its own. Two costs, both visible: it looked unrelated
2280
+ * to every other disclosure in the library, and its chevron was the CHARACTER `▼`,
2281
+ * a fourth way to draw a chevron here and the only one that is not a glyph — so it
2282
+ * could not take `--aparte-icon-size`, could not be replaced through the icon
2283
+ * provider, and rendered in whatever the platform font felt like.
2284
+ *
2285
+ * What stays thinking's own is the left rail and the quieter tone, which is the
2286
+ * only part that is genuinely about reasoning rather than about disclosure.
2287
+ * `.aparte-thinking-label` stays too: `relabel` below queries it.
2288
+ */
2289
+ 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>${contextConfig().getIcon("expand")}</summary><div class="aparte-accordion__panel aparte-thinking-content">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
2280
2290
  /**
2281
2291
  * The default label is the only config-derived text here — and `segment.label`
2282
2292
  * still wins, exactly as in `render`, because that string is the app's.
@@ -2350,7 +2360,7 @@ const codeRenderer = {
2350
2360
  <div class="aparte-code-header">
2351
2361
  ${segment.filename ? `<span class="aparte-code-filename">${escapeHtml(segment.filename)}</span>` : `<span class="aparte-code-header-filler"></span>`}
2352
2362
  <span class="aparte-code-language">${escapeHtml(segment.language || "")}</span>
2353
- <button class="aparte-code-copy" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}">
2363
+ <button class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-code-copy" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}">
2354
2364
  ${contextConfig().getIcon("copy")}
2355
2365
  </button>
2356
2366
  </div>
@@ -2419,7 +2429,7 @@ const errorRenderer = {
2419
2429
  return out;
2420
2430
  }
2421
2431
  return `
2422
- <div class="aparte-segment aparte-segment-error" data-segment-id="${escapeHtml(segment.id)}">
2432
+ <div class="aparte-segment aparte-alert aparte-alert--danger aparte-segment-error" data-segment-id="${escapeHtml(segment.id)}">
2423
2433
  <div class="aparte-error-icon-wrapper">
2424
2434
  ${contextConfig().getIcon("error") || "⚠"}
2425
2435
  </div>
@@ -2518,7 +2528,7 @@ function pillMarkup(segment, name, status) {
2518
2528
  <span class="aparte-tool-icon">${contextConfig().getIcon("tool")}</span>
2519
2529
  <span class="aparte-tool-name">${escapeHtml(name)}</span>
2520
2530
  </span>
2521
- <span class="aparte-tool-spinner" aria-hidden="true"${status === "pending" ? "" : " hidden"}></span>
2531
+ <span class="aparte-spinner aparte-tool-spinner" aria-hidden="true"${status === "pending" ? "" : " hidden"}></span>
2522
2532
  <span class="aparte-tool-state">${badge}</span>`;
2523
2533
  }
2524
2534
  const toolCallRenderer = {
@@ -2529,10 +2539,12 @@ const toolCallRenderer = {
2529
2539
  const toolCallId = segment.toolCall?.id ?? "";
2530
2540
  const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
2531
2541
  if (customRenderer) {
2542
+ injectToolRendererStyles(segment.toolCall?.name ?? "", customRenderer);
2532
2543
  const out = customRenderer.render(segment);
2533
2544
  if (out) return out;
2534
2545
  }
2535
2546
  const pill = pillMarkup(segment, name, status);
2547
+ const toggle = contextConfig().getIcon("expand");
2536
2548
  const input = describeInput(segment);
2537
2549
  const result = segment.result;
2538
2550
  if (!input && !result) {
@@ -2544,7 +2556,7 @@ const toolCallRenderer = {
2544
2556
  const cfg = contextConfig();
2545
2557
  return `
2546
2558
  <details class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}">
2547
- <summary class="aparte-tool-summary"><span class="aparte-tool-toggle"></span>${pill}</summary>
2559
+ <summary class="aparte-tool-summary"><span class="aparte-tool-toggle">${toggle}</span>${pill}</summary>
2548
2560
  <div class="aparte-tool-detail">
2549
2561
  ${input ? `
2550
2562
  <div class="aparte-tool-part" data-part="input">
@@ -2619,7 +2631,7 @@ const toolCallRenderer = {
2619
2631
  const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
2620
2632
  customRenderer?.setup?.(element, segment);
2621
2633
  },
2622
- // CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
2634
+ // CSS lives in styles/segment/ — see that folder for why a
2623
2635
  // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
2624
2636
  // has no other way onto the page.
2625
2637
  getStyles: () => ""
@@ -2630,9 +2642,9 @@ const pipelineWaitingRenderer = {
2630
2642
  render: (segment) => {
2631
2643
  return `
2632
2644
  <div class="aparte-segment aparte-segment-pipeline-waiting" data-segment-id="${escapeHtml(segment.id)}" aria-label="${escapeAttr(waitingLabel())}" role="status">
2633
- <span class="aparte-pw-dot"></span>
2634
- <span class="aparte-pw-dot"></span>
2635
- <span class="aparte-pw-dot"></span>
2645
+ <span class="aparte-dot aparte-pw-dot"></span>
2646
+ <span class="aparte-dot aparte-pw-dot"></span>
2647
+ <span class="aparte-dot aparte-pw-dot"></span>
2636
2648
  </div>`;
2637
2649
  },
2638
2650
  update: () => {
@@ -2657,7 +2669,7 @@ const pipelineWaitingRenderer = {
2657
2669
  });
2658
2670
  observer.observe(parent, { childList: true });
2659
2671
  },
2660
- // CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
2672
+ // CSS lives in styles/segment/ — see that folder for why a
2661
2673
  // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
2662
2674
  // has no other way onto the page.
2663
2675
  getStyles: () => ""
@@ -2863,7 +2875,7 @@ function renderBinaryFileArtifact(segment, kind) {
2863
2875
  if (cached && !isStreaming) {
2864
2876
  const previewBody = cached.previewHtml ? contextConfig().sanitizeHtml(cached.previewHtml) : `<div class="aparte-art-file__preview-empty">Preview not available for ${escapeHtml(kind)} yet</div>`;
2865
2877
  return `
2866
- <div class="aparte-segment aparte-segment-artifact-file"
2878
+ <div class="aparte-segment aparte-card aparte-segment-artifact-file"
2867
2879
  data-segment-id="${escapeHtml(segment.id)}"
2868
2880
  data-artifact-type="${escapeHtml(kind)}"
2869
2881
  data-state="ready">
@@ -2874,7 +2886,7 @@ function renderBinaryFileArtifact(segment, kind) {
2874
2886
  <div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(formatBytes(cached.bytes))} · ${escapeHtml(kind.toUpperCase())}</div>
2875
2887
  </div>
2876
2888
  <div class="aparte-art-file__actions">
2877
- ${canRedownload ? `<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download">${downloadLabel}</button>` : ""}
2889
+ ${canRedownload ? `<button type="button" class="aparte-btn aparte-btn--primary aparte-btn--solid aparte-art-file__btn aparte-art-file__btn--primary" data-action="download">${downloadLabel}</button>` : ""}
2878
2890
  </div>
2879
2891
  </div>
2880
2892
  <div class="aparte-art-file__body">
@@ -2889,7 +2901,7 @@ function renderBinaryFileArtifact(segment, kind) {
2889
2901
  const cleanContent = stripCodeFences(segment.content || "");
2890
2902
  const subText = isStreaming ? contextConfig().t("generating") : contextConfig().t("rebuildingPreview");
2891
2903
  return `
2892
- <div class="aparte-segment aparte-segment-artifact-file"
2904
+ <div class="aparte-segment aparte-card aparte-segment-artifact-file"
2893
2905
  data-segment-id="${escapeHtml(segment.id)}"
2894
2906
  data-artifact-type="${escapeHtml(kind)}"
2895
2907
  data-state="${isStreaming ? "streaming" : "compiling"}">
@@ -2900,7 +2912,7 @@ function renderBinaryFileArtifact(segment, kind) {
2900
2912
  <div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(subText)}</div>
2901
2913
  </div>
2902
2914
  <div class="aparte-art-file__actions">
2903
- ${canRedownload ? `<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download" disabled>${downloadLabel}</button>` : ""}
2915
+ ${canRedownload ? `<button type="button" class="aparte-btn aparte-btn--primary aparte-btn--solid aparte-art-file__btn aparte-art-file__btn--primary" data-action="download" disabled>${downloadLabel}</button>` : ""}
2904
2916
  </div>
2905
2917
  </div>
2906
2918
  <div class="aparte-art-file__body">
@@ -3058,7 +3070,7 @@ function showSandboxError(element, phase, errorMsg) {
3058
3070
  body.innerHTML = `
3059
3071
  <div class="aparte-art-file__error">
3060
3072
  <div class="aparte-art-file__error-title">${escapeHtml(contextConfig().t("sandboxError"))}</div>
3061
- <div class="aparte-art-file__error-msg">${escapeHtml(short)}</div>
3073
+ <div class="aparte-output aparte-art-file__error-msg">${escapeHtml(short)}</div>
3062
3074
  <div class="aparte-art-file__error-hint">${escapeHtml(contextConfig().t("sandboxErrorHint"))}</div>
3063
3075
  </div>
3064
3076
  `;
@@ -3083,8 +3095,9 @@ const artifactRenderer = {
3083
3095
  const codeLabel = cfg.t("code");
3084
3096
  const isBinary = BINARY_FILE_KINDS.has(kind);
3085
3097
  const cleanContent = stripCodeFences(segment.content || "");
3098
+ const cardId = escapeAttr(segment.id);
3086
3099
  return `
3087
- <div class="aparte-segment aparte-segment-artifact-card"
3100
+ <div class="aparte-segment aparte-card aparte-segment-artifact-card"
3088
3101
  data-segment-id="${escapeHtml(segment.id)}"
3089
3102
  data-artifact-type="${escapeHtml(kind)}"
3090
3103
  data-streaming="${isStreaming ? "true" : "false"}"
@@ -3093,31 +3106,31 @@ const artifactRenderer = {
3093
3106
  data-binary="${isBinary ? "true" : "false"}">
3094
3107
  <header class="aparte-art-card__header">
3095
3108
  <div class="aparte-art-card__title-block">
3096
- <span class="aparte-art-card__kind" data-kind="${escapeHtml(kind)}">${escapeHtml(displayLang)}</span>
3109
+ <span class="aparte-badge aparte-badge--outline aparte-art-card__kind" data-kind="${escapeHtml(kind)}">${escapeHtml(displayLang)}</span>
3097
3110
  <span class="aparte-art-card__title">${escapeHtml(title)}</span>
3098
- ${isStreaming ? '<span class="aparte-art-card__pulse" aria-label="Streaming"></span>' : ""}
3111
+ ${isStreaming ? `<span class="aparte-dot aparte-art-card__pulse" role="img" aria-label="${escapeAttr(cfg.t("generating"))}"></span>` : ""}
3099
3112
  </div>
3100
3113
  <div class="aparte-art-card__actions">
3101
- <button type="button" class="aparte-art-card__btn" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}" aria-label="${escapeAttr(contextConfig().t("copy"))}">
3114
+ <button type="button" class="aparte-btn aparte-btn--icon aparte-art-card__btn" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}" aria-label="${escapeAttr(contextConfig().t("copy"))}">
3102
3115
  ${contextConfig().getIcon("copy")}
3103
3116
  </button>
3104
- <button type="button" class="aparte-art-card__btn" data-action="download" title="${escapeAttr(downloadLabel)}" aria-label="${escapeAttr(downloadLabel)}" ${isStreaming ? "disabled" : ""}>
3105
- <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v9m0 0l-3-3m3 3l3-3M2 13h12"/></svg>
3117
+ <button type="button" class="aparte-btn aparte-btn--icon aparte-art-card__btn" data-action="download" title="${escapeAttr(downloadLabel)}" aria-label="${escapeAttr(downloadLabel)}" ${isStreaming ? "disabled" : ""}>
3118
+ ${contextConfig().getIcon("download")}
3106
3119
  </button>
3107
3120
  </div>
3108
3121
  </header>
3109
- <nav class="aparte-art-card__tabs" role="tablist">
3110
- <button type="button" role="tab" data-tab-target="code" aria-selected="true">${escapeHtml(codeLabel)}</button>
3111
- ${previewable ? `<button type="button" role="tab" data-tab-target="preview" aria-selected="false" ${isStreaming ? "disabled" : ""}>${escapeHtml(previewLabel)}</button>` : ""}
3122
+ <nav class="aparte-tabs aparte-tabs--underline aparte-art-card__tabs" role="tablist">
3123
+ <button type="button" class="aparte-tabs__tab" role="tab" id="aparte-art-${cardId}-tab-code" aria-controls="aparte-art-${cardId}-pane-code" aria-selected="true" tabindex="0" data-tab-target="code">${escapeHtml(codeLabel)}</button>
3124
+ ${previewable ? `<button type="button" class="aparte-tabs__tab" role="tab" id="aparte-art-${cardId}-tab-preview" aria-controls="aparte-art-${cardId}-pane-preview" aria-selected="false" tabindex="-1" data-tab-target="preview" ${isStreaming ? "disabled" : ""}>${escapeHtml(previewLabel)}</button>` : ""}
3112
3125
  </nav>
3113
3126
  <div class="aparte-art-card__body">
3114
- <div class="aparte-art-card__pane" data-pane="code">
3127
+ <div class="aparte-art-card__pane" role="tabpanel" id="aparte-art-${cardId}-pane-code" aria-labelledby="aparte-art-${cardId}-tab-code" tabindex="0" data-pane="code">
3115
3128
  <div class="aparte-code-content-wrapper">
3116
3129
  <pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>
3117
3130
  </div>
3118
3131
  </div>
3119
3132
  ${previewable ? `
3120
- <div class="aparte-art-card__pane" data-pane="preview">
3133
+ <div class="aparte-art-card__pane" role="tabpanel" id="aparte-art-${cardId}-pane-preview" aria-labelledby="aparte-art-${cardId}-tab-preview" tabindex="0" data-pane="preview">
3121
3134
  <div class="aparte-art-card__pending">${escapeHtml(cfg.t("previewPending"))}</div>
3122
3135
  </div>
3123
3136
  ` : ""}
@@ -3172,17 +3185,38 @@ const artifactRenderer = {
3172
3185
  }).catch(() => {
3173
3186
  });
3174
3187
  }
3175
- element.querySelectorAll("[data-tab-target]").forEach((btn) => {
3176
- btn.addEventListener("click", () => {
3177
- const target = btn.getAttribute("data-tab-target");
3178
- if (!target) return;
3179
- if (target === "preview") mountPreviewFrame(element, segment);
3180
- element.setAttribute("data-tab", target);
3181
- element.querySelectorAll("[data-tab-target]").forEach((b) => {
3182
- b.setAttribute("aria-selected", b === btn ? "true" : "false");
3183
- });
3188
+ const tabs = [...element.querySelectorAll("[data-tab-target]")];
3189
+ const select = (btn, focus) => {
3190
+ const target = btn.getAttribute("data-tab-target");
3191
+ if (!target || btn.disabled) return;
3192
+ if (target === "preview") mountPreviewFrame(element, segment);
3193
+ element.setAttribute("data-tab", target);
3194
+ for (const b of tabs) {
3195
+ const on = b === btn;
3196
+ b.setAttribute("aria-selected", on ? "true" : "false");
3197
+ b.tabIndex = on ? 0 : -1;
3198
+ }
3199
+ if (focus) btn.focus();
3200
+ };
3201
+ for (const [i, btn] of tabs.entries()) {
3202
+ btn.addEventListener("click", () => select(btn, false));
3203
+ btn.addEventListener("keydown", (e) => {
3204
+ const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
3205
+ let next = -1;
3206
+ if (step) next = (i + step + tabs.length) % tabs.length;
3207
+ else if (e.key === "Home") next = 0;
3208
+ else if (e.key === "End") next = tabs.length - 1;
3209
+ if (next < 0) return;
3210
+ e.preventDefault();
3211
+ for (let n = 0; n < tabs.length; n++) {
3212
+ const candidate = tabs[(next + n * (step || 1) + tabs.length) % tabs.length];
3213
+ if (candidate && !candidate.disabled) {
3214
+ select(candidate, true);
3215
+ return;
3216
+ }
3217
+ }
3184
3218
  });
3185
- });
3219
+ }
3186
3220
  const copyBtn = element.querySelector('[data-action="copy"]');
3187
3221
  if (copyBtn) {
3188
3222
  copyBtn.addEventListener("click", () => {
@@ -3267,7 +3301,7 @@ const artifactRenderer = {
3267
3301
  });
3268
3302
  }
3269
3303
  },
3270
- // CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
3304
+ // CSS lives in styles/segment/ — see that folder for why a
3271
3305
  // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
3272
3306
  // has no other way onto the page.
3273
3307
  getStyles: () => ""
@@ -3407,6 +3441,17 @@ function injectRendererStyles(config = contextConfig()) {
3407
3441
  }
3408
3442
  if (added) styleElement.textContent = Array.from(injectedRendererStyles).join("\n");
3409
3443
  }
3444
+ function injectToolRendererStyles(toolName, renderer) {
3445
+ if (typeof document === "undefined") return;
3446
+ const css = renderer.getStyles?.();
3447
+ if (!css) return;
3448
+ const id = `aparte-tool-renderer-${toolName}`;
3449
+ if (document.getElementById(id)) return;
3450
+ const el2 = document.createElement("style");
3451
+ el2.id = id;
3452
+ el2.textContent = css;
3453
+ document.head.appendChild(el2);
3454
+ }
3410
3455
  function registerDefaultRenderers(config = contextConfig()) {
3411
3456
  const reg = registryFor(config);
3412
3457
  reg.defaultsInstalled = true;
@@ -5170,18 +5215,7 @@ function createStreamAdapter(opts) {
5170
5215
  };
5171
5216
  const renderer = config.getToolRenderer(e.name);
5172
5217
  if (renderer) {
5173
- if (renderer.getStyles) {
5174
- const styles = renderer.getStyles();
5175
- if (styles) {
5176
- const styleId = `aparte-tool-renderer-${e.name}`;
5177
- if (!document.getElementById(styleId)) {
5178
- const styleEl = document.createElement("style");
5179
- styleEl.id = styleId;
5180
- styleEl.textContent = styles;
5181
- document.head.appendChild(styleEl);
5182
- }
5183
- }
5184
- }
5218
+ injectToolRendererStyles(e.name, renderer);
5185
5219
  const html = renderer.render(toolSeg);
5186
5220
  if (html) target.addSegment?.(toolSeg);
5187
5221
  } else {
@@ -5269,6 +5303,7 @@ function readableToAsyncIterable(stream, signal) {
5269
5303
  }
5270
5304
  };
5271
5305
  }
5306
+ let steppedPanelSeq = 0;
5272
5307
  let _relabels = null;
5273
5308
  function onRelabel(fn) {
5274
5309
  _relabels?.push(fn);
@@ -5279,6 +5314,18 @@ function el$1(tag, className, text) {
5279
5314
  if (text != null) node.textContent = text;
5280
5315
  return node;
5281
5316
  }
5317
+ function optionBody(label, description) {
5318
+ const body = el$1("span", "aparte-elic-option-body");
5319
+ body.appendChild(el$1("span", "aparte-elic-option-title", label));
5320
+ if (description) body.appendChild(el$1("span", "aparte-elic-option-desc", description));
5321
+ return body;
5322
+ }
5323
+ function commandOption(label, description, recommended) {
5324
+ const button = el$1("button", "aparte-btn aparte-btn--block aparte-btn--surface aparte-elic-option aparte-elic-option--command" + (recommended ? " aparte-elic-option--recommended" : ""));
5325
+ button.type = "button";
5326
+ button.appendChild(optionBody(label, description));
5327
+ return button;
5328
+ }
5282
5329
  function fieldHeader(parent, field, skipTitle) {
5283
5330
  let titleId;
5284
5331
  if (field.title && field.title.trim() !== skipTitle?.trim()) {
@@ -5291,61 +5338,87 @@ function fieldHeader(parent, field, skipTitle) {
5291
5338
  return titleId;
5292
5339
  }
5293
5340
  function labelGroup(list, opts) {
5294
- list.setAttribute("role", opts.multiple ? "group" : "radiogroup");
5341
+ list.setAttribute("role", opts.multiple || opts.commands ? "group" : "radiogroup");
5295
5342
  if (opts.titleId) list.setAttribute("aria-labelledby", opts.titleId);
5296
5343
  else if (opts.fallbackLabel) list.setAttribute("aria-label", opts.fallbackLabel);
5297
5344
  }
5298
- function buildEnumField(field, onChange, fallbackLabel) {
5345
+ function buildEnumField(field, onChange, fallbackLabel, settle) {
5299
5346
  const wrap = el$1("div", "aparte-elic-field aparte-elic-enum");
5300
5347
  const titleId = fieldHeader(wrap, field, fallbackLabel);
5301
5348
  const list = el$1("div", "aparte-elic-options");
5302
- labelGroup(list, { multiple: field.multiple, titleId, fallbackLabel });
5303
5349
  const name = `elic-${uuid()}`;
5304
5350
  const type = field.multiple ? "checkbox" : "radio";
5351
+ const controlRecipe = type === "checkbox" ? "aparte-checkbox" : "aparte-radio";
5305
5352
  const allowOther = field.allowOther ?? contextConfig().getElicitationOptions().allowOther;
5306
5353
  const defaults = new Set(Array.isArray(field.default) ? field.default : field.default != null ? [field.default] : []);
5354
+ const settling = settle != null && !field.multiple && defaults.size === 0;
5355
+ labelGroup(list, { multiple: field.multiple, commands: settling, titleId, fallbackLabel });
5307
5356
  const buildOption = (value, label, description, recommended) => {
5308
- const row = el$1("label", "aparte-elic-option" + (recommended ? " aparte-elic-option--recommended" : ""));
5309
- const control = el$1("input", "aparte-elic-control");
5357
+ if (settling) {
5358
+ const button = commandOption(label, description, recommended);
5359
+ button.addEventListener("click", () => settle?.(value));
5360
+ return button;
5361
+ }
5362
+ const row = el$1("label", "aparte-field-choice aparte-elic-option" + (recommended ? " aparte-elic-option--recommended" : ""));
5363
+ const control = el$1("input", controlRecipe + " aparte-elic-control");
5310
5364
  control.type = type;
5311
5365
  control.name = name;
5312
5366
  control.value = value;
5313
5367
  if (defaults.has(value)) control.checked = true;
5314
- const body = el$1("span", "aparte-elic-option-body");
5315
- body.appendChild(el$1("span", "aparte-elic-option-title", label));
5316
- if (description) body.appendChild(el$1("span", "aparte-elic-option-desc", description));
5317
- row.append(control, body);
5368
+ row.append(control, optionBody(label, description));
5318
5369
  return row;
5319
5370
  };
5320
5371
  for (const opt of field.options) {
5321
5372
  list.appendChild(buildOption(opt.value, opt.label ?? opt.value, opt.description, opt.recommended));
5322
5373
  }
5323
5374
  let otherText = null;
5375
+ let otherOpen = false;
5324
5376
  if (allowOther) {
5325
- const row = el$1("label", "aparte-elic-option aparte-elic-option--other");
5326
- const control = el$1("input", "aparte-elic-control");
5327
- control.type = type;
5328
- control.name = name;
5329
- control.value = "__other__";
5330
- const body = el$1("span", "aparte-elic-option-body");
5331
5377
  const t = contextConfig();
5332
- const otherTitle = el$1("span", "aparte-elic-option-title", t.t("elicitationOther"));
5333
- body.appendChild(otherTitle);
5334
- otherText = el$1("input", "aparte-elic-other-input");
5378
+ otherText = el$1("input", "aparte-field aparte-elic-other-input");
5335
5379
  otherText.type = "text";
5336
5380
  otherText.placeholder = t.t("elicitationOtherPlaceholder");
5337
5381
  otherText.style.display = "none";
5338
5382
  otherText.setAttribute("aria-label", t.t("elicitationOtherLabel"));
5339
5383
  const otherInput = otherText;
5340
- onRelabel(() => {
5341
- const now = contextConfig();
5342
- otherTitle.textContent = now.t("elicitationOther");
5343
- otherInput.placeholder = now.t("elicitationOtherPlaceholder");
5344
- otherInput.setAttribute("aria-label", now.t("elicitationOtherLabel"));
5345
- });
5346
- body.appendChild(otherText);
5347
- row.append(control, body);
5348
- list.appendChild(row);
5384
+ if (settling) {
5385
+ const button = commandOption(t.t("elicitationOther"));
5386
+ button.classList.add("aparte-elic-option--other");
5387
+ button.setAttribute("aria-expanded", "false");
5388
+ button.addEventListener("click", () => {
5389
+ otherOpen = true;
5390
+ button.setAttribute("aria-expanded", "true");
5391
+ otherInput.style.display = "";
5392
+ otherInput.focus();
5393
+ onChange();
5394
+ });
5395
+ const otherTitle = button.querySelector(".aparte-elic-option-title");
5396
+ onRelabel(() => {
5397
+ const now = contextConfig();
5398
+ otherTitle.textContent = now.t("elicitationOther");
5399
+ otherInput.placeholder = now.t("elicitationOtherPlaceholder");
5400
+ otherInput.setAttribute("aria-label", now.t("elicitationOtherLabel"));
5401
+ });
5402
+ list.append(button, otherInput);
5403
+ } else {
5404
+ const row = el$1("label", "aparte-field-choice aparte-elic-option aparte-elic-option--other");
5405
+ const control = el$1("input", controlRecipe + " aparte-elic-control");
5406
+ control.type = type;
5407
+ control.name = name;
5408
+ control.value = "__other__";
5409
+ const body = el$1("span", "aparte-elic-option-body");
5410
+ const otherTitle = el$1("span", "aparte-elic-option-title", t.t("elicitationOther"));
5411
+ body.appendChild(otherTitle);
5412
+ onRelabel(() => {
5413
+ const now = contextConfig();
5414
+ otherTitle.textContent = now.t("elicitationOther");
5415
+ otherInput.placeholder = now.t("elicitationOtherPlaceholder");
5416
+ otherInput.setAttribute("aria-label", now.t("elicitationOtherLabel"));
5417
+ });
5418
+ body.appendChild(otherText);
5419
+ row.append(control, body);
5420
+ list.appendChild(row);
5421
+ }
5349
5422
  }
5350
5423
  wrap.appendChild(list);
5351
5424
  const controls = () => Array.from(list.querySelectorAll(".aparte-elic-control"));
@@ -5359,8 +5432,18 @@ function buildEnumField(field, onChange, fallbackLabel) {
5359
5432
  onChange();
5360
5433
  });
5361
5434
  otherText?.addEventListener("input", onChange);
5435
+ const typedOther = () => otherText?.value.trim() ?? "";
5436
+ if (settling) {
5437
+ return {
5438
+ el: wrap,
5439
+ getValue: () => typedOther(),
5440
+ isComplete: () => otherOpen && typedOther() !== "",
5441
+ focus: () => list.querySelector(".aparte-elic-option--command")?.focus(),
5442
+ offersSubmit: () => otherOpen
5443
+ };
5444
+ }
5362
5445
  const getValue = () => {
5363
- const otherVal = other()?.checked && otherText?.value.trim() ? otherText.value.trim() : "";
5446
+ const otherVal = other()?.checked && typedOther() ? typedOther() : "";
5364
5447
  if (field.multiple) {
5365
5448
  const vals = controls().filter((c) => c.checked && c.value !== "__other__").map((c) => c.value);
5366
5449
  if (otherVal) vals.push(otherVal);
@@ -5380,15 +5463,42 @@ function buildEnumField(field, onChange, fallbackLabel) {
5380
5463
  focus: () => controls()[0]?.focus()
5381
5464
  };
5382
5465
  }
5383
- function buildBooleanField(field, onChange, fallbackLabel) {
5466
+ function buildBooleanField(field, onChange, fallbackLabel, settle) {
5384
5467
  const wrap = el$1("div", "aparte-elic-field aparte-elic-boolean");
5385
5468
  const titleId = fieldHeader(wrap, field, fallbackLabel);
5386
5469
  const list = el$1("div", "aparte-elic-options");
5387
- labelGroup(list, { titleId, fallbackLabel });
5388
5470
  const name = `elic-${uuid()}`;
5471
+ const settling = settle != null && field.default == null;
5472
+ labelGroup(list, { commands: settling, titleId, fallbackLabel });
5473
+ if (settling) {
5474
+ const t2 = contextConfig();
5475
+ const mkCommand = (val, label, defaulted) => {
5476
+ const button = commandOption(label);
5477
+ button.addEventListener("click", () => settle?.(val));
5478
+ if (defaulted) {
5479
+ const title = button.querySelector(".aparte-elic-option-title");
5480
+ onRelabel(() => {
5481
+ title.textContent = contextConfig().t(defaulted);
5482
+ });
5483
+ }
5484
+ list.appendChild(button);
5485
+ };
5486
+ mkCommand(true, field.trueLabel ?? t2.t("elicitationYes"), field.trueLabel ? void 0 : "elicitationYes");
5487
+ mkCommand(false, field.falseLabel ?? t2.t("elicitationNo"), field.falseLabel ? void 0 : "elicitationNo");
5488
+ wrap.appendChild(list);
5489
+ return {
5490
+ el: wrap,
5491
+ // Never read: the click settles, so nothing downstream asks this shape for
5492
+ // a value. `false` rather than a throw keeps the field total.
5493
+ getValue: () => false,
5494
+ isComplete: () => false,
5495
+ focus: () => list.querySelector(".aparte-elic-option--command")?.focus(),
5496
+ offersSubmit: () => false
5497
+ };
5498
+ }
5389
5499
  const mk = (val, label, defaulted) => {
5390
- const row = el$1("label", "aparte-elic-option");
5391
- const control = el$1("input", "aparte-elic-control");
5500
+ const row = el$1("label", "aparte-field-choice aparte-elic-option");
5501
+ const control = el$1("input", "aparte-radio aparte-elic-control");
5392
5502
  control.type = "radio";
5393
5503
  control.name = name;
5394
5504
  control.value = val;
@@ -5418,7 +5528,7 @@ function buildBooleanField(field, onChange, fallbackLabel) {
5418
5528
  function buildStringField(field, onChange, fallbackLabel) {
5419
5529
  const wrap = el$1("div", "aparte-elic-field aparte-elic-string");
5420
5530
  fieldHeader(wrap, field, fallbackLabel);
5421
- const input = field.multiline ? el$1("textarea", "aparte-elic-text") : el$1("input", "aparte-elic-text");
5531
+ const input = field.multiline ? el$1("textarea", "aparte-field aparte-field--textarea aparte-elic-text") : el$1("input", "aparte-field aparte-elic-text");
5422
5532
  if (!field.multiline) input.type = "text";
5423
5533
  if (field.placeholder) input.setAttribute("placeholder", field.placeholder);
5424
5534
  if (field.default) input.value = field.default;
@@ -5446,7 +5556,7 @@ function buildStringField(field, onChange, fallbackLabel) {
5446
5556
  focus: () => input.focus()
5447
5557
  };
5448
5558
  }
5449
- function buildField(field, onChange, fallbackLabel, key) {
5559
+ function buildField(field, onChange, fallbackLabel, key, settle) {
5450
5560
  const custom = contextConfig().getElicitationFieldRenderer()?.(field, { key, notifyChange: onChange });
5451
5561
  if (custom) {
5452
5562
  return {
@@ -5458,9 +5568,10 @@ function buildField(field, onChange, fallbackLabel, key) {
5458
5568
  }
5459
5569
  switch (field.type) {
5460
5570
  case "enum":
5461
- return buildEnumField(field, onChange, fallbackLabel);
5571
+ return buildEnumField(field, onChange, fallbackLabel, settle);
5462
5572
  case "boolean":
5463
- return buildBooleanField(field, onChange, fallbackLabel);
5573
+ return buildBooleanField(field, onChange, fallbackLabel, settle);
5574
+ // No `settle`: text is written, not chosen, so it always needs a commit.
5464
5575
  case "string":
5465
5576
  return buildStringField(field, onChange, fallbackLabel);
5466
5577
  }
@@ -5482,6 +5593,16 @@ function buildPanel(message, schema, onChange) {
5482
5593
  const panel = el$1("div", "aparte-elic-panel");
5483
5594
  const asked = typeof message === "function" ? message() : message;
5484
5595
  if (asked) panel.appendChild(el$1("p", "aparte-elic-message", asked));
5596
+ let settleCb = null;
5597
+ let settled = false;
5598
+ const settle = (content) => {
5599
+ if (settled) return;
5600
+ settled = true;
5601
+ settleCb?.(content);
5602
+ };
5603
+ const onSettle = (cb) => {
5604
+ settleCb = cb;
5605
+ };
5485
5606
  const body = el$1("div", "aparte-elic-body");
5486
5607
  panel.appendChild(body);
5487
5608
  const dismiss = el$1("div", "aparte-elic-dismiss");
@@ -5522,15 +5643,20 @@ function buildPanel(message, schema, onChange) {
5522
5643
  mode: () => "submit",
5523
5644
  canProceed: isComplete,
5524
5645
  proceed: () => {
5525
- }
5646
+ },
5647
+ // A form never settles from inside: its questions are collected and
5648
+ // submitted together, and answering the last one on the click would be
5649
+ // WCAG's F36 word for word. Registered so the shape is total; never fired.
5650
+ onSettle
5526
5651
  };
5527
5652
  if (fields.length < 2 || cfg.getElicitationOptions().layout !== "stepped") return api;
5528
5653
  panel.classList.add("aparte-elic-panel--stepped");
5529
5654
  let current = 0;
5530
- const steps = el$1("div", "aparte-elic-steps");
5655
+ const scope = `aparte-elic-${++steppedPanelSeq}`;
5656
+ const steps = el$1("div", "aparte-tabs aparte-tabs--underline aparte-elic-steps");
5531
5657
  steps.setAttribute("role", "tablist");
5532
5658
  const chips = fields.map((f, i) => {
5533
- const chip = el$1("button", "aparte-elic-step");
5659
+ const chip = el$1("button", "aparte-tabs__tab aparte-elic-step");
5534
5660
  chip.type = "button";
5535
5661
  chip.setAttribute("role", "tab");
5536
5662
  chip.textContent = f.header?.trim() || String(i + 1);
@@ -5538,6 +5664,22 @@ function buildPanel(message, schema, onChange) {
5538
5664
  show(i);
5539
5665
  f.field.focus();
5540
5666
  });
5667
+ chip.id = `${scope}-tab-${i}`;
5668
+ f.field.el.id = `${scope}-panel-${i}`;
5669
+ f.field.el.setAttribute("role", "tabpanel");
5670
+ f.field.el.setAttribute("aria-labelledby", chip.id);
5671
+ chip.setAttribute("aria-controls", f.field.el.id);
5672
+ chip.addEventListener("keydown", (e) => {
5673
+ const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
5674
+ let next = -1;
5675
+ if (step) next = (i + step + fields.length) % fields.length;
5676
+ else if (e.key === "Home") next = 0;
5677
+ else if (e.key === "End") next = fields.length - 1;
5678
+ if (next < 0) return;
5679
+ e.preventDefault();
5680
+ show(next);
5681
+ chips[next]?.focus();
5682
+ });
5541
5683
  steps.appendChild(chip);
5542
5684
  return chip;
5543
5685
  });
@@ -5545,6 +5687,7 @@ function buildPanel(message, schema, onChange) {
5545
5687
  syncNav = () => {
5546
5688
  chips.forEach((chip, i) => {
5547
5689
  chip.setAttribute("aria-selected", String(i === current));
5690
+ chip.tabIndex = i === current ? 0 : -1;
5548
5691
  chip.toggleAttribute("data-answered", fields[i].field.isComplete());
5549
5692
  });
5550
5693
  };
@@ -5565,7 +5708,7 @@ function buildPanel(message, schema, onChange) {
5565
5708
  }
5566
5709
  };
5567
5710
  }
5568
- const field = buildField(schema, onChange, asked);
5711
+ const field = buildField(schema, onChange, asked, void 0, settle);
5569
5712
  body.appendChild(field.el);
5570
5713
  return {
5571
5714
  el: panel,
@@ -5573,10 +5716,11 @@ function buildPanel(message, schema, onChange) {
5573
5716
  getContent: () => field.getValue(),
5574
5717
  isComplete: () => field.isComplete(),
5575
5718
  focus: () => field.focus(),
5576
- mode: () => "submit",
5719
+ mode: () => field.offersSubmit?.() ?? true ? "submit" : "none",
5577
5720
  canProceed: () => field.isComplete(),
5578
5721
  proceed: () => {
5579
- }
5722
+ },
5723
+ onSettle
5580
5724
  };
5581
5725
  }
5582
5726
  function el(tag, className, text) {
@@ -5606,7 +5750,8 @@ function buildApprovalPanel(message, options, onChange) {
5606
5750
  const labelled = [];
5607
5751
  const textOf = (option) => typeof option.label === "function" ? option.label() : option.label;
5608
5752
  for (const option of options) {
5609
- const button = el("button", `aparte-approval-option aparte-approval-option--${option.tone ?? "affirm"}`);
5753
+ const tone = option.tone ?? "affirm";
5754
+ const button = el("button", `aparte-btn aparte-btn--block aparte-btn--surface aparte-approval-option aparte-approval-option--${tone}`);
5610
5755
  button.type = "button";
5611
5756
  button.textContent = textOf(option);
5612
5757
  button.addEventListener("click", () => fire({ option: option.value }));
@@ -5710,6 +5855,27 @@ class AparteClient {
5710
5855
  }
5711
5856
  this._setupListeners();
5712
5857
  }
5858
+ /**
5859
+ * What every fresh user action does before it starts a turn.
5860
+ *
5861
+ * Clearing the abort flag has to happen at the START of the action rather than inside
5862
+ * `_streamTurn`, which runs after the auth and file-reading awaits — a reset there
5863
+ * erased an abort that arrived while the user was still waiting for a large attachment
5864
+ * to be read.
5865
+ *
5866
+ * Cancelling the previous turn's tool controllers used to happen only on `aparte-send`.
5867
+ * Retry and edit reset the flag and left them running, so a tool handler from the turn
5868
+ * being superseded stayed alive — with its timeout still counting — while its reply was
5869
+ * already off the active path. A cold audit found the asymmetry; three callers, one
5870
+ * rule, no third copy to fall out of step.
5871
+ */
5872
+ _beginUserTurn() {
5873
+ this._isAborted = false;
5874
+ for (const controller of this._activeToolControllers) {
5875
+ controller.abort();
5876
+ }
5877
+ this._activeToolControllers.clear();
5878
+ }
5713
5879
  /**
5714
5880
  * Sets up the event listeners.
5715
5881
  * This is called once in the constructor.
@@ -5720,11 +5886,7 @@ class AparteClient {
5720
5886
  const event = e;
5721
5887
  if (event.type !== "aparte-send") return;
5722
5888
  if (!this._isForThisInstance(event)) return;
5723
- this._isAborted = false;
5724
- for (const controller of this._activeToolControllers) {
5725
- controller.abort();
5726
- }
5727
- this._activeToolControllers.clear();
5889
+ this._beginUserTurn();
5728
5890
  void this._handleSend(event);
5729
5891
  };
5730
5892
  }
@@ -6074,7 +6236,7 @@ ${summary}`,
6074
6236
  }
6075
6237
  async _handleRetry(event) {
6076
6238
  const { messageId, targetId } = event.detail ?? {};
6077
- this._isAborted = false;
6239
+ this._beginUserTurn();
6078
6240
  if (!messageId) return;
6079
6241
  const targetElement = this._resolveTarget(targetId);
6080
6242
  if (!targetElement) {
@@ -6101,9 +6263,7 @@ ${summary}`,
6101
6263
  if (!provider) return;
6102
6264
  const authConfig = await this._resolveAuth(providerId);
6103
6265
  const chatMessages = this._messagesToChatMessages(historyMessages);
6104
- const systemMessages = [];
6105
- const userSystemPrompt = this._config.resolveSystemPrompt();
6106
- if (userSystemPrompt) systemMessages.push({ role: "system", content: userSystemPrompt });
6266
+ const systemMessages = this._systemMessages();
6107
6267
  await this._streamTurn(
6108
6268
  targetElement,
6109
6269
  newMessageId,
@@ -6114,13 +6274,30 @@ ${summary}`,
6114
6274
  { temperature: 0.4 }
6115
6275
  );
6116
6276
  }
6277
+ /**
6278
+ * The system messages a turn opens with: the app's own template, then the registered
6279
+ * tools' `systemPrompt`s.
6280
+ *
6281
+ * A helper rather than three copies. The two lines it replaces were written out at each
6282
+ * of the three turn entry points (send, retry, edit), which is exactly how the tool half
6283
+ * would have been added to two of them and forgotten in the third — the same shape as
6284
+ * the abort-flag asymmetry `_beginUserTurn` exists to prevent.
6285
+ */
6286
+ _systemMessages() {
6287
+ const messages = [];
6288
+ const appPrompt = this._config.resolveSystemPrompt();
6289
+ if (appPrompt) messages.push({ role: "system", content: appPrompt });
6290
+ const toolPrompts = this._config.resolveToolSystemPrompts();
6291
+ if (toolPrompts) messages.push({ role: "system", content: toolPrompts });
6292
+ return messages;
6293
+ }
6117
6294
  /**
6118
6295
  * Handle aparte-edit — update the user message in place, truncate all subsequent
6119
6296
  * messages, then re-stream a fresh assistant response.
6120
6297
  */
6121
6298
  async _handleEdit(event) {
6122
6299
  const { messageId, content: newContent, targetId } = event.detail ?? {};
6123
- this._isAborted = false;
6300
+ this._beginUserTurn();
6124
6301
  if (!messageId || newContent === void 0) return;
6125
6302
  const targetElement = this._resolveTarget(targetId);
6126
6303
  if (!targetElement) {
@@ -6140,9 +6317,7 @@ ${summary}`,
6140
6317
  }
6141
6318
  const historyMessages = editIdx >= 0 ? allMessages.slice(0, editIdx + 1) : allMessages;
6142
6319
  const chatMessages = this._messagesToChatMessages(historyMessages);
6143
- const systemMessages = [];
6144
- const userSystemPrompt = this._config.resolveSystemPrompt();
6145
- if (userSystemPrompt) systemMessages.push({ role: "system", content: userSystemPrompt });
6320
+ const systemMessages = this._systemMessages();
6146
6321
  const config = this._config.getModelConfig();
6147
6322
  const providerId = config.defaultProvider;
6148
6323
  if (!providerId) return;
@@ -6311,11 +6486,7 @@ ${summary}`,
6311
6486
  * Build the initial messages array, prepending system prompts and conversation history.
6312
6487
  */
6313
6488
  _buildMessages(userContent, target, parts) {
6314
- const messages = [];
6315
- const userSystemPrompt = this._config.resolveSystemPrompt();
6316
- if (userSystemPrompt) {
6317
- messages.push({ role: "system", content: userSystemPrompt });
6318
- }
6489
+ const messages = this._systemMessages();
6319
6490
  const historyOption = this.options.history ?? "viewport";
6320
6491
  const viewportMessages = target?.getMessages?.() ?? [];
6321
6492
  if (historyOption === "viewport") {
@@ -6581,18 +6752,7 @@ ${content}
6581
6752
  };
6582
6753
  const toolRenderer = this._config.getToolRenderer(event.name);
6583
6754
  if (toolRenderer) {
6584
- if (toolRenderer.getStyles) {
6585
- const styles = toolRenderer.getStyles();
6586
- if (styles) {
6587
- const styleId = `aparte-tool-renderer-${event.name}`;
6588
- if (!document.getElementById(styleId)) {
6589
- const el2 = document.createElement("style");
6590
- el2.id = styleId;
6591
- el2.textContent = styles;
6592
- document.head.appendChild(el2);
6593
- }
6594
- }
6595
- }
6755
+ injectToolRendererStyles(event.name, toolRenderer);
6596
6756
  const html = toolRenderer.render(toolSeg);
6597
6757
  if (html) {
6598
6758
  targetElement.addSegment?.(toolSeg);
@@ -6771,6 +6931,21 @@ ${content}
6771
6931
  ...wholeParser.parse(response).segments,
6772
6932
  ...wholeParser.finalize()
6773
6933
  ];
6934
+ const stringHint = baseRequest._meta?.artifactHint;
6935
+ if (stringHint) {
6936
+ const codeIdx = parsed.findIndex((s) => s.type === "code");
6937
+ if (codeIdx !== -1) {
6938
+ const codeSeg = parsed[codeIdx];
6939
+ parsed[codeIdx] = {
6940
+ id: codeSeg.id,
6941
+ type: "artifact",
6942
+ mimeType: stringHint.mimeType,
6943
+ artifactType: stringHint.kind,
6944
+ title: codeSeg.filename ?? stringHint.kind,
6945
+ content: codeSeg.content
6946
+ };
6947
+ }
6948
+ }
6774
6949
  if (parsed.length > 0) {
6775
6950
  for (const segment of parsed) targetElement.addSegment?.(segment);
6776
6951
  this._updateMessage(targetElement, messageId, { status: "completed" });
@@ -7107,7 +7282,7 @@ ${content}
7107
7282
  details: error.code
7108
7283
  };
7109
7284
  if (target.updateMessage) {
7110
- const rendered = target.getMessages?.()?.find((m) => m.id === messageId)?.segments ?? [];
7285
+ const rendered = target.getMessage?.(messageId)?.segments ?? target.getMessages?.()?.find((m) => m.id === messageId)?.segments ?? [];
7111
7286
  target.updateMessage(messageId, {
7112
7287
  status: "error",
7113
7288
  segments: [...rendered, errorSegment]
@@ -7464,13 +7639,14 @@ export {
7464
7639
  unregisterSegmentRenderer as a5,
7465
7640
  uuid as a6,
7466
7641
  mergeSegmentUpdate as a7,
7467
- stampSegmentActivity as a8,
7468
- stampSegmentOnInsert as a9,
7469
- stampSegmentOnUpdate as aa,
7470
- renumberSegments as ab,
7471
- openSegmentIds as ac,
7472
- isTerminalStatus as ad,
7473
- adoptMessageSegments as ae,
7642
+ writeStreamedMarkdown as a8,
7643
+ stampSegmentActivity as a9,
7644
+ stampSegmentOnInsert as aa,
7645
+ stampSegmentOnUpdate as ab,
7646
+ renumberSegments as ac,
7647
+ openSegmentIds as ad,
7648
+ isTerminalStatus as ae,
7649
+ adoptMessageSegments as af,
7474
7650
  APARTE_DEFAULT_BUBBLE_ACTIONS as b,
7475
7651
  APARTE_DEFAULT_HOST_HANDLERS as c,
7476
7652
  APARTE_DEFAULT_ICON_FALLBACKS as d,
@@ -7497,4 +7673,4 @@ export {
7497
7673
  buildElicitationPanel as y,
7498
7674
  collectRendererStyles as z
7499
7675
  };
7500
- //# sourceMappingURL=is-awaiting-reply-BWTDvQ__.js.map
7676
+ //# sourceMappingURL=is-awaiting-reply-CWp1ffHh.js.map