@aparte/core 0.12.0 → 0.13.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.
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/components/bubble/aparte-chat-bubble.d.ts +117 -0
  4. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  5. package/dist/components/chat/aparte-chat.d.ts +70 -15
  6. package/dist/components/chat/aparte-chat.d.ts.map +1 -1
  7. package/dist/components/composer/aparte-composer-action.d.ts +48 -7
  8. package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
  9. package/dist/components/composer/aparte-composer-add-attachment.d.ts +37 -4
  10. package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
  11. package/dist/components/composer/aparte-composer-attachments.d.ts +35 -3
  12. package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
  13. package/dist/components/composer/aparte-composer-cancel.d.ts +25 -2
  14. package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
  15. package/dist/components/composer/aparte-composer-input.d.ts +54 -5
  16. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  17. package/dist/components/composer/aparte-composer-send.d.ts +47 -4
  18. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  19. package/dist/components/composer/aparte-composer-toolbar.d.ts +15 -4
  20. package/dist/components/composer/aparte-composer-toolbar.d.ts.map +1 -1
  21. package/dist/components/composer/aparte-composer.d.ts +139 -7
  22. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  23. package/dist/components/composer/index.d.ts +1 -1
  24. package/dist/components/composer/index.d.ts.map +1 -1
  25. package/dist/components/conversation-list/aparte-conversation-list.d.ts +50 -8
  26. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  27. package/dist/components/elicitation/aparte-elicitation.d.ts +45 -0
  28. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  29. package/dist/components/index.d.ts +1 -1
  30. package/dist/components/index.d.ts.map +1 -1
  31. package/dist/components/status/aparte-chat-status.d.ts +47 -5
  32. package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
  33. package/dist/components/viewport/aparte-chat-viewport.d.ts +78 -8
  34. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  35. package/dist/config/aparte-config.d.ts +22 -0
  36. package/dist/config/aparte-config.d.ts.map +1 -1
  37. package/dist/config/icon-provider.d.ts +17 -15
  38. package/dist/config/icon-provider.d.ts.map +1 -1
  39. package/dist/config/sanitize.d.ts.map +1 -1
  40. package/dist/custom-elements.json +5400 -3278
  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/host/aparte-chat-host.d.ts +22 -21
  49. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  50. package/dist/icons/extended.d.ts +153 -0
  51. package/dist/icons/extended.d.ts.map +1 -0
  52. package/dist/icons/glyphs.d.ts +98 -0
  53. package/dist/icons/glyphs.d.ts.map +1 -0
  54. package/dist/icons/index.d.ts +24 -0
  55. package/dist/icons/index.d.ts.map +1 -0
  56. package/dist/icons.d.ts +14 -0
  57. package/dist/icons.d.ts.map +1 -0
  58. package/dist/icons.js +157 -0
  59. package/dist/icons.js.map +1 -0
  60. package/dist/index.css +5082 -2119
  61. package/dist/index.d.ts +37 -3
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +167 -67
  64. package/dist/index.js.map +1 -1
  65. package/dist/index.node.d.ts +2 -2
  66. package/dist/index.node.d.ts.map +1 -1
  67. package/dist/index.node.js +1 -1
  68. package/dist/index.node.js.map +1 -1
  69. package/dist/{is-awaiting-reply-CxWRISBC.js → is-awaiting-reply-DrO_oR32.js} +369 -148
  70. package/dist/is-awaiting-reply-DrO_oR32.js.map +1 -0
  71. package/dist/primitives/icon/aparte-icon.d.ts +49 -0
  72. package/dist/primitives/icon/aparte-icon.d.ts.map +1 -0
  73. package/dist/primitives/icon/index.d.ts +2 -0
  74. package/dist/primitives/icon/index.d.ts.map +1 -0
  75. package/dist/primitives/index.d.ts +1 -0
  76. package/dist/primitives/index.d.ts.map +1 -1
  77. package/dist/primitives/progress-spinner/aparte-progress-spinner.d.ts +20 -5
  78. package/dist/primitives/progress-spinner/aparte-progress-spinner.d.ts.map +1 -1
  79. package/dist/primitives/select/aparte-optgroup.d.ts +30 -7
  80. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  81. package/dist/primitives/select/aparte-option.d.ts +29 -3
  82. package/dist/primitives/select/aparte-option.d.ts.map +1 -1
  83. package/dist/primitives/select/aparte-select.d.ts +36 -11
  84. package/dist/primitives/select/aparte-select.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-CxWRISBC.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
  }
@@ -959,6 +946,7 @@ const SAFE_STYLE_PROPS = /* @__PURE__ */ new Set([
959
946
  "white-space"
960
947
  ]);
961
948
  function isSafeCustomProperty(prop) {
949
+ if (prop.includes("\\")) return false;
962
950
  return prop.startsWith("--") && !prop.startsWith("--aparte-");
963
951
  }
