@aparte/core 0.23.0 → 0.24.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 (45) hide show
  1. package/dist/client/aparte-client.d.ts +5 -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/conversation-list/aparte-conversation-list.d.ts +38 -0
  5. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  6. package/dist/components/conversation-list/index.d.ts +1 -1
  7. package/dist/components/conversation-list/index.d.ts.map +1 -1
  8. package/dist/components/index.d.ts +1 -1
  9. package/dist/components/index.d.ts.map +1 -1
  10. package/dist/config/aparte-config.d.ts +10 -0
  11. package/dist/config/aparte-config.d.ts.map +1 -1
  12. package/dist/config/locale.d.ts +4 -0
  13. package/dist/config/locale.d.ts.map +1 -1
  14. package/dist/config/tool-table.d.ts +2 -0
  15. package/dist/config/tool-table.d.ts.map +1 -1
  16. package/dist/conversations/conversation-controller.d.ts +19 -0
  17. package/dist/conversations/conversation-controller.d.ts.map +1 -1
  18. package/dist/conversations/conversation-status.d.ts +27 -0
  19. package/dist/conversations/conversation-status.d.ts.map +1 -0
  20. package/dist/custom-elements.json +10301 -9868
  21. package/dist/{dialog-triggers-3G9TqG9g.js → dialog-triggers-BqXRdHWZ.js} +296 -63
  22. package/dist/dialog-triggers-BqXRdHWZ.js.map +1 -0
  23. package/dist/index.css +1 -1
  24. package/dist/index.d.ts +3 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +58 -4
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.node.d.ts +2 -2
  29. package/dist/index.node.d.ts.map +1 -1
  30. package/dist/index.node.js +1 -1
  31. package/dist/index.node.js.map +1 -1
  32. package/dist/interop/element-props.d.ts +1 -1
  33. package/dist/interop/element-props.d.ts.map +1 -1
  34. package/dist/renderers/segments/thinking.d.ts.map +1 -1
  35. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  36. package/dist/types/event-map.d.ts +4 -2
  37. package/dist/types/event-map.d.ts.map +1 -1
  38. package/dist/types/events.d.ts +15 -0
  39. package/dist/types/events.d.ts.map +1 -1
  40. package/dist/types/index.d.ts +2 -2
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/dist/types/tools.d.ts +39 -0
  43. package/dist/types/tools.d.ts.map +1 -1
  44. package/package.json +2 -2
  45. package/dist/dialog-triggers-3G9TqG9g.js.map +0 -1