964
952
  function scrubStyle(value) {
@@ -1873,6 +1861,24 @@ class AparteConfig {
1873
1861
  getTools() {
1874
1862
  return Array.from(this._tools.values()).map((e) => e.tool);
1875
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
+ }
1876
1882
  /** Get the handler for a tool by name */
1877
1883
  getToolHandler(name) {
1878
1884
  return this._tools.get(name)?.handler;
@@ -2122,16 +2128,15 @@ class AparteConfig {
2122
2128
  _defaultHighlightRenderer(code) {
2123
2129
  return `<pre><code>${escapeHtml(code)}</code></pre>`;
2124
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
+ */
2125
2138
  _defaultSkeletonRenderer(type) {
2126
- const fallbacks = {
2127
- message: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading message...</div>',
2128
- code: '<div class="aparte-skeleton-fallback" style="padding:16px;background:#1e293b;color:#64748b;border-radius:8px;">Loading code...</div>',
2129
- text: '<div class="aparte-skeleton-fallback" style="padding:8px;color:#9ca3af;">Loading...</div>',
2130
- thinking: '<div class="aparte-skeleton-fallback" style="padding:8px;color:#9ca3af;">Thinking...</div>',
2131
- input: '<div class="aparte-skeleton-fallback" style="padding:12px;color:#9ca3af;">...</div>',
2132
- list: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading items...</div>'
2133
- };
2134
- return fallbacks[type] || fallbacks.text;
2139
+ return APARTE_DEFAULT_SKELETON_FALLBACKS[type] ?? APARTE_DEFAULT_SKELETON_FALLBACKS.text;
2135
2140
  }
2136
2141
  }
2137
2142
  const GLOBAL_CONFIG_KEY = Symbol.for("aparte.globalConfig.1");
@@ -2195,14 +2200,6 @@ function subscribeConfigChange(el2, handler) {
2195
2200
  window.addEventListener(APARTE_CONFIG_CHANGE, listener);
2196
2201
  return () => window.removeEventListener(APARTE_CONFIG_CHANGE, listener);
2197
2202
  }
2198
- const APARTE_DEFAULT_SKELETON_FALLBACKS = {
2199
- message: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading...</div>',
2200
- code: '<div class="aparte-skeleton-fallback" style="padding:16px;background:#1e293b;color:#64748b;border-radius:8px;">Loading code...</div>',
2201
- thinking: '<div class="aparte-skeleton-fallback" style="padding:8px;color:#9ca3af;">Thinking...</div>',
2202
- input: '<div class="aparte-skeleton-fallback" style="padding:12px;color:#9ca3af;">...</div>',
2203
- list: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading items...</div>',
2204
- text: '<div class="aparte-skeleton-fallback" style="padding:8px;color:#9ca3af;">...</div>'
2205
- };
2206
2203
  function writeStreamedMarkdown(host, contentEl, content, streaming) {
2207
2204
  if (streaming) {
2208
2205
  if (host._aparteSmd === void 0) {
@@ -2275,7 +2272,21 @@ const thinkingRenderer = {
2275
2272
  * new class of risk. With no Markdown provider registered it degrades to the
2276
2273
  * zero-dependency default (escape + `<br>`), which is what it used to be.
2277
2274
  */
2278
- 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>`,
2279
2290
  /**
2280
2291
  * The default label is the only config-derived text here — and `segment.label`
2281
2292
  * still wins, exactly as in `render`, because that string is the app's.
@@ -2349,7 +2360,7 @@ const codeRenderer = {
2349
2360
  <div class="aparte-code-header">
2350
2361
  ${segment.filename ? `<span class="aparte-code-filename">${escapeHtml(segment.filename)}</span>` : `<span class="aparte-code-header-filler"></span>`}
2351
2362
  <span class="aparte-code-language">${escapeHtml(segment.language || "")}</span>
2352
- <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"))}">
2353
2364
  ${contextConfig().getIcon("copy")}
2354
2365
  </button>
2355
2366
  </div>
@@ -2418,7 +2429,7 @@ const errorRenderer = {
2418
2429
  return out;
2419
2430
  }
2420
2431
  return `
2421
- <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)}">
2422
2433
  <div class="aparte-error-icon-wrapper">
2423
2434
  ${contextConfig().getIcon("error") || "⚠"}
2424
2435
  </div>
@@ -2517,7 +2528,7 @@ function pillMarkup(segment, name, status) {
2517
2528
  <span class="aparte-tool-icon">${contextConfig().getIcon("tool")}</span>
2518
2529
  <span class="aparte-tool-name">${escapeHtml(name)}</span>
2519
2530
  </span>
2520
- <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>
2521
2532
  <span class="aparte-tool-state">${badge}</span>`;
2522
2533
  }
2523
2534
  const toolCallRenderer = {
@@ -2532,6 +2543,7 @@ const toolCallRenderer = {
2532
2543
  if (out) return out;
2533
2544
  }
2534
2545
  const pill = pillMarkup(segment, name, status);
2546
+ const toggle = contextConfig().getIcon("expand");
2535
2547
  const input = describeInput(segment);
2536
2548
  const result = segment.result;
2537
2549
  if (!input && !result) {
@@ -2543,7 +2555,7 @@ const toolCallRenderer = {
2543
2555
  const cfg = contextConfig();
2544
2556
  return `
2545
2557
  <details class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}">
2546
- <summary class="aparte-tool-summary"><span class="aparte-tool-toggle"></span>${pill}</summary>
2558
+ <summary class="aparte-tool-summary"><span class="aparte-tool-toggle">${toggle}</span>${pill}</summary>
2547
2559
  <div class="aparte-tool-detail">
2548
2560
  ${input ? `
2549
2561
  <div class="aparte-tool-part" data-part="input">
@@ -2618,7 +2630,7 @@ const toolCallRenderer = {
2618
2630
  const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
2619
2631
  customRenderer?.setup?.(element, segment);
2620
2632
  },
2621
- // CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
2633
+ // CSS lives in styles/segment/ — see that folder for why a
2622
2634
  // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
2623
2635
  // has no other way onto the page.
2624
2636
  getStyles: () => ""
@@ -2629,9 +2641,9 @@ const pipelineWaitingRenderer = {
2629
2641
  render: (segment) => {
2630
2642
  return `
2631
2643
  <div class="aparte-segment aparte-segment-pipeline-waiting" data-segment-id="${escapeHtml(segment.id)}" aria-label="${escapeAttr(waitingLabel())}" role="status">
2632
- <span class="aparte-pw-dot"></span>
2633
- <span class="aparte-pw-dot"></span>
2634
- <span class="aparte-pw-dot"></span>
2644
+ <span class="aparte-dot aparte-pw-dot"></span>
2645
+ <span class="aparte-dot aparte-pw-dot"></span>
2646
+ <span class="aparte-dot aparte-pw-dot"></span>
2635
2647
  </div>`;
2636
2648
  },
2637
2649
  update: () => {
@@ -2656,7 +2668,7 @@ const pipelineWaitingRenderer = {
2656
2668
  });
2657
2669
  observer.observe(parent, { childList: true });
2658
2670
  },
2659
- // CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
2671
+ // CSS lives in styles/segment/ — see that folder for why a
2660
2672
  // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
2661
2673
  // has no other way onto the page.
2662
2674
  getStyles: () => ""
@@ -2862,7 +2874,7 @@ function renderBinaryFileArtifact(segment, kind) {
2862
2874
  if (cached && !isStreaming) {
2863
2875
  const previewBody = cached.previewHtml ? contextConfig().sanitizeHtml(cached.previewHtml) : `<div class="aparte-art-file__preview-empty">Preview not available for ${escapeHtml(kind)} yet</div>`;
2864
2876
  return `
2865
- <div class="aparte-segment aparte-segment-artifact-file"
2877
+ <div class="aparte-segment aparte-card aparte-segment-artifact-file"
2866
2878
  data-segment-id="${escapeHtml(segment.id)}"
2867
2879
  data-artifact-type="${escapeHtml(kind)}"
2868
2880
  data-state="ready">
@@ -2873,7 +2885,7 @@ function renderBinaryFileArtifact(segment, kind) {
2873
2885
  <div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(formatBytes(cached.bytes))} · ${escapeHtml(kind.toUpperCase())}</div>
2874
2886
  </div>
2875
2887
  <div class="aparte-art-file__actions">
2876
- ${canRedownload ? `<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download">${downloadLabel}</button>` : ""}
2888
+ ${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>` : ""}
2877
2889
  </div>
2878
2890
  </div>
2879
2891
  <div class="aparte-art-file__body">
@@ -2888,7 +2900,7 @@ function renderBinaryFileArtifact(segment, kind) {
2888
2900
  const cleanContent = stripCodeFences(segment.content || "");
2889
2901
  const subText = isStreaming ? contextConfig().t("generating") : contextConfig().t("rebuildingPreview");
2890
2902
  return `
2891
- <div class="aparte-segment aparte-segment-artifact-file"
2903
+ <div class="aparte-segment aparte-card aparte-segment-artifact-file"
2892
2904
  data-segment-id="${escapeHtml(segment.id)}"
2893
2905
  data-artifact-type="${escapeHtml(kind)}"
2894
2906
  data-state="${isStreaming ? "streaming" : "compiling"}">
@@ -2899,7 +2911,7 @@ function renderBinaryFileArtifact(segment, kind) {
2899
2911
  <div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(subText)}</div>
2900
2912
  </div>
2901
2913
  <div class="aparte-art-file__actions">
2902
- ${canRedownload ? `<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download" disabled>${downloadLabel}</button>` : ""}
2914
+ ${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>` : ""}
2903
2915
  </div>
2904
2916
  </div>
2905
2917
  <div class="aparte-art-file__body">
@@ -3057,7 +3069,7 @@ function showSandboxError(element, phase, errorMsg) {
3057
3069
  body.innerHTML = `
3058
3070
  <div class="aparte-art-file__error">
3059
3071
  <div class="aparte-art-file__error-title">${escapeHtml(contextConfig().t("sandboxError"))}</div>
3060
- <div class="aparte-art-file__error-msg">${escapeHtml(short)}</div>
3072
+ <div class="aparte-output aparte-art-file__error-msg">${escapeHtml(short)}</div>
3061
3073
  <div class="aparte-art-file__error-hint">${escapeHtml(contextConfig().t("sandboxErrorHint"))}</div>
3062
3074
  </div>
3063
3075
  `;
@@ -3082,8 +3094,9 @@ const artifactRenderer = {
3082
3094
  const codeLabel = cfg.t("code");
3083
3095
  const isBinary = BINARY_FILE_KINDS.has(kind);
3084
3096
  const cleanContent = stripCodeFences(segment.content || "");
3097
+ const cardId = escapeAttr(segment.id);
3085
3098
  return `
3086
- <div class="aparte-segment aparte-segment-artifact-card"
3099
+ <div class="aparte-segment aparte-card aparte-segment-artifact-card"
3087
3100
  data-segment-id="${escapeHtml(segment.id)}"
3088
3101
  data-artifact-type="${escapeHtml(kind)}"
3089
3102
  data-streaming="${isStreaming ? "true" : "false"}"
@@ -3092,31 +3105,31 @@ const artifactRenderer = {
3092
3105
  data-binary="${isBinary ? "true" : "false"}">
3093
3106
  <header class="aparte-art-card__header">
3094
3107
  <div class="aparte-art-card__title-block">
3095
- <span class="aparte-art-card__kind" data-kind="${escapeHtml(kind)}">${escapeHtml(displayLang)}</span>
3108
+ <span class="aparte-badge aparte-badge--outline aparte-art-card__kind" data-kind="${escapeHtml(kind)}">${escapeHtml(displayLang)}</span>
3096
3109
  <span class="aparte-art-card__title">${escapeHtml(title)}</span>
3097
- ${isStreaming ? '<span class="aparte-art-card__pulse" aria-label="Streaming"></span>' : ""}
3110
+ ${isStreaming ? `<span class="aparte-dot aparte-art-card__pulse" role="img" aria-label="${escapeAttr(cfg.t("generating"))}"></span>` : ""}
3098
3111
  </div>
3099
3112
  <div class="aparte-art-card__actions">
3100
- <button type="button" class="aparte-art-card__btn" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}" aria-label="${escapeAttr(contextConfig().t("copy"))}">
3113
+ <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"))}">
3101
3114
  ${contextConfig().getIcon("copy")}
3102
3115
  </button>
3103
- <button type="button" class="aparte-art-card__btn" data-action="download" title="${escapeAttr(downloadLabel)}" aria-label="${escapeAttr(downloadLabel)}" ${isStreaming ? "disabled" : ""}>
3104
- <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>
3116
+ <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" : ""}>
3117
+ ${contextConfig().getIcon("download")}
3105
3118
  </button>
3106
3119
  </div>
3107
3120
  </header>
3108
- <nav class="aparte-art-card__tabs" role="tablist">
3109
- <button type="button" role="tab" data-tab-target="code" aria-selected="true">${escapeHtml(codeLabel)}</button>
3110
- ${previewable ? `<button type="button" role="tab" data-tab-target="preview" aria-selected="false" ${isStreaming ? "disabled" : ""}>${escapeHtml(previewLabel)}</button>` : ""}
3121
+ <nav class="aparte-tabs aparte-tabs--underline aparte-art-card__tabs" role="tablist">
3122
+ <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>
3123
+ ${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>` : ""}
3111
3124
  </nav>
3112
3125
  <div class="aparte-art-card__body">
3113
- <div class="aparte-art-card__pane" data-pane="code">
3126
+ <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">
3114
3127
  <div class="aparte-code-content-wrapper">
3115
3128
  <pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>
3116
3129
  </div>
3117
3130
  </div>
3118
3131
  ${previewable ? `
3119
- <div class="aparte-art-card__pane" data-pane="preview">
3132
+ <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">
3120
3133
  <div class="aparte-art-card__pending">${escapeHtml(cfg.t("previewPending"))}</div>
3121
3134
  </div>
3122
3135
  ` : ""}
@@ -3171,17 +3184,38 @@ const artifactRenderer = {
3171
3184
  }).catch(() => {
3172
3185
  });
3173
3186
  }
3174
- element.querySelectorAll("[data-tab-target]").forEach((btn) => {
3175
- btn.addEventListener("click", () => {
3176
- const target = btn.getAttribute("data-tab-target");
3177
- if (!target) return;
3178
- if (target === "preview") mountPreviewFrame(element, segment);
3179
- element.setAttribute("data-tab", target);
3180
- element.querySelectorAll("[data-tab-target]").forEach((b) => {
3181
- b.setAttribute("aria-selected", b === btn ? "true" : "false");
3182
- });
3187
+ const tabs = [...element.querySelectorAll("[data-tab-target]")];
3188
+ const select = (btn, focus) => {
3189
+ const target = btn.getAttribute("data-tab-target");
3190
+ if (!target || btn.disabled) return;
3191
+ if (target === "preview") mountPreviewFrame(element, segment);
3192
+ element.setAttribute("data-tab", target);
3193
+ for (const b of tabs) {
3194
+ const on = b === btn;
3195
+ b.setAttribute("aria-selected", on ? "true" : "false");
3196
+ b.tabIndex = on ? 0 : -1;
3197
+ }
3198
+ if (focus) btn.focus();
3199
+ };
3200
+ for (const [i, btn] of tabs.entries()) {
3201
+ btn.addEventListener("click", () => select(btn, false));
3202
+ btn.addEventListener("keydown", (e) => {
3203
+ const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
3204
+ let next = -1;
3205
+ if (step) next = (i + step + tabs.length) % tabs.length;
3206
+ else if (e.key === "Home") next = 0;
3207
+ else if (e.key === "End") next = tabs.length - 1;
3208
+ if (next < 0) return;
3209
+ e.preventDefault();
3210
+ for (let n = 0; n < tabs.length; n++) {
3211
+ const candidate = tabs[(next + n * (step || 1) + tabs.length) % tabs.length];
3212
+ if (candidate && !candidate.disabled) {
3213
+ select(candidate, true);
3214
+ return;
3215
+ }
3216
+ }
3183
3217
  });
3184
- });
3218
+ }
3185
3219
  const copyBtn = element.querySelector('[data-action="copy"]');
3186
3220
  if (copyBtn) {
3187
3221
  copyBtn.addEventListener("click", () => {
@@ -3266,7 +3300,7 @@ const artifactRenderer = {
3266
3300
  });
3267
3301
  }
3268
3302
  },
3269
- // CSS lives in styles/aparte.css — see its "Segment renderers" section for why a
3303
+ // CSS lives in styles/segment/ — see that folder for why a
3270
3304
  // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
3271
3305
  // has no other way onto the page.
3272
3306
  getStyles: () => ""
@@ -4120,6 +4154,7 @@ class AparteConversationController {
4120
4154
  function cssEscape(value) {
4121
4155
  return value.replace(/["\\]/g, "\\$&");
4122
4156
  }
4157
+ const MAX_SIBLING_RETRIES = 6;
4123
4158
  class AparteChatHost {
4124
4159
  binding;
4125
4160
  options;
@@ -4129,6 +4164,8 @@ class AparteChatHost {
4129
4164
  _renderedIds = /* @__PURE__ */ new Set();
4130
4165
  /** Sibling info from the last path-change, applied after the next render. */
4131
4166
  _pendingSiblings;
4167
+ /** How many render passes the sibling write has waited for — see `_applyPendingSiblings`. */
4168
+ _siblingRetries = 0;
4132
4169
  /** Abort handle for an in-flight {@link streamTokens} loop. */
4133
4170
  _streamAbort;
4134
4171
  /** Segment chunks written to the bubble, awaiting one coalesced state sync. */
@@ -4630,17 +4667,48 @@ class AparteChatHost {
4630
4667
  }
4631
4668
  }
4632
4669
  }
4670
+ /**
4671
+ * Write the branch counts onto their bubbles — and RESCHEDULE for the ones that are
4672
+ * not on the page yet, instead of dropping them.
4673
+ *
4674
+ * This used to `continue` past a missing bubble and then clear `_pendingSiblings`
4675
+ * unconditionally, which lost the fork for good. That is the "1 / 1" defect: the
4676
+ * bubble kept its default of one sibling, the picker hid itself, and the other version
4677
+ * became unreachable for the life of the page.
4678
+ *
4679
+ * It needed a framework that renders LATE, which is every framework here. React's
4680
+ * `afterRender` is `requestAnimationFrame(() => cb())` — a bet that the next paint
4681
+ * lands after React's commit, and it does not always; the repo has lost that same bet
4682
+ * before (`25f356b`, "a bet on rAF phase"). The fix is here rather than in the
4683
+ * wrapper's rAF call, because ANY binding whose `afterRender` can precede its commit
4684
+ * hits this, and one fix at the host covers all four.
4685
+ *
4686
+ * Bounded, because a sibling can legitimately never arrive — a message that left the
4687
+ * path has no bubble and never will, and an unbounded retry would hold a callback for
4688
+ * the page's lifetime. Six passes is far more than the one tick the race needs.
4689
+ */
4633
4690
  _applyPendingSiblings() {
4634
4691
  if (!this._pendingSiblings) return;
4635
4692
  const msgs = this.binding.getMessages();
4693
+ const unresolved = [];
4636
4694
  for (const sib of this._pendingSiblings) {
4637
4695
  if (sib.count <= 1) continue;
4638
4696
  const bubble = this._bubbleById(sib.id);
4639
- if (!bubble) continue;
4697
+ if (!bubble) {
4698
+ unresolved.push(sib);
4699
+ continue;
4700
+ }
4640
4701
  const message = msgs.find((m) => m.id === sib.id);
4641
4702
  if (message) populateBubbleFromMessage(bubble, message, sib);
4642
4703
  }
4643
4704
  this._pendingSiblings = void 0;
4705
+ if (unresolved.length && this._siblingRetries < MAX_SIBLING_RETRIES) {
4706
+ this._siblingRetries += 1;
4707
+ this._pendingSiblings = unresolved;
4708
+ this.binding.afterRender(() => this._applyPendingSiblings());
4709
+ return;
4710
+ }
4711
+ this._siblingRetries = 0;
4644
4712
  }
4645
4713
  // ── internals ──────────────────────────────────────────────────────────
4646
4714
  /** Orphan-stream guard: a stream is in flight but the last msg isn't it. */
@@ -5234,6 +5302,7 @@ function readableToAsyncIterable(stream, signal) {
5234
5302
  }
5235
5303
  };
5236
5304
  }
5305
+ let steppedPanelSeq = 0;
5237
5306
  let _relabels = null;
5238
5307
  function onRelabel(fn) {
5239
5308
  _relabels?.push(fn);
@@ -5244,6 +5313,18 @@ function el$1(tag, className, text) {
5244
5313
  if (text != null) node.textContent = text;
5245
5314
  return node;
5246
5315
  }
5316
+ function optionBody(label, description) {
5317
+ const body = el$1("span", "aparte-elic-option-body");
5318
+ body.appendChild(el$1("span", "aparte-elic-option-title", label));
5319
+ if (description) body.appendChild(el$1("span", "aparte-elic-option-desc", description));
5320
+ return body;
5321
+ }
5322
+ function commandOption(label, description, recommended) {
5323
+ 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" : ""));
5324
+ button.type = "button";
5325
+ button.appendChild(optionBody(label, description));
5326
+ return button;
5327
+ }
5247
5328
  function fieldHeader(parent, field, skipTitle) {
5248
5329
  let titleId;
5249
5330
  if (field.title && field.title.trim() !== skipTitle?.trim()) {
@@ -5256,61 +5337,87 @@ function fieldHeader(parent, field, skipTitle) {
5256
5337
  return titleId;
5257
5338
  }
5258
5339
  function labelGroup(list, opts) {
5259
- list.setAttribute("role", opts.multiple ? "group" : "radiogroup");
5340
+ list.setAttribute("role", opts.multiple || opts.commands ? "group" : "radiogroup");
5260
5341
  if (opts.titleId) list.setAttribute("aria-labelledby", opts.titleId);
5261
5342
  else if (opts.fallbackLabel) list.setAttribute("aria-label", opts.fallbackLabel);
5262
5343
  }
5263
- function buildEnumField(field, onChange, fallbackLabel) {
5344
+ function buildEnumField(field, onChange, fallbackLabel, settle) {
5264
5345
  const wrap = el$1("div", "aparte-elic-field aparte-elic-enum");
5265
5346
  const titleId = fieldHeader(wrap, field, fallbackLabel);
5266
5347
  const list = el$1("div", "aparte-elic-options");
5267
- labelGroup(list, { multiple: field.multiple, titleId, fallbackLabel });
5268
5348
  const name = `elic-${uuid()}`;
5269
5349
  const type = field.multiple ? "checkbox" : "radio";
5350
+ const controlRecipe = type === "checkbox" ? "aparte-checkbox" : "aparte-radio";
5270
5351
  const allowOther = field.allowOther ?? contextConfig().getElicitationOptions().allowOther;
5271
5352
  const defaults = new Set(Array.isArray(field.default) ? field.default : field.default != null ? [field.default] : []);
5353
+ const settling = settle != null && !field.multiple && defaults.size === 0;
5354
+ labelGroup(list, { multiple: field.multiple, commands: settling, titleId, fallbackLabel });
5272
5355
  const buildOption = (value, label, description, recommended) => {
5273
- const row = el$1("label", "aparte-elic-option" + (recommended ? " aparte-elic-option--recommended" : ""));
5274
- const control = el$1("input", "aparte-elic-control");
5356
+ if (settling) {
5357
+ const button = commandOption(label, description, recommended);
5358
+ button.addEventListener("click", () => settle?.(value));
5359
+ return button;
5360
+ }
5361
+ const row = el$1("label", "aparte-field-choice aparte-elic-option" + (recommended ? " aparte-elic-option--recommended" : ""));
5362
+ const control = el$1("input", controlRecipe + " aparte-elic-control");
5275
5363
  control.type = type;
5276
5364
  control.name = name;
5277
5365
  control.value = value;
5278
5366
  if (defaults.has(value)) control.checked = true;
5279
- const body = el$1("span", "aparte-elic-option-body");
5280
- body.appendChild(el$1("span", "aparte-elic-option-title", label));
5281
- if (description) body.appendChild(el$1("span", "aparte-elic-option-desc", description));
5282
- row.append(control, body);
5367
+ row.append(control, optionBody(label, description));
5283
5368
  return row;
5284
5369
  };
5285
5370
  for (const opt of field.options) {
5286
5371
  list.appendChild(buildOption(opt.value, opt.label ?? opt.value, opt.description, opt.recommended));
5287
5372
  }
5288
5373
  let otherText = null;
5374
+ let otherOpen = false;
5289
5375
  if (allowOther) {
5290
- const row = el$1("label", "aparte-elic-option aparte-elic-option--other");
5291
- const control = el$1("input", "aparte-elic-control");
5292
- control.type = type;
5293
- control.name = name;
5294
- control.value = "__other__";
5295
- const body = el$1("span", "aparte-elic-option-body");
5296
5376
  const t = contextConfig();
5297
- const otherTitle = el$1("span", "aparte-elic-option-title", t.t("elicitationOther"));
5298
- body.appendChild(otherTitle);
5299
- otherText = el$1("input", "aparte-elic-other-input");
5377
+ otherText = el$1("input", "aparte-field aparte-elic-other-input");
5300
5378
  otherText.type = "text";
5301
5379
  otherText.placeholder = t.t("elicitationOtherPlaceholder");
5302
5380
  otherText.style.display = "none";
5303
5381
  otherText.setAttribute("aria-label", t.t("elicitationOtherLabel"));
5304
5382
  const otherInput = otherText;
5305
- onRelabel(() => {
5306
- const now = contextConfig();
5307
- otherTitle.textContent = now.t("elicitationOther");
5308
- otherInput.placeholder = now.t("elicitationOtherPlaceholder");
5309
- otherInput.setAttribute("aria-label", now.t("elicitationOtherLabel"));
5310
- });
5311
- body.appendChild(otherText);
5312
- row.append(control, body);
5313
- list.appendChild(row);
5383
+ if (settling) {
5384
+ const button = commandOption(t.t("elicitationOther"));
5385
+ button.classList.add("aparte-elic-option--other");
5386
+ button.setAttribute("aria-expanded", "false");
5387
+ button.addEventListener("click", () => {
5388
+ otherOpen = true;
5389
+ button.setAttribute("aria-expanded", "true");
5390
+ otherInput.style.display = "";
5391
+ otherInput.focus();
5392
+ onChange();
5393
+ });
5394
+ const otherTitle = button.querySelector(".aparte-elic-option-title");
5395
+ onRelabel(() => {
5396
+ const now = contextConfig();
5397
+ otherTitle.textContent = now.t("elicitationOther");
5398
+ otherInput.placeholder = now.t("elicitationOtherPlaceholder");
5399
+ otherInput.setAttribute("aria-label", now.t("elicitationOtherLabel"));
5400
+ });
5401
+ list.append(button, otherInput);
5402
+ } else {
5403
+ const row = el$1("label", "aparte-field-choice aparte-elic-option aparte-elic-option--other");
5404
+ const control = el$1("input", controlRecipe + " aparte-elic-control");
5405
+ control.type = type;
5406
+ control.name = name;
5407
+ control.value = "__other__";
5408
+ const body = el$1("span", "aparte-elic-option-body");
5409
+ const otherTitle = el$1("span", "aparte-elic-option-title", t.t("elicitationOther"));
5410
+ body.appendChild(otherTitle);
5411
+ onRelabel(() => {
5412
+ const now = contextConfig();
5413
+ otherTitle.textContent = now.t("elicitationOther");
5414
+ otherInput.placeholder = now.t("elicitationOtherPlaceholder");
5415
+ otherInput.setAttribute("aria-label", now.t("elicitationOtherLabel"));
5416
+ });
5417
+ body.appendChild(otherText);
5418
+ row.append(control, body);
5419
+ list.appendChild(row);
5420
+ }
5314
5421
  }
5315
5422
  wrap.appendChild(list);
5316
5423
  const controls = () => Array.from(list.querySelectorAll(".aparte-elic-control"));
@@ -5324,8 +5431,18 @@ function buildEnumField(field, onChange, fallbackLabel) {
5324
5431
  onChange();
5325
5432
  });
5326
5433
  otherText?.addEventListener("input", onChange);
5434
+ const typedOther = () => otherText?.value.trim() ?? "";
5435
+ if (settling) {
5436
+ return {
5437
+ el: wrap,
5438
+ getValue: () => typedOther(),
5439
+ isComplete: () => otherOpen && typedOther() !== "",
5440
+ focus: () => list.querySelector(".aparte-elic-option--command")?.focus(),
5441
+ offersSubmit: () => otherOpen
5442
+ };
5443
+ }
5327
5444
  const getValue = () => {
5328
- const otherVal = other()?.checked && otherText?.value.trim() ? otherText.value.trim() : "";
5445
+ const otherVal = other()?.checked && typedOther() ? typedOther() : "";
5329
5446
  if (field.multiple) {
5330
5447
  const vals = controls().filter((c) => c.checked && c.value !== "__other__").map((c) => c.value);
5331
5448
  if (otherVal) vals.push(otherVal);
@@ -5345,15 +5462,42 @@ function buildEnumField(field, onChange, fallbackLabel) {
5345
5462
  focus: () => controls()[0]?.focus()
5346
5463
  };
5347
5464
  }
5348
- function buildBooleanField(field, onChange, fallbackLabel) {
5465
+ function buildBooleanField(field, onChange, fallbackLabel, settle) {
5349
5466
  const wrap = el$1("div", "aparte-elic-field aparte-elic-boolean");
5350
5467
  const titleId = fieldHeader(wrap, field, fallbackLabel);
5351
5468
  const list = el$1("div", "aparte-elic-options");
5352
- labelGroup(list, { titleId, fallbackLabel });
5353
5469
  const name = `elic-${uuid()}`;
5470
+ const settling = settle != null && field.default == null;
5471
+ labelGroup(list, { commands: settling, titleId, fallbackLabel });
5472
+ if (settling) {
5473
+ const t2 = contextConfig();
5474
+ const mkCommand = (val, label, defaulted) => {
5475
+ const button = commandOption(label);
5476
+ button.addEventListener("click", () => settle?.(val));
5477
+ if (defaulted) {
5478
+ const title = button.querySelector(".aparte-elic-option-title");
5479
+ onRelabel(() => {
5480
+ title.textContent = contextConfig().t(defaulted);
5481
+ });
5482
+ }
5483
+ list.appendChild(button);
5484
+ };
5485
+ mkCommand(true, field.trueLabel ?? t2.t("elicitationYes"), field.trueLabel ? void 0 : "elicitationYes");
5486
+ mkCommand(false, field.falseLabel ?? t2.t("elicitationNo"), field.falseLabel ? void 0 : "elicitationNo");
5487
+ wrap.appendChild(list);
5488
+ return {
5489
+ el: wrap,
5490
+ // Never read: the click settles, so nothing downstream asks this shape for
5491
+ // a value. `false` rather than a throw keeps the field total.
5492
+ getValue: () => false,
5493
+ isComplete: () => false,
5494
+ focus: () => list.querySelector(".aparte-elic-option--command")?.focus(),
5495
+ offersSubmit: () => false
5496
+ };
5497
+ }
5354
5498
  const mk = (val, label, defaulted) => {
5355
- const row = el$1("label", "aparte-elic-option");
5356
- const control = el$1("input", "aparte-elic-control");
5499
+ const row = el$1("label", "aparte-field-choice aparte-elic-option");
5500
+ const control = el$1("input", "aparte-radio aparte-elic-control");
5357
5501
  control.type = "radio";
5358
5502
  control.name = name;
5359
5503
  control.value = val;
@@ -5383,7 +5527,7 @@ function buildBooleanField(field, onChange, fallbackLabel) {
5383
5527
  function buildStringField(field, onChange, fallbackLabel) {
5384
5528
  const wrap = el$1("div", "aparte-elic-field aparte-elic-string");
5385
5529
  fieldHeader(wrap, field, fallbackLabel);
5386
- const input = field.multiline ? el$1("textarea", "aparte-elic-text") : el$1("input", "aparte-elic-text");
5530
+ const input = field.multiline ? el$1("textarea", "aparte-field aparte-field--textarea aparte-elic-text") : el$1("input", "aparte-field aparte-elic-text");
5387
5531
  if (!field.multiline) input.type = "text";
5388
5532
  if (field.placeholder) input.setAttribute("placeholder", field.placeholder);
5389
5533
  if (field.default) input.value = field.default;
@@ -5411,7 +5555,7 @@ function buildStringField(field, onChange, fallbackLabel) {
5411
5555
  focus: () => input.focus()
5412
5556
  };
5413
5557
  }
5414
- function buildField(field, onChange, fallbackLabel, key) {
5558
+ function buildField(field, onChange, fallbackLabel, key, settle) {
5415
5559
  const custom = contextConfig().getElicitationFieldRenderer()?.(field, { key, notifyChange: onChange });
5416
5560
  if (custom) {
5417
5561
  return {
@@ -5423,9 +5567,10 @@ function buildField(field, onChange, fallbackLabel, key) {
5423
5567
  }
5424
5568
  switch (field.type) {
5425
5569
  case "enum":
5426
- return buildEnumField(field, onChange, fallbackLabel);
5570
+ return buildEnumField(field, onChange, fallbackLabel, settle);
5427
5571
  case "boolean":
5428
- return buildBooleanField(field, onChange, fallbackLabel);
5572
+ return buildBooleanField(field, onChange, fallbackLabel, settle);
5573
+ // No `settle`: text is written, not chosen, so it always needs a commit.
5429
5574
  case "string":
5430
5575
  return buildStringField(field, onChange, fallbackLabel);
5431
5576
  }
@@ -5447,6 +5592,16 @@ function buildPanel(message, schema, onChange) {
5447
5592
  const panel = el$1("div", "aparte-elic-panel");
5448
5593
  const asked = typeof message === "function" ? message() : message;
5449
5594
  if (asked) panel.appendChild(el$1("p", "aparte-elic-message", asked));
5595
+ let settleCb = null;
5596
+ let settled = false;
5597
+ const settle = (content) => {
5598
+ if (settled) return;
5599
+ settled = true;
5600
+ settleCb?.(content);
5601
+ };
5602
+ const onSettle = (cb) => {
5603
+ settleCb = cb;
5604
+ };
5450
5605
  const body = el$1("div", "aparte-elic-body");
5451
5606
  panel.appendChild(body);
5452
5607
  const dismiss = el$1("div", "aparte-elic-dismiss");
@@ -5487,15 +5642,20 @@ function buildPanel(message, schema, onChange) {
5487
5642
  mode: () => "submit",
5488
5643
  canProceed: isComplete,
5489
5644
  proceed: () => {
5490
- }
5645
+ },
5646
+ // A form never settles from inside: its questions are collected and
5647
+ // submitted together, and answering the last one on the click would be
5648
+ // WCAG's F36 word for word. Registered so the shape is total; never fired.
5649
+ onSettle
5491
5650
  };
5492
5651
  if (fields.length < 2 || cfg.getElicitationOptions().layout !== "stepped") return api;
5493
5652
  panel.classList.add("aparte-elic-panel--stepped");
5494
5653
  let current = 0;
5495
- const steps = el$1("div", "aparte-elic-steps");
5654
+ const scope = `aparte-elic-${++steppedPanelSeq}`;
5655
+ const steps = el$1("div", "aparte-tabs aparte-tabs--underline aparte-elic-steps");
5496
5656
  steps.setAttribute("role", "tablist");
5497
5657
  const chips = fields.map((f, i) => {
5498
- const chip = el$1("button", "aparte-elic-step");
5658
+ const chip = el$1("button", "aparte-tabs__tab aparte-elic-step");
5499
5659
  chip.type = "button";
5500
5660
  chip.setAttribute("role", "tab");
5501
5661
  chip.textContent = f.header?.trim() || String(i + 1);
@@ -5503,6 +5663,22 @@ function buildPanel(message, schema, onChange) {
5503
5663
  show(i);
5504
5664
  f.field.focus();
5505
5665
  });
5666
+ chip.id = `${scope}-tab-${i}`;
5667
+ f.field.el.id = `${scope}-panel-${i}`;
5668
+ f.field.el.setAttribute("role", "tabpanel");
5669
+ f.field.el.setAttribute("aria-labelledby", chip.id);
5670
+ chip.setAttribute("aria-controls", f.field.el.id);
5671
+ chip.addEventListener("keydown", (e) => {
5672
+ const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
5673
+ let next = -1;
5674
+ if (step) next = (i + step + fields.length) % fields.length;
5675
+ else if (e.key === "Home") next = 0;
5676
+ else if (e.key === "End") next = fields.length - 1;
5677
+ if (next < 0) return;
5678
+ e.preventDefault();
5679
+ show(next);
5680
+ chips[next]?.focus();
5681
+ });
5506
5682
  steps.appendChild(chip);
5507
5683
  return chip;
5508
5684
  });
@@ -5510,6 +5686,7 @@ function buildPanel(message, schema, onChange) {
5510
5686
  syncNav = () => {
5511
5687
  chips.forEach((chip, i) => {
5512
5688
  chip.setAttribute("aria-selected", String(i === current));
5689
+ chip.tabIndex = i === current ? 0 : -1;
5513
5690
  chip.toggleAttribute("data-answered", fields[i].field.isComplete());
5514
5691
  });
5515
5692
  };
@@ -5530,7 +5707,7 @@ function buildPanel(message, schema, onChange) {
5530
5707
  }
5531
5708
  };
5532
5709
  }
5533
- const field = buildField(schema, onChange, asked);
5710
+ const field = buildField(schema, onChange, asked, void 0, settle);
5534
5711
  body.appendChild(field.el);
5535
5712
  return {
5536
5713
  el: panel,
@@ -5538,10 +5715,11 @@ function buildPanel(message, schema, onChange) {
5538
5715
  getContent: () => field.getValue(),
5539
5716
  isComplete: () => field.isComplete(),
5540
5717
  focus: () => field.focus(),
5541
- mode: () => "submit",
5718
+ mode: () => field.offersSubmit?.() ?? true ? "submit" : "none",
5542
5719
  canProceed: () => field.isComplete(),
5543
5720
  proceed: () => {
5544
- }
5721
+ },
5722
+ onSettle
5545
5723
  };
5546
5724
  }
5547
5725
  function el(tag, className, text) {
@@ -5571,7 +5749,8 @@ function buildApprovalPanel(message, options, onChange) {
5571
5749
  const labelled = [];
5572
5750
  const textOf = (option) => typeof option.label === "function" ? option.label() : option.label;
5573
5751
  for (const option of options) {
5574
- const button = el("button", `aparte-approval-option aparte-approval-option--${option.tone ?? "affirm"}`);
5752
+ const tone = option.tone ?? "affirm";
5753
+ const button = el("button", `aparte-btn aparte-btn--block aparte-btn--surface aparte-approval-option aparte-approval-option--${tone}`);
5575
5754
  button.type = "button";
5576
5755
  button.textContent = textOf(option);
5577
5756
  button.addEventListener("click", () => fire({ option: option.value }));
@@ -5675,6 +5854,27 @@ class AparteClient {
5675
5854
  }
5676
5855
  this._setupListeners();
5677
5856
  }
5857
+ /**
5858
+ * What every fresh user action does before it starts a turn.
5859
+ *
5860
+ * Clearing the abort flag has to happen at the START of the action rather than inside
5861
+ * `_streamTurn`, which runs after the auth and file-reading awaits — a reset there
5862
+ * erased an abort that arrived while the user was still waiting for a large attachment
5863
+ * to be read.
5864
+ *
5865
+ * Cancelling the previous turn's tool controllers used to happen only on `aparte-send`.
5866
+ * Retry and edit reset the flag and left them running, so a tool handler from the turn
5867
+ * being superseded stayed alive — with its timeout still counting — while its reply was
5868
+ * already off the active path. A cold audit found the asymmetry; three callers, one
5869
+ * rule, no third copy to fall out of step.
5870
+ */
5871
+ _beginUserTurn() {
5872
+ this._isAborted = false;
5873
+ for (const controller of this._activeToolControllers) {
5874
+ controller.abort();
5875
+ }
5876
+ this._activeToolControllers.clear();
5877
+ }
5678
5878
  /**
5679
5879
  * Sets up the event listeners.
5680
5880
  * This is called once in the constructor.
@@ -5685,11 +5885,7 @@ class AparteClient {
5685
5885
  const event = e;
5686
5886
  if (event.type !== "aparte-send") return;
5687
5887
  if (!this._isForThisInstance(event)) return;
5688
- this._isAborted = false;
5689
- for (const controller of this._activeToolControllers) {
5690
- controller.abort();
5691
- }
5692
- this._activeToolControllers.clear();
5888
+ this._beginUserTurn();
5693
5889
  void this._handleSend(event);
5694
5890
  };
5695
5891
  }
@@ -6039,7 +6235,7 @@ ${summary}`,
6039
6235
  }
6040
6236
  async _handleRetry(event) {
6041
6237
  const { messageId, targetId } = event.detail ?? {};
6042
- this._isAborted = false;
6238
+ this._beginUserTurn();
6043
6239
  if (!messageId) return;
6044
6240
  const targetElement = this._resolveTarget(targetId);
6045
6241
  if (!targetElement) {
@@ -6066,9 +6262,7 @@ ${summary}`,
6066
6262
  if (!provider) return;
6067
6263
  const authConfig = await this._resolveAuth(providerId);
6068
6264
  const chatMessages = this._messagesToChatMessages(historyMessages);
6069
- const systemMessages = [];
6070
- const userSystemPrompt = this._config.resolveSystemPrompt();
6071
- if (userSystemPrompt) systemMessages.push({ role: "system", content: userSystemPrompt });
6265
+ const systemMessages = this._systemMessages();
6072
6266
  await this._streamTurn(
6073
6267
  targetElement,
6074
6268
  newMessageId,
@@ -6079,13 +6273,30 @@ ${summary}`,
6079
6273
  { temperature: 0.4 }
6080
6274
  );
6081
6275
  }
6276
+ /**
6277
+ * The system messages a turn opens with: the app's own template, then the registered
6278
+ * tools' `systemPrompt`s.
6279
+ *
6280
+ * A helper rather than three copies. The two lines it replaces were written out at each
6281
+ * of the three turn entry points (send, retry, edit), which is exactly how the tool half
6282
+ * would have been added to two of them and forgotten in the third — the same shape as
6283
+ * the abort-flag asymmetry `_beginUserTurn` exists to prevent.
6284
+ */
6285
+ _systemMessages() {
6286
+ const messages = [];
6287
+ const appPrompt = this._config.resolveSystemPrompt();
6288
+ if (appPrompt) messages.push({ role: "system", content: appPrompt });
6289
+ const toolPrompts = this._config.resolveToolSystemPrompts();
6290
+ if (toolPrompts) messages.push({ role: "system", content: toolPrompts });
6291
+ return messages;
6292
+ }
6082
6293
  /**
6083
6294
  * Handle aparte-edit — update the user message in place, truncate all subsequent
6084
6295
  * messages, then re-stream a fresh assistant response.
6085
6296
  */
6086
6297
  async _handleEdit(event) {
6087
6298
  const { messageId, content: newContent, targetId } = event.detail ?? {};
6088
- this._isAborted = false;
6299
+ this._beginUserTurn();
6089
6300
  if (!messageId || newContent === void 0) return;
6090
6301
  const targetElement = this._resolveTarget(targetId);
6091
6302
  if (!targetElement) {
@@ -6105,9 +6316,7 @@ ${summary}`,
6105
6316
  }
6106
6317
  const historyMessages = editIdx >= 0 ? allMessages.slice(0, editIdx + 1) : allMessages;
6107
6318
  const chatMessages = this._messagesToChatMessages(historyMessages);
6108
- const systemMessages = [];
6109
- const userSystemPrompt = this._config.resolveSystemPrompt();
6110
- if (userSystemPrompt) systemMessages.push({ role: "system", content: userSystemPrompt });
6319
+ const systemMessages = this._systemMessages();
6111
6320
  const config = this._config.getModelConfig();
6112
6321
  const providerId = config.defaultProvider;
6113
6322
  if (!providerId) return;
@@ -6276,11 +6485,7 @@ ${summary}`,
6276
6485
  * Build the initial messages array, prepending system prompts and conversation history.
6277
6486
  */
6278
6487
  _buildMessages(userContent, target, parts) {
6279
- const messages = [];
6280
- const userSystemPrompt = this._config.resolveSystemPrompt();
6281
- if (userSystemPrompt) {
6282
- messages.push({ role: "system", content: userSystemPrompt });
6283
- }
6488
+ const messages = this._systemMessages();
6284
6489
  const historyOption = this.options.history ?? "viewport";
6285
6490
  const viewportMessages = target?.getMessages?.() ?? [];
6286
6491
  if (historyOption === "viewport") {
@@ -6736,6 +6941,21 @@ ${content}
6736
6941
  ...wholeParser.parse(response).segments,
6737
6942
  ...wholeParser.finalize()
6738
6943
  ];
6944
+ const stringHint = baseRequest._meta?.artifactHint;
6945
+ if (stringHint) {
6946
+ const codeIdx = parsed.findIndex((s) => s.type === "code");
6947
+ if (codeIdx !== -1) {
6948
+ const codeSeg = parsed[codeIdx];
6949
+ parsed[codeIdx] = {
6950
+ id: codeSeg.id,
6951
+ type: "artifact",
6952
+ mimeType: stringHint.mimeType,
6953
+ artifactType: stringHint.kind,
6954
+ title: codeSeg.filename ?? stringHint.kind,
6955
+ content: codeSeg.content
6956
+ };
6957
+ }
6958
+ }
6739
6959
  if (parsed.length > 0) {
6740
6960
  for (const segment of parsed) targetElement.addSegment?.(segment);
6741
6961
  this._updateMessage(targetElement, messageId, { status: "completed" });
@@ -7072,7 +7292,7 @@ ${content}
7072
7292
  details: error.code
7073
7293
  };
7074
7294
  if (target.updateMessage) {
7075
- const rendered = target.getMessages?.()?.find((m) => m.id === messageId)?.segments ?? [];
7295
+ const rendered = target.getMessage?.(messageId)?.segments ?? target.getMessages?.()?.find((m) => m.id === messageId)?.segments ?? [];
7076
7296
  target.updateMessage(messageId, {
7077
7297
  status: "error",
7078
7298
  segments: [...rendered, errorSegment]
@@ -7429,13 +7649,14 @@ export {
7429
7649
  unregisterSegmentRenderer as a5,
7430
7650
  uuid as a6,
7431
7651
  mergeSegmentUpdate as a7,
7432
- stampSegmentActivity as a8,
7433
- stampSegmentOnInsert as a9,
7434
- stampSegmentOnUpdate as aa,
7435
- renumberSegments as ab,
7436
- openSegmentIds as ac,
7437
- isTerminalStatus as ad,
7438
- adoptMessageSegments as ae,
7652
+ writeStreamedMarkdown as a8,
7653
+ stampSegmentActivity as a9,
7654
+ stampSegmentOnInsert as aa,
7655
+ stampSegmentOnUpdate as ab,
7656
+ renumberSegments as ac,
7657
+ openSegmentIds as ad,
7658
+ isTerminalStatus as ae,
7659
+ adoptMessageSegments as af,
7439
7660
  APARTE_DEFAULT_BUBBLE_ACTIONS as b,
7440
7661
  APARTE_DEFAULT_HOST_HANDLERS as c,
7441
7662
  APARTE_DEFAULT_ICON_FALLBACKS as d,
@@ -7462,4 +7683,4 @@ export {
7462
7683
  buildElicitationPanel as y,
7463
7684
  collectRendererStyles as z
7464
7685
  };
7465
- //# sourceMappingURL=is-awaiting-reply-CxWRISBC.js.map
7686
+ //# sourceMappingURL=is-awaiting-reply-DrO_oR32.js.map