@@ -173,6 +173,8 @@ const APARTE_DEFAULT_LOCALE = {
173
173
  archiveConversation: "Archive",
174
174
  unarchiveConversation: "Unarchive",
175
175
  conversationActions: "Conversation actions",
176
+ conversationRunning: "Answering",
177
+ conversationWaiting: "Waiting for you",
176
178
  renameConversation: "Rename",
177
179
  conversationTitle: "Conversation title",
178
180
  pinConversation: "Pin",
@@ -1102,12 +1104,20 @@ function createStreamAdapter(opts) {
1102
1104
  };
1103
1105
  let thinkingContent = "";
1104
1106
  let thinkingCollapsed = false;
1107
+ const settleThinking = (fold) => {
1108
+ if (!thinkingId || thinkingCollapsed) return;
1109
+ const chatConfig = typeof Element !== "undefined" && target instanceof Element ? resolveConfig(target) : config;
1110
+ const keepOpen = !fold || chatConfig.getSegmentDefaults("thinking")?.collapsed === false;
1111
+ target.updateSegment?.(thinkingId, keepOpen ? { isStreaming: false } : { collapsed: true, isStreaming: false });
1112
+ thinkingCollapsed = true;
1113
+ };
1105
1114
  return (e) => {
1106
1115
  switch (e.type) {
1107
1116
  case "run-start":
1108
1117
  target.updateMessage?.(messageId, { status: "streaming" });
1109
1118
  break;
1110
1119
  case "turn-start":
1120
+ settleThinking(false);
1111
1121
  parser = new AparteStreamParser({ blocks: config.getStreamBlocks() });
1112
1122
  streaming = /* @__PURE__ */ new Set();
1113
1123
  thinkingId = null;
@@ -1120,7 +1130,8 @@ function createStreamAdapter(opts) {
1120
1130
  const seg = {
1121
1131
  id: `think-${uuid()}`,
1122
1132
  type: "thinking",
1123
- content: thinkingContent
1133
+ content: thinkingContent,
1134
+ isStreaming: true
1124
1135
  };
1125
1136
  thinkingId = seg.id;
1126
1137
  streaming.add(seg.id);
@@ -1131,12 +1142,7 @@ function createStreamAdapter(opts) {
1131
1142
  break;
1132
1143
  }
1133
1144
  case "text-delta": {
1134
- if (thinkingId && !thinkingCollapsed) {
1135
- const chatConfig = typeof Element !== "undefined" && target instanceof Element ? resolveConfig(target) : config;
1136
- const keepOpen = chatConfig.getSegmentDefaults("thinking")?.collapsed === false;
1137
- target.updateSegment?.(thinkingId, keepOpen ? { isStreaming: false } : { collapsed: true, isStreaming: false });
1138
- thinkingCollapsed = true;
1139
- }
1145
+ settleThinking(true);
1140
1146
  const result = parser.parse(e.delta);
1141
1147
  for (const segment of result.segments) {
1142
1148
  if (!streaming.has(segment.id)) {
@@ -1166,6 +1172,7 @@ function createStreamAdapter(opts) {
1166
1172
  break;
1167
1173
  }
1168
1174
  case "tool-start": {
1175
+ settleThinking(false);
1169
1176
  const toolSeg = {
1170
1177
  id: `tool-${e.toolCallId}`,
1171
1178
  type: "tool_call",
@@ -1890,6 +1897,12 @@ class ToolTable {
1890
1897
  }
1891
1898
  return this.any !== void 0 ? { value: this.any, level: "any" } : void 0;
1892
1899
  }
1900
+ /** Every value registered, whatever its matcher. */
1901
+ values() {
1902
+ const out = [...this.exact.values(), ...[...this.patterns.values()].map((p) => p.value)];
1903
+ if (this.any !== void 0) out.push(this.any);
1904
+ return out;
1905
+ }
1893
1906
  clear() {
1894
1907
  this.exact.clear();
1895
1908
  this.patterns.clear();
@@ -1915,6 +1928,7 @@ function renderingVersionOf(cfg) {
1915
1928
  function bumpRenderingVersion(cfg) {
1916
1929
  renderingVersions.set(cfg, renderingVersionOf(cfg) + 1);
1917
1930
  }
1931
+ const ELEMENT_SHAPING = ["hidden", "open", "details", "quiet"];
1918
1932
  function drawingOf(provider, name) {
1919
1933
  if (!provider || !(name in provider) || name in Object.prototype) return void 0;
1920
1934
  const fn = provider[name];
@@ -2943,21 +2957,35 @@ class AparteConfig {
2943
2957
  * @example
2944
2958
  * // A consumer chat: the reply is what matters; the calls stay in the history.
2945
2959
  * aparteGlobalConfig.registerToolDisplay('*', { hidden: true });
2960
+ *
2961
+ * @example
2962
+ * // The line, and no raw JSON behind it: nothing, or a detail of the app's own.
2963
+ * aparteGlobalConfig.registerToolDisplay('read_file', { label: 'Reading a file', details: false });
2964
+ * aparteGlobalConfig.registerToolDisplay('web_search', {
2965
+ * label: 'Searched the web',
2966
+ * details: (segment) => sourcesList(segment.result),
2967
+ * });
2946
2968
  */
2947
2969
  registerToolDisplay(match, display) {
2948
2970
  const before = this._toolDisplays.get(match);
2949
2971
  const next = { ...before, ...display };
2950
2972
  this._toolDisplays.set(match, next);
2951
- if (before?.hidden !== next.hidden || before?.open !== next.open) bumpRenderingVersion(this);
2973
+ const shapes = ELEMENT_SHAPING.some((field) => before?.[field] !== next[field]) || before === void 0 && this._anotherShapes(next);
2974
+ if (shapes) bumpRenderingVersion(this);
2952
2975
  this._notify();
2953
2976
  }
2954
2977
  /** Forget the display registered under this matcher. */
2955
2978
  unregisterToolDisplay(match) {
2956
2979
  const before = this._toolDisplays.get(match);
2957
2980
  this._toolDisplays.delete(match);
2958
- if (before?.hidden !== void 0 || before?.open !== void 0) bumpRenderingVersion(this);
2981
+ const shapes = ELEMENT_SHAPING.some((field) => before?.[field] !== void 0) || before !== void 0 && this._anotherShapes(before);
2982
+ if (shapes) bumpRenderingVersion(this);
2959
2983
  this._notify();
2960
2984
  }
2985
+ /** Whether an entry other than `own` sets a field that shapes a row. */
2986
+ _anotherShapes(own) {
2987
+ return this._toolDisplays.values().some((entry) => entry !== own && ELEMENT_SHAPING.some((field) => entry[field] !== void 0));
2988
+ }
2961
2989
  /** How a tool named `toolName` is shown — the entry that applies to it — or undefined. */
2962
2990
  getToolDisplay(toolName) {
2963
2991
  return this._toolDisplays.resolve(toolName)?.value;
@@ -3406,10 +3434,12 @@ function labelOf(segment) {
3406
3434
  if (ms === void 0) return cfg.t("thought");
3407
3435
  return cfg.t("thoughtFor").replace("{duration}", () => formatDuration(ms, cfg.getLocale().tag || void 0));
3408
3436
  }
3437
+ const arriving = (segment) => segment.isStreaming === true;
3409
3438
  function setLabel(el2, segment) {
3410
3439
  const text = labelOf(segment);
3411
3440
  const label = el2.querySelector(".aparte-thinking-label");
3412
3441
  if (label && label.textContent !== text) label.textContent = text;
3442
+ label?.classList.toggle("aparte-shimmer", arriving(segment));
3413
3443
  el2.querySelector(".aparte-thinking-content")?.setAttribute("aria-label", text);
3414
3444
  }
3415
3445
  const thinkingRenderer = {
@@ -3442,7 +3472,7 @@ const thinkingRenderer = {
3442
3472
  * only part that is genuinely about reasoning rather than about disclosure.
3443
3473
  * `.aparte-thinking-label` stays too: `relabel` below queries it.
3444
3474
  */
3445
- render: (segment) => `<details class="aparte-segment aparte-segment-thinking aparte-accordion__item" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed === false ? "open" : ""}><summary class="aparte-accordion__header aparte-thinking-header"><span class="aparte-thinking-label">${escapeHtml(labelOf(segment))}</span><span class="aparte-accordion__icon">${contextConfig().getIcon("expand")}</span></summary><div class="aparte-accordion__panel aparte-thinking-content" role="region" tabindex="0" aria-label="${escapeAttr(labelOf(segment))}">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
3475
+ 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${escapeAttr(arriving(segment) ? " aparte-shimmer" : "")}">${escapeHtml(labelOf(segment))}</span><span class="aparte-accordion__icon">${contextConfig().getIcon("expand")}</span></summary><div class="aparte-accordion__panel aparte-thinking-content" role="region" tabindex="0" aria-label="${escapeAttr(labelOf(segment))}">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
3446
3476
  /**
3447
3477
  * The default label is the only config-derived text here — and `segment.label`
3448
3478
  * still wins, exactly as in `render`, because that string is the app's.
@@ -3742,6 +3772,72 @@ function outcomeOf(segment) {
3742
3772
  if (segment.status === "rejected" && segment.refusal) return null;
3743
3773
  return { key: "toolOutput", text };
3744
3774
  }
3775
+ function behindOf(segment) {
3776
+ const details = contextConfig().getToolDisplay(segment.toolCall?.name ?? "")?.details;
3777
+ if (details === false) return { kind: "none" };
3778
+ if (typeof details !== "function") return { kind: "parts" };
3779
+ try {
3780
+ const out = details(segment);
3781
+ if (!out) return { kind: "none" };
3782
+ return { kind: "body", body: typeof out === "string" ? contextConfig().sanitizeHtml(out) : out };
3783
+ } catch {
3784
+ return { kind: "none" };
3785
+ }
3786
+ }
3787
+ const appDetailOf = (element) => element.querySelector(':scope > .aparte-tool-detail[data-details="app"]');
3788
+ function setBody(element, body) {
3789
+ const detail = element.querySelector(".aparte-tool-detail");
3790
+ if (!detail) return;
3791
+ if (typeof body === "string") detail.innerHTML = body;
3792
+ else detail.replaceChildren(body);
3793
+ drawnBodies.set(detail, serialized(body));
3794
+ }
3795
+ const drawnBodies = /* @__PURE__ */ new WeakMap();
3796
+ const serialized = (body) => typeof body === "string" ? body : body.outerHTML;
3797
+ function relabelBody(detail, body) {
3798
+ if (typeof body !== "string" && detail.contains(body)) {
3799
+ drawnBodies.set(detail, body.outerHTML);
3800
+ return;
3801
+ }
3802
+ const before = drawnBodies.get(detail);
3803
+ const now = serialized(body);
3804
+ if (before === now) return;
3805
+ const next = document.createElement("div");
3806
+ if (typeof body === "string") next.innerHTML = body;
3807
+ else next.append(body);
3808
+ const prev = document.createElement("div");
3809
+ if (before !== void 0) prev.innerHTML = before;
3810
+ if (before !== void 0 && sameShape(prev, next) && sameShape(prev, detail)) patchChanges(detail, prev, next);
3811
+ else detail.replaceChildren(...Array.from(next.childNodes));
3812
+ drawnBodies.set(detail, now);
3813
+ }
3814
+ function sameShape(a, b) {
3815
+ if (a.childNodes.length !== b.childNodes.length) return false;
3816
+ for (let i = 0; i < a.childNodes.length; i++) {
3817
+ const x = a.childNodes[i];
3818
+ const y = b.childNodes[i];
3819
+ if (x.nodeType !== y.nodeType || x.nodeName !== y.nodeName || !sameShape(x, y)) return false;
3820
+ }
3821
+ return true;
3822
+ }
3823
+ function patchChanges(live, prev, next) {
3824
+ for (let i = 0; i < live.childNodes.length; i++) {
3825
+ const l = live.childNodes[i];
3826
+ const p = prev.childNodes[i];
3827
+ const n = next.childNodes[i];
3828
+ if (l instanceof Element && p instanceof Element && n instanceof Element) {
3829
+ for (const { name, value } of Array.from(n.attributes)) {
3830
+ if (p.getAttribute(name) !== value) l.setAttribute(name, value);
3831
+ }
3832
+ for (const { name } of Array.from(p.attributes)) {
3833
+ if (!n.hasAttribute(name)) l.removeAttribute(name);
3834
+ }
3835
+ patchChanges(l, p, n);
3836
+ } else if (p.nodeValue !== n.nodeValue) {
3837
+ l.nodeValue = n.nodeValue;
3838
+ }
3839
+ }
3840
+ }
3745
3841
  function setNote(element, segment) {
3746
3842
  const el2 = element.querySelector(".aparte-tool-note");
3747
3843
  if (!el2) return;
@@ -3848,6 +3944,23 @@ function iconOf(segment) {
3848
3944
  const key = cfg.getToolDisplay(segment.toolCall?.name ?? "")?.icon;
3849
3945
  return (key ? cfg.getIcon(key) : void 0) || cfg.getIcon("tool");
3850
3946
  }
3947
+ const isQuiet = (segment) => contextConfig().getToolDisplay(segment.toolCall?.name ?? "")?.quiet === true;
3948
+ function quietOf(segment) {
3949
+ const status = segment.status ?? "pending";
3950
+ const quiet = isQuiet(segment);
3951
+ return {
3952
+ shimmer: quiet && status === "pending",
3953
+ spinner: !quiet && status === "pending",
3954
+ stateHidden: quiet && (status === "pending" || status === "resolved")
3955
+ };
3956
+ }
3957
+ function setQuiet(element, segment) {
3958
+ const quiet = quietOf(segment);
3959
+ element.toggleAttribute("data-quiet", isQuiet(segment));
3960
+ element.querySelector(".aparte-tool-spinner")?.toggleAttribute("hidden", !quiet.spinner);
3961
+ element.querySelector(".aparte-tool-name")?.classList.toggle("aparte-shimmer", quiet.shimmer);
3962
+ element.querySelector(".aparte-tool-state")?.classList.toggle("aparte-sr-only", quiet.stateHidden);
3963
+ }
3851
3964
  function setName(element, segment) {
3852
3965
  const el2 = element.querySelector(".aparte-tool-name");
3853
3966
  if (!el2) return;
@@ -3855,68 +3968,85 @@ function setName(element, segment) {
3855
3968
  if (el2.textContent !== text) el2.textContent = text;
3856
3969
  el2.classList.toggle("aparte-tool-name--label", isLabel);
3857
3970
  }
3858
- function pillMarkup(segment, status) {
3971
+ function pillMarkup(segment) {
3859
3972
  const badge = stateBadge(segment);
3860
3973
  const icon = iconOf(segment);
3861
3974
  const target = toolTarget(segment);
3862
3975
  const note = noteOf(segment);
3863
3976
  const name = nameOf(segment);
3977
+ const quiet = quietOf(segment);
3864
3978
  return `
3865
3979
  <span class="aparte-tool-label">
3866
3980
  <span class="aparte-tool-icon">${icon}</span>
3867
- <span class="aparte-tool-name${name.isLabel ? " aparte-tool-name--label" : ""}">${escapeHtml(name.text)}</span>
3981
+ <span class="aparte-tool-name${name.isLabel ? " aparte-tool-name--label" : ""}${quiet.shimmer ? " aparte-shimmer" : ""}">${escapeHtml(name.text)}</span>
3868
3982
  <span class="aparte-tool-target"${target ? "" : " hidden"}>${escapeHtml(target)}</span>
3869
3983
  </span>
3870
- <span class="aparte-spinner aparte-tool-spinner" aria-hidden="true"${status === "pending" ? "" : " hidden"}></span>
3871
- <span class="aparte-tool-state">${badge}</span>
3984
+ <span class="aparte-spinner aparte-tool-spinner" aria-hidden="true"${quiet.spinner ? "" : " hidden"}></span>
3985
+ <span class="aparte-tool-state${quiet.stateHidden ? " aparte-sr-only" : ""}">${badge}</span>
3872
3986
  <span class="aparte-tool-note"${note ? ` title="${escapeAttr(note)}"` : " hidden"}>${escapeHtml(note)}</span>`;
3873
3987
  }
3874
3988
  function placeholderMarkup(segment) {
3875
3989
  return `<div class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(segment.status ?? "pending")}" data-tool-call-id="${escapeAttr(segment.toolCall?.id ?? "")}" data-tool-name="${escapeAttr(segment.toolCall?.name ?? "tool")}" hidden></div>`;
3876
3990
  }
3877
3991
  const isPlaceholder = (element) => element.hidden && element.classList.contains("aparte-segment-tool-call") && element.childElementCount === 0;
3992
+ function drawDefault(segment, behind) {
3993
+ const name = segment.toolCall?.name ?? "tool";
3994
+ const status = segment.status ?? "pending";
3995
+ const toolCallId = segment.toolCall?.id ?? "";
3996
+ const pill = pillMarkup(segment);
3997
+ const quiet = isQuiet(segment) ? " data-quiet" : "";
3998
+ const toggle = contextConfig().getIcon("expand");
3999
+ const input = behind.kind === "parts" ? describeInput(segment) : "";
4000
+ const outcome = behind.kind === "parts" ? outcomeOf(segment) : null;
4001
+ if (behind.kind === "none" || behind.kind === "parts" && !input && !outcome) {
4002
+ return `
4003
+ <div class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}" data-tool-name="${escapeAttr(name)}"${quiet}><div class="aparte-tool-line">${pill}</div>
4004
+ </div>
4005
+ `;
4006
+ }
4007
+ const cfg = contextConfig();
4008
+ const copy = copyButtonMarkup("aparte-tool-copy");
4009
+ const open = segment.collapsed !== void 0 ? !segment.collapsed : cfg.getToolDisplay(name)?.open === true;
4010
+ if (behind.kind === "body") {
4011
+ const tpl = document.createElement("template");
4012
+ tpl.innerHTML = `
4013
+ <details class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}" data-tool-name="${escapeAttr(name)}"${quiet}${open ? " open" : ""}>
4014
+ <summary class="aparte-tool-summary"><span class="aparte-tool-toggle">${toggle}</span>${pill}</summary>
4015
+ <div class="aparte-tool-detail" data-details="app"></div>
4016
+ </details>`.trim();
4017
+ const row = tpl.content.firstElementChild;
4018
+ setBody(row, behind.body);
4019
+ return row;
4020
+ }
4021
+ return `
4022
+ <details class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}" data-tool-name="${escapeAttr(name)}"${quiet}${open ? " open" : ""}>
4023
+ <summary class="aparte-tool-summary"><span class="aparte-tool-toggle">${toggle}</span>${pill}</summary>
4024
+ <div class="aparte-tool-detail">
4025
+ ${input ? `
4026
+ <div class="aparte-tool-part" data-part="input">
4027
+ <div class="aparte-tool-part-head"><span class="aparte-tool-part-label">${escapeHtml(cfg.t("toolInput"))}</span>${copy}</div>
4028
+ <div class="aparte-tool-part-body" role="region" tabindex="0" aria-label="${escapeAttr(cfg.t("toolInput"))}"><pre><code>${escapeHtml(input)}</code></pre></div>
4029
+ </div>` : ""}
4030
+ ${outcome ? `
4031
+ <div class="aparte-tool-part" data-part="output">
4032
+ <div class="aparte-tool-part-head"><span class="aparte-tool-part-label">${escapeHtml(cfg.t(outcome.key))}</span>${copy}</div>
4033
+ <div class="aparte-tool-part-body" role="region" tabindex="0" aria-label="${escapeAttr(cfg.t(outcome.key))}"><pre><code>${escapeHtml(outcome.text)}</code></pre></div>
4034
+ </div>` : ""}
4035
+ </div>
4036
+ </details>
4037
+ `;
4038
+ }
3878
4039
  const toolCallRenderer = {
3879
4040
  type: "tool_call",
3880
4041
  render: (segment) => {
3881
4042
  const name = segment.toolCall?.name ?? "tool";
3882
- const status = segment.status ?? "pending";
3883
- const toolCallId = segment.toolCall?.id ?? "";
3884
4043
  if (contextConfig().isToolHidden(name)) return placeholderMarkup(segment);
3885
4044
  const customRenderer = contextConfig().getToolRenderer(name);
3886
4045
  if (customRenderer) {
3887
4046
  injectToolRendererStyles(name, customRenderer);
3888
4047
  return customRenderer.render(segment) || placeholderMarkup(segment);
3889
4048
  }
3890
- const pill = pillMarkup(segment, status);
3891
- const toggle = contextConfig().getIcon("expand");
3892
- const input = describeInput(segment);
3893
- const outcome = outcomeOf(segment);
3894
- if (!input && !outcome) {
3895
- return `
3896
- <div class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}" data-tool-name="${escapeAttr(name)}">${pill}
3897
- </div>
3898
- `;
3899
- }
3900
- const cfg = contextConfig();
3901
- const copy = copyButtonMarkup("aparte-tool-copy");
3902
- const open = segment.collapsed !== void 0 ? !segment.collapsed : cfg.getToolDisplay(name)?.open === true;
3903
- return `
3904
- <details class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}" data-tool-name="${escapeAttr(name)}"${open ? " open" : ""}>
3905
- <summary class="aparte-tool-summary"><span class="aparte-tool-toggle">${toggle}</span>${pill}</summary>
3906
- <div class="aparte-tool-detail">
3907
- ${input ? `
3908
- <div class="aparte-tool-part" data-part="input">
3909
- <div class="aparte-tool-part-head"><span class="aparte-tool-part-label">${escapeHtml(cfg.t("toolInput"))}</span>${copy}</div>
3910
- <div class="aparte-tool-part-body" role="region" tabindex="0" aria-label="${escapeAttr(cfg.t("toolInput"))}"><pre><code>${escapeHtml(input)}</code></pre></div>
3911
- </div>` : ""}
3912
- ${outcome ? `
3913
- <div class="aparte-tool-part" data-part="output">
3914
- <div class="aparte-tool-part-head"><span class="aparte-tool-part-label">${escapeHtml(cfg.t(outcome.key))}</span>${copy}</div>
3915
- <div class="aparte-tool-part-body" role="region" tabindex="0" aria-label="${escapeAttr(cfg.t(outcome.key))}"><pre><code>${escapeHtml(outcome.text)}</code></pre></div>
3916
- </div>` : ""}
3917
- </div>
3918
- </details>
3919
- `;
4049
+ return drawDefault(segment, behindOf(segment));
3920
4050
  },
3921
4051
  /**
3922
4052
  * The pill's glyphs, and the label it wears while a person is deciding.
@@ -3949,8 +4079,14 @@ const toolCallRenderer = {
3949
4079
  if (outputLabel && outcome) outputLabel.textContent = cfg.t(outcome.key);
3950
4080
  if (outcome) element.querySelector('[data-part="output"] .aparte-tool-part-body')?.setAttribute("aria-label", cfg.t(outcome.key));
3951
4081
  for (const button of element.querySelectorAll(".aparte-tool-copy")) relabelCopyButton(button);
4082
+ const appDetail = appDetailOf(element);
4083
+ if (appDetail) {
4084
+ const behind = behindOf(segment);
4085
+ if (behind.kind === "body") relabelBody(appDetail, behind.body);
4086
+ }
3952
4087
  const statusEl = element.querySelector(".aparte-tool-state");
3953
4088
  if (statusEl) statusEl.innerHTML = stateBadge(segment);
4089
+ setQuiet(element, segment);
3954
4090
  },
3955
4091
  /**
3956
4092
  * Patch in place. NEVER re-render, and never touch `open`.
@@ -3972,32 +4108,45 @@ const toolCallRenderer = {
3972
4108
  element.setAttribute("data-status", status);
3973
4109
  return;
3974
4110
  }
3975
- const input = describeInput(segment);
3976
- const outcome = outcomeOf(segment);
3977
- const wantsDetail = !!input || !!outcome;
3978
4111
  const custom = contextConfig().getToolRenderer(segment.toolCall?.name);
3979
4112
  if (custom?.update && !hidden && !isPlaceholder(element)) {
3980
4113
  custom.update(element, segment);
3981
4114
  return;
3982
4115
  }
3983
- if (custom || hidden || isPlaceholder(element) || wantsDetail !== (element.tagName === "DETAILS")) {
3984
- const out = toolCallRenderer.render(segment);
4116
+ const rebuild = (out) => {
3985
4117
  const tpl = document.createElement("template");
3986
4118
  tpl.innerHTML = typeof out === "string" ? out.trim() : "";
3987
4119
  const rebuilt = typeof out === "string" ? tpl.content.firstElementChild : out;
3988
4120
  if (rebuilt) {
4121
+ if (!rebuilt.hasAttribute("data-segment-id")) rebuilt.setAttribute("data-segment-id", segment.id);
3989
4122
  element.replaceWith(rebuilt);
3990
4123
  toolCallRenderer.setup?.(rebuilt, segment);
3991
4124
  }
4125
+ };
4126
+ if (custom || hidden || isPlaceholder(element)) {
4127
+ rebuild(toolCallRenderer.render(segment));
4128
+ return;
4129
+ }
4130
+ const behind = behindOf(segment);
4131
+ const input = behind.kind === "parts" ? describeInput(segment) : "";
4132
+ const outcome = behind.kind === "parts" ? outcomeOf(segment) : null;
4133
+ const wantsDetail = behind.kind === "body" || !!input || !!outcome;
4134
+ const holdsAppDetail = !!appDetailOf(element);
4135
+ if (wantsDetail !== (element.tagName === "DETAILS") || wantsDetail && behind.kind === "body" !== holdsAppDetail) {
4136
+ rebuild(drawDefault(segment, behind));
3992
4137
  return;
3993
4138
  }
3994
4139
  element.setAttribute("data-status", status);
3995
4140
  const badge = element.querySelector(".aparte-tool-state");
3996
4141
  if (badge) badge.innerHTML = stateBadge(segment);
3997
- element.querySelector(".aparte-tool-spinner")?.toggleAttribute("hidden", status !== "pending");
4142
+ setQuiet(element, segment);
3998
4143
  setName(element, segment);
3999
4144
  setTarget(element, segment);
4000
4145
  setNote(element, segment);
4146
+ if (behind.kind === "body") {
4147
+ setBody(element, behind.body);
4148
+ return;
4149
+ }
4001
4150
  setPart(element, "input", input, "toolInput");
4002
4151
  setPart(element, "output", outcome?.text ?? "", outcome?.key ?? "toolOutput");
4003
4152
  highlightParts(element);
@@ -4607,6 +4756,21 @@ function revokeAttachmentUrls(attachments) {
4607
4756
  if (attachment.url?.startsWith("blob:")) URL.revokeObjectURL(attachment.url);
4608
4757
  }
4609
4758
  }
4759
+ const known = /* @__PURE__ */ new Map();
4760
+ function rememberConversationStatus(id, status) {
4761
+ if (status) known.set(id, status);
4762
+ else known.delete(id);
4763
+ }
4764
+ function tellConversationStatus(id, status) {
4765
+ rememberConversationStatus(id, status);
4766
+ try {
4767
+ window.dispatchEvent(new CustomEvent("aparte-conversation-status", { detail: { id, status } }));
4768
+ } catch {
4769
+ }
4770
+ }
4771
+ function conversationStatusOf(id) {
4772
+ return known.get(id) ?? null;
4773
+ }
4610
4774
  const MAX_COMPACTION_SAVE_WAITS = 6;
4611
4775
  class AparteConversationController {
4612
4776
  _binding;
@@ -4615,6 +4779,10 @@ class AparteConversationController {
4615
4779
  _isStreaming = false;
4616
4780
  _isLoadingConversation = false;
4617
4781
  _ensureInFlight = null;
4782
+ /** The conversation the turn in flight started in, and what the lists were told of it. */
4783
+ _turn = null;
4784
+ /** The reply the turn in flight writes into, from its `aparte-message-start`. */
4785
+ _turnMessage = null;
4618
4786
  _onSendCapture = null;
4619
4787
  _onPathChanged = null;
4620
4788
  _onMessageStart = null;
@@ -4622,6 +4790,7 @@ class AparteConversationController {
4622
4790
  _onMessageError = null;
4623
4791
  _onMessageAborted = null;
4624
4792
  _onMessagePaused = null;
4793
+ _onMessageResumed = null;
4625
4794
  _onSelectConversation = null;
4626
4795
  _onCompactDone = null;
4627
4796
  _unsubscribeManager = null;
@@ -4702,6 +4871,33 @@ class AparteConversationController {
4702
4871
  "[aparte] This chat resolves its own config, but the conversation manager was registered on aparteGlobalConfig — so nothing is being persisted. Pass the same config to init(): `init(adapter, myConfig)`, using the config you gave the chat."
4703
4872
  );
4704
4873
  }
4874
+ /**
4875
+ * Tell every conversation list on the page what the turn is doing, through
4876
+ * `aparte-conversation-status` on `window` — the mirror of the list's
4877
+ * `aparte-conversation-select`, which this controller hears. Always about the
4878
+ * conversation the turn started in: the person may have opened another since. A turn
4879
+ * that starts before its conversation exists (a first message) is told once it does.
4880
+ */
4881
+ _setTurn(status) {
4882
+ if (!status) {
4883
+ if (this._turn) tellConversationStatus(this._turn.id, null);
4884
+ this._turn = null;
4885
+ return;
4886
+ }
4887
+ const id = this._turn?.id ?? (status === "running" ? this._activeId ?? this._manager()?.activeId ?? null : null);
4888
+ if (!id) return;
4889
+ this._turn = { id, status };
4890
+ tellConversationStatus(id, status);
4891
+ }
4892
+ /**
4893
+ * Whether a lifecycle event is about the turn in flight. A turn a newer send replaced
4894
+ * (a chip, a retry, an edit) says it stopped AFTER the new one started: its end must not
4895
+ * end the new turn's state. An event without a message id is taken as this turn's.
4896
+ */
4897
+ _ofThisTurn(e) {
4898
+ const id = e.detail?.messageId;
4899
+ return !id || !this._turnMessage || id === this._turnMessage;
4900
+ }
4705
4901
  /**
4706
4902
  * Attach event listeners to the binding host. Returns an unbind function.
4707
4903
  */
@@ -4732,29 +4928,42 @@ class AparteConversationController {
4732
4928
  }
4733
4929
  };
4734
4930
  host.addEventListener("aparte-path-changed", this._onPathChanged);
4735
- this._onMessageStart = () => {
4931
+ this._onMessageStart = (e) => {
4736
4932
  this._isStreaming = true;
4933
+ this._turnMessage = e.detail?.messageId ?? null;
4934
+ this._setTurn("running");
4737
4935
  };
4738
- this._onMessageDone = () => {
4936
+ const ended = (e) => {
4937
+ if (!this._ofThisTurn(e)) return;
4739
4938
  this._isStreaming = false;
4939
+ this._setTurn(null);
4940
+ this._turnMessage = null;
4941
+ };
4942
+ this._onMessageDone = (e) => {
4943
+ ended(e);
4740
4944
  queueMicrotask(() => this._persistActive());
4741
4945
  };
4742
- this._onMessageError = () => {
4743
- this._isStreaming = false;
4946
+ this._onMessageError = (e) => {
4947
+ ended(e);
4744
4948
  queueMicrotask(() => this._persistActive());
4745
4949
  };
4746
- this._onMessageAborted = () => {
4747
- this._isStreaming = false;
4950
+ this._onMessageAborted = (e) => {
4951
+ ended(e);
4748
4952
  queueMicrotask(() => this._persistActive());
4749
4953
  };
4750
- this._onMessagePaused = () => {
4954
+ this._onMessagePaused = (e) => {
4955
+ if (this._ofThisTurn(e)) this._setTurn("waiting");
4751
4956
  queueMicrotask(() => this._persistActive());
4752
4957
  };
4958
+ this._onMessageResumed = (e) => {
4959
+ if (this._ofThisTurn(e)) this._setTurn("running");
4960
+ };
4753
4961
  host.addEventListener("aparte-message-start", this._onMessageStart);
4754
4962
  host.addEventListener("aparte-message-done", this._onMessageDone);
4755
4963
  host.addEventListener("aparte-message-error", this._onMessageError);
4756
4964
  host.addEventListener("aparte-message-aborted", this._onMessageAborted);
4757
4965
  host.addEventListener("aparte-message-paused", this._onMessagePaused);
4966
+ host.addEventListener("aparte-message-resumed", this._onMessageResumed);
4758
4967
  this._onSelectConversation = (e) => {
4759
4968
  const evt = e;
4760
4969
  const targetId = evt.detail?.targetId;
@@ -4781,6 +4990,9 @@ class AparteConversationController {
4781
4990
  this._bound = false;
4782
4991
  this._subscribedTo = null;
4783
4992
  this._persistActive();
4993
+ if (this._turn?.status === "running") this._setTurn(null);
4994
+ this._turn = null;
4995
+ this._turnMessage = null;
4784
4996
  this._stopHydrationRetry?.();
4785
4997
  this._stopHydrationRetry = null;
4786
4998
  const host = this._binding.host;
@@ -4812,6 +5024,10 @@ class AparteConversationController {
4812
5024
  host.removeEventListener("aparte-message-paused", this._onMessagePaused);
4813
5025
  this._onMessagePaused = null;
4814
5026
  }
5027
+ if (this._onMessageResumed) {
5028
+ host.removeEventListener("aparte-message-resumed", this._onMessageResumed);
5029
+ this._onMessageResumed = null;
5030
+ }
4815
5031
  if (this._onCompactDone && typeof window !== "undefined") {
4816
5032
  window.removeEventListener("aparte-compact-done", this._onCompactDone);
4817
5033
  this._onCompactDone = null;
@@ -4840,6 +5056,8 @@ class AparteConversationController {
4840
5056
  const isSwitch = id !== this._activeId;
4841
5057
  const manager = this._manager();
4842
5058
  if (isSwitch && this._isStreaming) {
5059
+ if (this._turn?.status === "waiting") this._turn = null;
5060
+ else this._setTurn(null);
4843
5061
  this._persistActive();
4844
5062
  try {
4845
5063
  window.dispatchEvent(new CustomEvent("aparte-abort", {
@@ -5021,6 +5239,7 @@ class AparteConversationController {
5021
5239
  }
5022
5240
  this._activeId = convId;
5023
5241
  this._options.onConversationCreated?.(convId);
5242
+ if (this._isStreaming && !this._turn) this._setTurn("running");
5024
5243
  } else if (this._activeId !== null && this._activeId !== convId) {
5025
5244
  await manager.addMessage(convId, userMsg).catch(() => {
5026
5245
  });
@@ -6685,6 +6904,7 @@ class AparteClient {
6685
6904
  return;
6686
6905
  }
6687
6906
  dispatchLifecycleEvent(targetElement, "aparte-message-start", { messageId, role: "assistant" }, chatId);
6907
+ if (opts?.resume) dispatchLifecycleEvent(targetElement, "aparte-message-paused", { messageId }, chatId);
6688
6908
  if (!opts?.resume && provider.id) {
6689
6909
  this._updateMessage(targetElement, messageId, {
6690
6910
  model: {
@@ -6697,6 +6917,7 @@ class AparteClient {
6697
6917
  const streamController = new AbortController();
6698
6918
  if (this._streamController !== streamController) this._streamController?.abort();
6699
6919
  this._streamController = streamController;
6920
+ this._turnsInFlight.add(messageId);
6700
6921
  try {
6701
6922
  let result = opts?.resume ? { usage: void 0, suspended: opts.resume } : await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController);
6702
6923
  const maxResumes = this.options.maxTurns ?? 10;
@@ -6728,6 +6949,7 @@ class AparteClient {
6728
6949
  };
6729
6950
  const messages2 = this._resumeMessages(targetElement, messageId, baseRequest.messages);
6730
6951
  this._spendPause(targetElement, messageId);
6952
+ dispatchLifecycleEvent(targetElement, "aparte-message-resumed", { messageId }, chatId);
6731
6953
  result = await this._streamLoop(
6732
6954
  targetElement,
6733
6955
  messageId,
@@ -6754,8 +6976,15 @@ class AparteClient {
6754
6976
  }
6755
6977
  const aparteError = AparteError.from(error, AparteErrorCode.UNKNOWN_ERROR);
6756
6978
  this._handleLifecycleError(targetElement, messageId, aparteError);
6979
+ } finally {
6980
+ this._turnsInFlight.delete(messageId);
6757
6981
  }
6758
6982
  }
6983
+ /**
6984
+ * Replies a turn is running into right now, a pause it waits on included: a second
6985
+ * load of their conversation must not start them again (`resumeDeferred`).
6986
+ */
6987
+ _turnsInFlight = /* @__PURE__ */ new Set();
6759
6988
  /**
6760
6989
  * Handle aparte-retry — add a sibling branch to the assistant message and re-stream
6761
6990
  * using the same conversation history minus the retried reply.
@@ -7627,7 +7856,7 @@ ${content}
7627
7856
  const last = [...messages].reverse().find((m) => m.role === "assistant");
7628
7857
  const open = (last?.segments ?? []).filter(isOpenDeferred);
7629
7858
  const ticket = open.find((s) => s.deferred?.ticket)?.deferred?.ticket;
7630
- if (!last || open.length === 0 || !ticket || this._resuming.has(last.id)) return false;
7859
+ if (!last || open.length === 0 || !ticket || this._resuming.has(last.id) || this._turnsInFlight.has(last.id)) return false;
7631
7860
  const config = this._config.getModelConfig();
7632
7861
  const recorded = last.model && this._config.getAIProvider(last.model.providerId) ? last.model : void 0;
7633
7862
  const providerId = recorded?.providerId ?? config.defaultProvider;
@@ -8060,6 +8289,7 @@ const APARTE_DEFAULT_UI_EVENTS = [
8060
8289
  "aparte-message-error",
8061
8290
  "aparte-message-aborted",
8062
8291
  "aparte-message-paused",
8292
+ "aparte-message-resumed",
8063
8293
  "aparte-tool-approval-request"
8064
8294
  ];
8065
8295
  function applyElementProps(el2, props, transformValue = (value) => value) {
@@ -8188,8 +8418,11 @@ export {
8188
8418
  isTerminalStatus as al,
8189
8419
  adoptMessageSegments as am,
8190
8420
  APARTE_DERIVED_SEGMENTS as an,
8191
- chatBoundaryOf as ao,
8192
- rootOf as ap,
8421
+ rememberConversationStatus as ao,
8422
+ tellConversationStatus as ap,
8423
+ conversationStatusOf as aq,
8424
+ chatBoundaryOf as ar,
8425
+ rootOf as as,
8193
8426
  APARTE_DEFAULT_BUBBLE_ACTIONS as b,
8194
8427
  APARTE_DEFAULT_HOST_HANDLERS as c,
8195
8428
  APARTE_DEFAULT_ICON_FALLBACKS as d,
@@ -8216,4 +8449,4 @@ export {
8216
8449
  collectRendererStyles as y,
8217
8450
  contentToText as z
8218
8451
  };
8219
- //# sourceMappingURL=dialog-triggers-3G9TqG9g.js.map
8452
+ //# sourceMappingURL=dialog-triggers-BqXRdHWZ.js.map