@aparte/core 0.21.0 → 0.23.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 (50) hide show
  1. package/dist/client/aparte-client.d.ts +5 -22
  2. package/dist/client/aparte-client.d.ts.map +1 -1
  3. package/dist/client/stream-adapter.d.ts +16 -0
  4. package/dist/client/stream-adapter.d.ts.map +1 -1
  5. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  6. package/dist/components/viewport/aparte-chat-viewport.d.ts +10 -1
  7. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  8. package/dist/config/aparte-config.d.ts +66 -19
  9. package/dist/config/aparte-config.d.ts.map +1 -1
  10. package/dist/config/error-renderer.d.ts +3 -1
  11. package/dist/config/error-renderer.d.ts.map +1 -1
  12. package/dist/config/locale.d.ts +45 -4
  13. package/dist/config/locale.d.ts.map +1 -1
  14. package/dist/config/tool-table.d.ts +28 -0
  15. package/dist/config/tool-table.d.ts.map +1 -0
  16. package/dist/custom-elements.json +11043 -10524
  17. package/dist/{dialog-triggers-3Rk7PuZa.js → dialog-triggers-3G9TqG9g.js} +473 -165
  18. package/dist/dialog-triggers-3G9TqG9g.js.map +1 -0
  19. package/dist/index.css +1 -1
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +35 -16
  23. package/dist/index.js.map +1 -1
  24. package/dist/index.node.d.ts +2 -1
  25. package/dist/index.node.d.ts.map +1 -1
  26. package/dist/index.node.js +4 -3
  27. package/dist/index.node.js.map +1 -1
  28. package/dist/renderers/copy-button.d.ts +12 -0
  29. package/dist/renderers/copy-button.d.ts.map +1 -0
  30. package/dist/renderers/segment-renderers.d.ts +6 -3
  31. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  32. package/dist/renderers/segments/code.d.ts.map +1 -1
  33. package/dist/renderers/segments/error.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/transport/agent-handler.d.ts.map +1 -1
  37. package/dist/types/errors.d.ts +2 -0
  38. package/dist/types/errors.d.ts.map +1 -1
  39. package/dist/types/index.d.ts +1 -1
  40. package/dist/types/index.d.ts.map +1 -1
  41. package/dist/types/segments.d.ts +42 -2
  42. package/dist/types/segments.d.ts.map +1 -1
  43. package/dist/types/tools.d.ts +38 -0
  44. package/dist/types/tools.d.ts.map +1 -1
  45. package/dist/utils/format-duration.d.ts +9 -0
  46. package/dist/utils/format-duration.d.ts.map +1 -0
  47. package/dist/utils/tool-line.d.ts +23 -0
  48. package/dist/utils/tool-line.d.ts.map +1 -0
  49. package/package.json +2 -2
  50. package/dist/dialog-triggers-3Rk7PuZa.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { unansweredTail, runStreamAgent } from "@aparte/engine";
1
+ import { toolRefusal as toolRefusal$1, unansweredTail, runStreamAgent } from "@aparte/engine";
2
2
  import { A as APARTE_ICON_GLYPHS } from "./glyphs-C8z2oJUg.js";
3
3
  function contentToText(content) {
4
4
  if (typeof content === "string") return content;
@@ -18,6 +18,7 @@ var AparteErrorCode = /* @__PURE__ */ ((AparteErrorCode2) => {
18
18
  AparteErrorCode2["PROVIDER_ERROR"] = "PROVIDER_ERROR";
19
19
  AparteErrorCode2["PROVIDER_UNAVAILABLE"] = "PROVIDER_UNAVAILABLE";
20
20
  AparteErrorCode2["PROVIDER_POLICY"] = "PROVIDER_POLICY";
21
+ AparteErrorCode2["MAX_TURNS_EXCEEDED"] = "MAX_TURNS_EXCEEDED";
21
22
  AparteErrorCode2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
22
23
  return AparteErrorCode2;
23
24
  })(AparteErrorCode || {});
@@ -98,7 +99,7 @@ const APARTE_DEFAULT_LOCALE = {
98
99
  elicitationNo: "No",
99
100
  elicitationAnswerLabel: "Your answer",
100
101
  approvalInstructionPlaceholder: "Or tell it what to do instead…",
101
- approvalAsk: "Run {tool}?",
102
+ approvalAsk: "Allow {tool}?",
102
103
  approvalWaiting: "Waiting",
103
104
  scrollToBottom: "Scroll to bottom",
104
105
  suggestionsLabel: "Suggested prompts",
@@ -111,6 +112,18 @@ const APARTE_DEFAULT_LOCALE = {
111
112
  toolFailed: "Failed",
112
113
  toolInput: "Input",
113
114
  toolOutput: "Output",
115
+ toolRejectedSaid: "You said: “{instruction}”",
116
+ toolRefusedByPolicy: "Refused by policy",
117
+ toolNotRunAfterRefusal: "Not run: an earlier call was refused",
118
+ toolTimedOut: "Timed out",
119
+ toolUnavailable: "Not available here",
120
+ toolNoApprover: "No one could approve it",
121
+ toolTurnLimit: "Turn limit reached",
122
+ maxTurnsReached: "Stopped after {n} turns to avoid an endless loop.",
123
+ questionDeclined: "You declined to answer.",
124
+ artifactNotCreated: "Not created",
125
+ thoughtFor: "Thought for {duration}",
126
+ thought: "Thought",
114
127
  approvalOptionsLabel: "Your decision",
115
128
  approvalArgsLabel: "Arguments",
116
129
  actionUpload: "Attach file",
@@ -1076,13 +1089,13 @@ function dispatchLifecycleEvent(target, name, detail, targetId) {
1076
1089
  const stamped = detail && typeof detail === "object" ? { targetId: id, ...detail } : detail;
1077
1090
  target.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail: stamped }));
1078
1091
  }
1092
+ const toolRefusal = toolRefusal$1;
1079
1093
  function createStreamAdapter(opts) {
1080
1094
  const { target, config, messageId, targetId } = opts;
1081
1095
  let parser = new AparteStreamParser({ blocks: config.getStreamBlocks() });
1082
1096
  let streaming = /* @__PURE__ */ new Set();
1083
1097
  let thinkingId = null;
1084
1098
  const toolSegments = /* @__PURE__ */ new Map();
1085
- const addedTools = /* @__PURE__ */ new Set();
1086
1099
  const settle = (toolCallId, update) => {
1087
1100
  const id = `tool-${toolCallId}`;
1088
1101
  target.updateSegment?.(id, toolSegments.has(id) ? update : { ...update, deferred: void 0 });
@@ -1161,17 +1174,8 @@ function createStreamAdapter(opts) {
1161
1174
  };
1162
1175
  toolSegments.set(toolSeg.id, toolSeg);
1163
1176
  const renderer = config.getToolRenderer(e.name);
1164
- if (renderer) {
1165
- injectToolRendererStyles(e.name, renderer);
1166
- const html = renderer.render(toolSeg);
1167
- if (html) {
1168
- target.addSegment?.(toolSeg);
1169
- addedTools.add(toolSeg.id);
1170
- }
1171
- } else {
1172
- target.addSegment?.(toolSeg);
1173
- addedTools.add(toolSeg.id);
1174
- }
1177
+ if (renderer) injectToolRendererStyles(e.name, renderer);
1178
+ target.addSegment?.(toolSeg);
1175
1179
  break;
1176
1180
  }
1177
1181
  case "tool-awaiting-approval":
@@ -1182,7 +1186,11 @@ function createStreamAdapter(opts) {
1182
1186
  settle(e.toolCallId, { status: "pending" });
1183
1187
  break;
1184
1188
  case "tool-rejected":
1185
- settle(e.toolCallId, { status: "rejected", result: e.reason });
1189
+ settle(e.toolCallId, {
1190
+ status: "rejected",
1191
+ result: e.reason,
1192
+ ...e.by ? { refusal: e.instruction ? { by: e.by, instruction: e.instruction } : { by: e.by } } : {}
1193
+ });
1186
1194
  break;
1187
1195
  case "tool-resolved":
1188
1196
  settle(e.toolCallId, {
@@ -1192,7 +1200,7 @@ function createStreamAdapter(opts) {
1192
1200
  });
1193
1201
  break;
1194
1202
  case "tool-aborted":
1195
- settle(e.toolCallId, { status: "aborted" });
1203
+ settle(e.toolCallId, e.cause ? { status: "aborted", abortCause: e.cause } : { status: "aborted" });
1196
1204
  break;
1197
1205
  case "tool-failed":
1198
1206
  settle(e.toolCallId, { status: "failed", result: e.error });
@@ -1202,11 +1210,14 @@ function createStreamAdapter(opts) {
1202
1210
  target.addSegment?.({
1203
1211
  id: `max-turns-${uuid()}`,
1204
1212
  type: "error",
1205
- content: `Stopped after ${e.limit} tool calls to prevent an infinite loop.`,
1206
- details: "MAX_TURNS_EXCEEDED"
1213
+ // The CHAT's text, in its language — resolved from the target, as the
1214
+ // reasoning default is: a client on the global config also answers a
1215
+ // chat with its own.
1216
+ content: (typeof Element !== "undefined" && target instanceof Element ? resolveConfig(target) : config).t("maxTurnsReached").replace("{n}", () => String(e.limit)),
1217
+ code: AparteErrorCode.MAX_TURNS_EXCEEDED
1207
1218
  });
1208
1219
  } else if (e.toolCallId) {
1209
- settle(e.toolCallId, { status: "aborted" });
1220
+ settle(e.toolCallId, { status: "aborted", abortCause: "turn-limit" });
1210
1221
  }
1211
1222
  break;
1212
1223
  case "run-aborted":
@@ -1217,15 +1228,7 @@ function createStreamAdapter(opts) {
1217
1228
  for (const p of e.pending) {
1218
1229
  const id = `tool-${p.toolCallId}`;
1219
1230
  const deferred = { reason: p.reason, ...typeof ticket === "string" ? { ticket } : {} };
1220
- if (addedTools.has(id) || !toolSegments.has(id)) {
1221
- target.updateSegment?.(id, { deferred });
1222
- } else {
1223
- const seg = toolSegments.get(id);
1224
- if (seg) {
1225
- target.addSegment?.({ ...seg, deferred });
1226
- addedTools.add(id);
1227
- }
1228
- }
1231
+ target.updateSegment?.(id, { deferred });
1229
1232
  }
1230
1233
  target.updateMessage?.(messageId, { status: "completed" });
1231
1234
  if (e.usage) {
@@ -1494,7 +1497,8 @@ function createAparteAgentHandler(options) {
1494
1497
  // Reached only for a server `deny`: an `ask` is deferred to the page.
1495
1498
  approvalResolver: async (call) => ({
1496
1499
  approved: false,
1497
- reason: rule(call)?.reason?.trim() || "Tool execution was refused by the server policy."
1500
+ reason: rule(call)?.reason?.trim() || "Tool execution was refused by the server policy.",
1501
+ by: "policy"
1498
1502
  }),
1499
1503
  deferral: { outcomes },
1500
1504
  emitter,
@@ -1848,6 +1852,50 @@ function chatBoundaryOf(el2) {
1848
1852
  }
1849
1853
  return null;
1850
1854
  }
1855
+ class ToolTable {
1856
+ exact = /* @__PURE__ */ new Map();
1857
+ // Keyed by the pattern's spelling, so two RegExp instances of one pattern are one
1858
+ // entry; a Map keeps first-insertion order, which is the order patterns are tried in.
1859
+ patterns = /* @__PURE__ */ new Map();
1860
+ any;
1861
+ /** The value registered under exactly this matcher — not a resolution. */
1862
+ get(match) {
1863
+ if (match instanceof RegExp) return this.patterns.get(String(match))?.value;
1864
+ return match === "*" ? this.any : this.exact.get(match);
1865
+ }
1866
+ set(match, value) {
1867
+ if (match instanceof RegExp) {
1868
+ const key = String(match);
1869
+ const entry = this.patterns.get(key);
1870
+ if (entry) entry.value = value;
1871
+ else this.patterns.set(key, { re: match, value });
1872
+ } else if (match === "*") {
1873
+ this.any = value;
1874
+ } else {
1875
+ this.exact.set(match, value);
1876
+ }
1877
+ }
1878
+ delete(match) {
1879
+ if (match instanceof RegExp) this.patterns.delete(String(match));
1880
+ else if (match === "*") this.any = void 0;
1881
+ else this.exact.delete(match);
1882
+ }
1883
+ /** The value that applies to a tool named `name`, and how specifically it names it. */
1884
+ resolve(name) {
1885
+ if (typeof name !== "string") return void 0;
1886
+ const own = this.exact.get(name);
1887
+ if (own !== void 0) return { value: own, level: "exact" };
1888
+ for (const { re, value } of this.patterns.values()) {
1889
+ if (name.search(re) !== -1) return { value, level: "pattern" };
1890
+ }
1891
+ return this.any !== void 0 ? { value: this.any, level: "any" } : void 0;
1892
+ }
1893
+ clear() {
1894
+ this.exact.clear();
1895
+ this.patterns.clear();
1896
+ this.any = void 0;
1897
+ }
1898
+ }
1851
1899
  const APARTE_DEFAULT_BUBBLE_ACTIONS = {
1852
1900
  copy: true,
1853
1901
  retry: false,
@@ -1975,9 +2023,9 @@ class AparteConfig {
1975
2023
  _elicitationQueue = /* @__PURE__ */ new Map();
1976
2024
  // Tool Registry
1977
2025
  _tools = /* @__PURE__ */ new Map();
1978
- _toolRenderers = /* @__PURE__ */ new Map();
1979
- _toolSummaries = /* @__PURE__ */ new Map();
1980
- _toolInputFormatters = /* @__PURE__ */ new Map();
2026
+ // By exact name, pattern or '*' — see `tool-table.ts` for the one resolution rule.
2027
+ _toolRenderers = new ToolTable();
2028
+ _toolDisplays = new ToolTable();
1981
2029
  /** Tagged blocks the stream parser recognises in the prose — see {@link registerStreamBlock}. */
1982
2030
  _streamBlocks = /* @__PURE__ */ new Map();
1983
2031
  _segmentDefaults = /* @__PURE__ */ new Map();
@@ -2810,16 +2858,20 @@ class AparteConfig {
2810
2858
  * instead — see [Custom tool
2811
2859
  * renderer](https://apartejs.dev/guides/tools/#custom-tool-renderer).
2812
2860
  *
2813
- * @example
2814
- * // Hide the segment entirely (UI-only tool like ask_user)
2815
- * aparteGlobalConfig.registerToolRenderer('ask_user', { render: () => '' });
2861
+ * The matcher is an exact name, a RegExp, or `'*'` — the same rule as
2862
+ * {@link registerToolDisplay}: the exact name wins, then the first pattern registered,
2863
+ * then `'*'`, which is one renderer for every tool without its own.
2816
2864
  *
2817
2865
  * @example
2818
2866
  * // Custom pill for a web-search tool
2819
2867
  * aparteGlobalConfig.registerToolRenderer('web_search', { render: (seg) => `<div class="aparte-tool-label">Searching...</div>` });
2868
+ *
2869
+ * @example
2870
+ * // One look for every tool an MCP server brings
2871
+ * aparteGlobalConfig.registerToolRenderer(/^mcp__/, { render: (seg) => myMcpCard(seg) });
2820
2872
  */
2821
- registerToolRenderer(toolName, renderer) {
2822
- this._toolRenderers.set(toolName, renderer);
2873
+ registerToolRenderer(match, renderer) {
2874
+ this._toolRenderers.set(match, renderer);
2823
2875
  }
2824
2876
  /**
2825
2877
  * Field defaults for a segment TYPE, filled in when a segment enters a message.
@@ -2859,13 +2911,79 @@ class AparteConfig {
2859
2911
  clearSegmentDefaults(type) {
2860
2912
  this._segmentDefaults.delete(type);
2861
2913
  }
2862
- /** Unregister a per-tool renderer */
2863
- unregisterToolRenderer(toolName) {
2864
- this._toolRenderers.delete(toolName);
2914
+ /** Unregister the renderer registered under this matcher. */
2915
+ unregisterToolRenderer(match) {
2916
+ this._toolRenderers.delete(match);
2865
2917
  }
2866
- /** Get the renderer for a specific tool name. Returns undefined if none registered. */
2918
+ /** The renderer that draws a tool named `toolName` — exact, pattern or `'*'` — or undefined. */
2867
2919
  getToolRenderer(toolName) {
2868
- return this._toolRenderers.get(toolName);
2920
+ return this._toolRenderers.resolve(toolName)?.value;
2921
+ }
2922
+ /**
2923
+ * Say how a tool's calls are shown: a label in place of its identifier, an icon, what
2924
+ * the call targets, how its arguments read, whether its row is drawn at all, and
2925
+ * whether it opens.
2926
+ *
2927
+ * The matcher is an exact name, a RegExp, or `'*'` — "every tool nothing more
2928
+ * specific describes". One entry per matcher, and calls MERGE into it; the entry that
2929
+ * applies to a tool is the exact name's, else the first pattern registered that
2930
+ * matches, else `'*'`'s — whole, never a mix of two. Registering tells the chats on
2931
+ * the page, so a label reaches rows already drawn.
2932
+ *
2933
+ * Core never makes a label up: without one the row shows the identifier, which is the
2934
+ * app's wire format.
2935
+ *
2936
+ * @example
2937
+ * aparteGlobalConfig.registerToolDisplay('web_search', {
2938
+ * label: 'Web search',
2939
+ * icon: 'search',
2940
+ * summary: (segment) => String(segment.toolCall.input['query'] ?? ''),
2941
+ * });
2942
+ *
2943
+ * @example
2944
+ * // A consumer chat: the reply is what matters; the calls stay in the history.
2945
+ * aparteGlobalConfig.registerToolDisplay('*', { hidden: true });
2946
+ */
2947
+ registerToolDisplay(match, display) {
2948
+ const before = this._toolDisplays.get(match);
2949
+ const next = { ...before, ...display };
2950
+ this._toolDisplays.set(match, next);
2951
+ if (before?.hidden !== next.hidden || before?.open !== next.open) bumpRenderingVersion(this);
2952
+ this._notify();
2953
+ }
2954
+ /** Forget the display registered under this matcher. */
2955
+ unregisterToolDisplay(match) {
2956
+ const before = this._toolDisplays.get(match);
2957
+ this._toolDisplays.delete(match);
2958
+ if (before?.hidden !== void 0 || before?.open !== void 0) bumpRenderingVersion(this);
2959
+ this._notify();
2960
+ }
2961
+ /** How a tool named `toolName` is shown — the entry that applies to it — or undefined. */
2962
+ getToolDisplay(toolName) {
2963
+ return this._toolDisplays.resolve(toolName)?.value;
2964
+ }
2965
+ /**
2966
+ * Whether a tool's calls draw nothing: its display says `hidden`, and no renderer is
2967
+ * registered for it MORE specifically. A `'*'` hide therefore leaves a tool with its
2968
+ * own renderer drawn (a plugin's receipt), while a hide as specific as the renderer
2969
+ * wins — the app said so for that tool. A hidden call stays in the message, in storage
2970
+ * and in the model's history; only its row is empty.
2971
+ */
2972
+ isToolHidden(toolName) {
2973
+ const display = this._toolDisplays.resolve(toolName);
2974
+ if (!display?.value.hidden) return false;
2975
+ const renderer = this._toolRenderers.resolve(toolName);
2976
+ const rank = { exact: 2, pattern: 1, any: 0 };
2977
+ return !renderer || rank[renderer.level] <= rank[display.level];
2978
+ }
2979
+ /** Drop one field of the entry under `match`, and the entry when nothing is left. */
2980
+ _dropToolDisplayField(match, field) {
2981
+ const entry = this._toolDisplays.get(match);
2982
+ if (!entry || !(field in entry)) return;
2983
+ const { [field]: _dropped, ...rest } = entry;
2984
+ if (Object.keys(rest).length) this._toolDisplays.set(match, rest);
2985
+ else this._toolDisplays.delete(match);
2986
+ this._notify();
2869
2987
  }
2870
2988
  /**
2871
2989
  * Say what a call targets, in the default row, after the tool's name.
@@ -2882,42 +3000,46 @@ class AparteConfig {
2882
3000
  * on every config change — so registering one after a row is on screen shows up on
2883
3001
  * that row's next update.
2884
3002
  *
3003
+ * The `summary` field of {@link registerToolDisplay}, on its own: same matcher, same
3004
+ * entry.
3005
+ *
2885
3006
  * @example
2886
3007
  * // read_file skills.json
2887
3008
  * aparteGlobalConfig.registerToolSummary('read_file', (segment) =>
2888
3009
  * String(segment.toolCall.input['path'] ?? ''));
2889
3010
  */
2890
- registerToolSummary(toolName, summary) {
2891
- this._toolSummaries.set(toolName, summary);
3011
+ registerToolSummary(match, summary) {
3012
+ this.registerToolDisplay(match, { summary });
2892
3013
  }
2893
- /** Unregister a per-tool summary. */
2894
- unregisterToolSummary(toolName) {
2895
- this._toolSummaries.delete(toolName);
3014
+ /** Drop the summary registered under this matcher; the rest of its display stays. */
3015
+ unregisterToolSummary(match) {
3016
+ this._dropToolDisplayField(match, "summary");
2896
3017
  }
2897
- /** Get the summary for a specific tool name. Returns undefined if none registered. */
3018
+ /** The summary that applies to a tool named `toolName`, or undefined. */
2898
3019
  getToolSummary(toolName) {
2899
- return this._toolSummaries.get(toolName);
3020
+ return this.getToolDisplay(toolName)?.summary;
2900
3021
  }
2901
3022
  /**
2902
3023
  * Show a tool's arguments as a person reads them, in the approval panel and in the
2903
3024
  * default tool row's disclosure — one formatter for both. Without one, both show the
2904
- * JSON the model sent.
3025
+ * JSON the model sent. The `formatInput` field of {@link registerToolDisplay}, on its
3026
+ * own: same matcher, same entry.
2905
3027
  *
2906
3028
  * @example
2907
3029
  * // Approve "write_file notes.md (2 KB)" rather than a JSON object.
2908
3030
  * aparteGlobalConfig.registerToolInputFormatter('write_file', (input) =>
2909
3031
  * `${String(input['path'] ?? '')} (${Math.ceil(String(input['content'] ?? '').length / 1024)} KB)`);
2910
3032
  */
2911
- registerToolInputFormatter(toolName, formatter) {
2912
- this._toolInputFormatters.set(toolName, formatter);
3033
+ registerToolInputFormatter(match, formatter) {
3034
+ this.registerToolDisplay(match, { formatInput: formatter });
2913
3035
  }
2914
- /** Unregister a tool's input formatter. */
2915
- unregisterToolInputFormatter(toolName) {
2916
- this._toolInputFormatters.delete(toolName);
3036
+ /** Drop the input formatter registered under this matcher; the rest of its display stays. */
3037
+ unregisterToolInputFormatter(match) {
3038
+ this._dropToolDisplayField(match, "formatInput");
2917
3039
  }
2918
- /** Get the input formatter for a tool name. Returns undefined if none registered. */
3040
+ /** The input formatter that applies to a tool named `toolName`, or undefined. */
2919
3041
  getToolInputFormatter(toolName) {
2920
- return this._toolInputFormatters.get(toolName);
3042
+ return this.getToolDisplay(toolName)?.formatInput;
2921
3043
  }
2922
3044
  /**
2923
3045
  * Teach the stream parser a tagged block: `<tag attr="…">…</tag>` in the model's
@@ -3128,8 +3250,7 @@ class AparteConfig {
3128
3250
  this._fetchedModels.clear();
3129
3251
  this._tools.clear();
3130
3252
  this._toolRenderers.clear();
3131
- this._toolSummaries.clear();
3132
- this._toolInputFormatters.clear();
3253
+ this._toolDisplays.clear();
3133
3254
  this._streamBlocks.clear();
3134
3255
  this._modelConfig = {};
3135
3256
  this._requireModelSelection = false;
@@ -3262,9 +3383,34 @@ const textRenderer = {
3262
3383
  },
3263
3384
  getStyles: () => ``
3264
3385
  };
3386
+ function formatDuration(ms, tag) {
3387
+ const total = Math.max(1, Math.round(ms / 1e3));
3388
+ try {
3389
+ const unit = (n, u) => new Intl.NumberFormat(tag, { style: "unit", unit: u, unitDisplay: "narrow" }).format(n);
3390
+ if (total < 60) return unit(total, "second");
3391
+ const minutes = Math.floor(total / 60);
3392
+ const seconds = total % 60;
3393
+ if (!seconds) return unit(minutes, "minute");
3394
+ return new Intl.ListFormat(tag, { style: "narrow", type: "unit" }).format([unit(minutes, "minute"), unit(seconds, "second")]);
3395
+ } catch {
3396
+ return `${total}s`;
3397
+ }
3398
+ }
3265
3399
  const THINKING_ANCHOR_SLACK = 24;
3266
3400
  function labelOf(segment) {
3267
- return segment.label && segment.label !== "Thinking" ? segment.label : contextConfig().t("thinking");
3401
+ if (segment.label && segment.label !== "Thinking") return segment.label;
3402
+ const cfg = contextConfig();
3403
+ if (!isSegmentSettled(segment)) return cfg.t("thinking");
3404
+ const show = segment.showDuration ?? cfg.getSegmentDefaults("thinking")?.["showDuration"] === true;
3405
+ const ms = show ? segmentDuration(segment) : void 0;
3406
+ if (ms === void 0) return cfg.t("thought");
3407
+ return cfg.t("thoughtFor").replace("{duration}", () => formatDuration(ms, cfg.getLocale().tag || void 0));
3408
+ }
3409
+ function setLabel(el2, segment) {
3410
+ const text = labelOf(segment);
3411
+ const label = el2.querySelector(".aparte-thinking-label");
3412
+ if (label && label.textContent !== text) label.textContent = text;
3413
+ el2.querySelector(".aparte-thinking-content")?.setAttribute("aria-label", text);
3268
3414
  }
3269
3415
  const thinkingRenderer = {
3270
3416
  type: "thinking",
@@ -3301,13 +3447,9 @@ const thinkingRenderer = {
3301
3447
  * The default label is the only config-derived text here — and `segment.label`
3302
3448
  * still wins, exactly as in `render`, because that string is the app's.
3303
3449
  */
3304
- relabel: (el2, segment) => {
3305
- const text = labelOf(segment);
3306
- const label = el2.querySelector(".aparte-thinking-label");
3307
- if (label) label.textContent = text;
3308
- el2.querySelector(".aparte-thinking-content")?.setAttribute("aria-label", text);
3309
- },
3450
+ relabel: (el2, segment) => setLabel(el2, segment),
3310
3451
  update: (el2, segment) => {
3452
+ setLabel(el2, segment);
3311
3453
  const contentEl = el2.querySelector(".aparte-thinking-content");
3312
3454
  if (!contentEl) return;
3313
3455
  const wasAtBottom = contentEl.scrollHeight - contentEl.scrollTop - contentEl.clientHeight <= THINKING_ANCHOR_SLACK;
@@ -3352,6 +3494,35 @@ function copyText(text) {
3352
3494
  else reject(new Error("[aparte] Clipboard write failed"));
3353
3495
  });
3354
3496
  }
3497
+ const CONFIRM_MS = 1500;
3498
+ function nameButton(button, label) {
3499
+ button.setAttribute("aria-label", label);
3500
+ button.setAttribute("title", label);
3501
+ }
3502
+ function copyButtonMarkup(className) {
3503
+ const label = escapeAttr(contextConfig().t("copy"));
3504
+ const icon = contextConfig().getIcon("copy");
3505
+ return `<button type="button" class="aparte-btn aparte-btn--icon aparte-btn--sm ${escapeAttr(className)}" data-action="copy" aria-label="${label}" title="${label}">${icon}</button>`;
3506
+ }
3507
+ function wireCopyButton(button, read) {
3508
+ button.addEventListener("click", () => {
3509
+ void copyText(read()).catch(() => {
3510
+ });
3511
+ button.innerHTML = contextConfig(button).getIcon("check");
3512
+ nameButton(button, contextConfig(button).t("copied"));
3513
+ button.dataset["copied"] = "1";
3514
+ setTimeout(() => {
3515
+ button.innerHTML = contextConfig(button).getIcon("copy");
3516
+ nameButton(button, contextConfig(button).t("copy"));
3517
+ delete button.dataset["copied"];
3518
+ }, CONFIRM_MS);
3519
+ });
3520
+ }
3521
+ function relabelCopyButton(button) {
3522
+ if (button.dataset["copied"]) return;
3523
+ nameButton(button, contextConfig().t("copy"));
3524
+ button.innerHTML = contextConfig().getIcon("copy");
3525
+ }
3355
3526
  const HIGHLIGHT_DEBOUNCE_MS = 400;
3356
3527
  const MAX_THROTTLE_ENTRIES = 256;
3357
3528
  const _lastHighlightAt = /* @__PURE__ */ new Map();
@@ -3399,32 +3570,27 @@ function streamHighlight(element, paneSelector, content, lang, segId) {
3399
3570
  }).catch(() => {
3400
3571
  });
3401
3572
  }
3402
- function nameButton(button, label) {
3403
- button.setAttribute("aria-label", label);
3404
- button.setAttribute("title", label);
3405
- }
3406
3573
  const codeRenderer = {
3407
3574
  type: "code",
3408
- render: (segment) => `
3575
+ render: (segment) => {
3576
+ const copy = copyButtonMarkup("aparte-code-copy");
3577
+ return `
3409
3578
  <div class="aparte-segment aparte-segment-code" data-segment-id="${escapeHtml(segment.id)}">
3410
3579
  <div class="aparte-code-header">
3411
3580
  ${segment.filename ? `<span class="aparte-code-filename">${escapeHtml(segment.filename)}</span>` : `<span class="aparte-code-header-filler"></span>`}
3412
3581
  <span class="aparte-code-language">${escapeHtml(segment.language || "")}</span>
3413
- <button type="button" class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-code-copy" data-action="copy" aria-label="${escapeAttr(contextConfig().t("copy"))}" title="${escapeAttr(contextConfig().t("copy"))}">
3414
- ${contextConfig().getIcon("copy")}
3415
- </button>
3582
+ ${copy}
3416
3583
  </div>
3417
3584
  <div class="aparte-code-content-wrapper">
3418
3585
  <pre><code class="language-${escapeHtml(segment.language || "text")}">${escapeHtml(segment.content)}</code></pre>
3419
3586
  </div>
3420
3587
  </div>
3421
- `,
3588
+ `;
3589
+ },
3422
3590
  /** The copy button's icon and tooltip — nothing else here comes from config. */
3423
3591
  relabel: (element) => {
3424
3592
  const copyBtn = element.querySelector(".aparte-code-copy");
3425
- if (!copyBtn || copyBtn.dataset.copied) return;
3426
- nameButton(copyBtn, contextConfig().t("copy"));
3427
- copyBtn.innerHTML = contextConfig().getIcon("copy");
3593
+ if (copyBtn) relabelCopyButton(copyBtn);
3428
3594
  },
3429
3595
  setup: (element, segment) => {
3430
3596
  const wrapper = element.querySelector(".aparte-code-content-wrapper");
@@ -3435,21 +3601,7 @@ const codeRenderer = {
3435
3601
  });
3436
3602
  }
3437
3603
  const copyBtn = element.querySelector(".aparte-code-copy");
3438
- if (copyBtn) {
3439
- copyBtn.addEventListener("click", () => {
3440
- const source = element.querySelector(".aparte-code-content-wrapper")?.textContent ?? "";
3441
- void copyText(source).catch(() => {
3442
- });
3443
- copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
3444
- nameButton(copyBtn, contextConfig(copyBtn).t("copied"));
3445
- copyBtn.dataset.copied = "1";
3446
- setTimeout(() => {
3447
- copyBtn.innerHTML = contextConfig(copyBtn).getIcon("copy");
3448
- nameButton(copyBtn, contextConfig(copyBtn).t("copy"));
3449
- delete copyBtn.dataset.copied;
3450
- }, 1500);
3451
- });
3452
- }
3604
+ if (copyBtn) wireCopyButton(copyBtn, () => element.querySelector(".aparte-code-content-wrapper")?.textContent ?? "");
3453
3605
  },
3454
3606
  update: (element, segment) => {
3455
3607
  if (segment.isStreaming) {
@@ -3471,7 +3623,11 @@ const errorRenderer = {
3471
3623
  render: (segment) => {
3472
3624
  const custom = contextConfig().getErrorRenderer?.();
3473
3625
  if (custom) {
3474
- const out = custom({ message: segment.content, details: segment.details });
3626
+ const out = custom({
3627
+ message: segment.content,
3628
+ ...segment.details !== void 0 ? { details: segment.details } : {},
3629
+ ...segment.code !== void 0 ? { code: segment.code } : {}
3630
+ });
3475
3631
  if (out instanceof HTMLElement) {
3476
3632
  out.setAttribute("data-segment-id", segment.id);
3477
3633
  return out;
@@ -3479,7 +3635,7 @@ const errorRenderer = {
3479
3635
  return out;
3480
3636
  }
3481
3637
  return `
3482
- <div class="aparte-segment aparte-alert aparte-alert--danger aparte-segment-error" data-segment-id="${escapeHtml(segment.id)}">
3638
+ <div class="aparte-segment aparte-alert aparte-alert--danger aparte-segment-error" data-segment-id="${escapeHtml(segment.id)}"${segment.code ? ` data-error-code="${escapeAttr(segment.code)}"` : ""}>
3483
3639
  <span class="aparte-alert__icon">${contextConfig().getIcon("error") || "⚠"}</span>
3484
3640
  <div class="aparte-alert__body">
3485
3641
  <div class="aparte-alert__title">${escapeHtml(contextConfig().t("error"))}</div>
@@ -3518,22 +3674,84 @@ function describeToolInput(input, format) {
3518
3674
  return "";
3519
3675
  }
3520
3676
  }
3677
+ function toolNameOf(segment, config) {
3678
+ const identifier = segment.toolCall?.name ?? "tool";
3679
+ const label = config.getToolDisplay(identifier)?.label;
3680
+ if (label !== void 0) {
3681
+ try {
3682
+ const text = typeof label === "function" ? label(segment) : label;
3683
+ if (typeof text === "string" && text.trim()) return { text: text.trim(), isLabel: true };
3684
+ } catch {
3685
+ }
3686
+ }
3687
+ return { text: identifier, isLabel: false };
3688
+ }
3689
+ function toolTargetOf(segment, config) {
3690
+ const summary = config.getToolSummary(segment.toolCall?.name ?? "");
3691
+ if (!summary) return "";
3692
+ try {
3693
+ const text = summary(segment);
3694
+ return typeof text === "string" ? text.trim() : "";
3695
+ } catch {
3696
+ return "";
3697
+ }
3698
+ }
3521
3699
  function describeInput(segment) {
3522
3700
  return describeToolInput(segment.toolCall?.input, contextConfig().getToolInputFormatter(segment.toolCall?.name ?? ""));
3523
3701
  }
3524
3702
  function stateBadge(segment) {
3525
3703
  const cfg = contextConfig();
3526
3704
  const status = segment.status ?? "pending";
3527
- const word = (key) => escapeHtml(cfg.t(key));
3528
- if (status === "awaiting-approval") return `${cfg.getIcon("pause")}${word("approvalWaiting")}`;
3529
- if (status === "pending") return word("toolRunning");
3530
- if (status === "resolved") return `${cfg.getIcon("check")}${word("toolCompleted")}`;
3531
- if (status === "rejected") return `${cfg.getIcon("close")}${word("toolRejected")}`;
3532
- if (status === "aborted") return `${cfg.getIcon("stop")}${word("toolStopped")}`;
3533
- if (status === "failed") return `${cfg.getIcon("close")}${word("toolFailed")}`;
3705
+ const word = (text) => escapeHtml(text);
3706
+ if (status === "awaiting-approval") return `${cfg.getIcon("pause")}${word(cfg.t("approvalWaiting"))}`;
3707
+ if (status === "pending") return word(cfg.t("toolRunning"));
3708
+ if (status === "resolved") return `${cfg.getIcon("check")}${word(cfg.t("toolCompleted"))}`;
3709
+ if (status === "rejected") return `${cfg.getIcon("close")}${word(cfg.t("toolRejected"))}`;
3710
+ if (status === "aborted") return `${cfg.getIcon("stop")}${word(cfg.t("toolStopped"))}`;
3711
+ if (status === "failed") return `${cfg.getIcon("close")}${word(cfg.t("toolFailed"))}`;
3712
+ return "";
3713
+ }
3714
+ function noteOf(segment) {
3715
+ const cfg = contextConfig();
3716
+ if (segment.status === "rejected") {
3717
+ const by = segment.refusal?.by;
3718
+ if (by === "policy") return cfg.t("toolRefusedByPolicy");
3719
+ if (by === "earlier-refusal") return cfg.t("toolNotRunAfterRefusal");
3720
+ const said = segment.refusal?.instruction?.trim();
3721
+ return said ? cfg.t("toolRejectedSaid").replace("{instruction}", () => said) : "";
3722
+ }
3723
+ if (segment.status === "aborted") {
3724
+ switch (segment.abortCause) {
3725
+ case "timeout":
3726
+ return cfg.t("toolTimedOut");
3727
+ case "no-handler":
3728
+ return cfg.t("toolUnavailable");
3729
+ case "no-resolver":
3730
+ return cfg.t("toolNoApprover");
3731
+ case "turn-limit":
3732
+ return cfg.t("toolTurnLimit");
3733
+ default:
3734
+ return "";
3735
+ }
3736
+ }
3534
3737
  return "";
3535
3738
  }
3536
- function setPart(element, part, text) {
3739
+ function outcomeOf(segment) {
3740
+ const text = displayText(segment.result ?? "");
3741
+ if (!text) return null;
3742
+ if (segment.status === "rejected" && segment.refusal) return null;
3743
+ return { key: "toolOutput", text };
3744
+ }
3745
+ function setNote(element, segment) {
3746
+ const el2 = element.querySelector(".aparte-tool-note");
3747
+ if (!el2) return;
3748
+ const text = noteOf(segment);
3749
+ if (el2.textContent !== text) el2.textContent = text;
3750
+ el2.toggleAttribute("hidden", !text);
3751
+ if (text) el2.setAttribute("title", text);
3752
+ else el2.removeAttribute("title");
3753
+ }
3754
+ function setPart(element, part, text, labelKey) {
3537
3755
  const existing = element.querySelector(`[data-part="${part}"]`);
3538
3756
  if (!text) {
3539
3757
  existing?.remove();
@@ -3542,6 +3760,10 @@ function setPart(element, part, text) {
3542
3760
  if (existing) {
3543
3761
  const code2 = existing.querySelector("code");
3544
3762
  if (code2 && code2.textContent !== text) code2.textContent = text;
3763
+ const label2 = existing.querySelector(".aparte-tool-part-label");
3764
+ const word = contextConfig().t(labelKey);
3765
+ if (label2 && label2.textContent !== word) label2.textContent = word;
3766
+ existing.querySelector(".aparte-tool-part-body")?.setAttribute("aria-label", word);
3545
3767
  return;
3546
3768
  }
3547
3769
  const detail = element.querySelector(".aparte-tool-detail");
@@ -3550,25 +3772,57 @@ function setPart(element, part, text) {
3550
3772
  const wrap = document.createElement("div");
3551
3773
  wrap.className = "aparte-tool-part";
3552
3774
  wrap.dataset["part"] = part;
3775
+ const head = document.createElement("div");
3776
+ head.className = "aparte-tool-part-head";
3553
3777
  const label = document.createElement("span");
3554
3778
  label.className = "aparte-tool-part-label";
3555
- label.textContent = cfg.t(part === "input" ? "toolInput" : "toolOutput");
3779
+ label.textContent = cfg.t(labelKey);
3780
+ const tpl = document.createElement("template");
3781
+ tpl.innerHTML = copyButtonMarkup("aparte-tool-copy");
3782
+ head.append(label, tpl.content);
3556
3783
  const body = document.createElement("div");
3557
3784
  body.className = "aparte-tool-part-body";
3785
+ body.setAttribute("role", "region");
3786
+ body.tabIndex = 0;
3787
+ body.setAttribute("aria-label", label.textContent);
3558
3788
  const pre = document.createElement("pre");
3559
3789
  const code = document.createElement("code");
3560
3790
  code.textContent = text;
3561
3791
  pre.appendChild(code);
3562
3792
  body.appendChild(pre);
3563
- wrap.append(label, body);
3793
+ wrap.append(head, body);
3564
3794
  if (part === "input") detail.prepend(wrap);
3565
3795
  else detail.appendChild(wrap);
3796
+ wireParts(wrap);
3797
+ }
3798
+ function wireParts(root) {
3799
+ for (const button of root.querySelectorAll(".aparte-tool-copy")) {
3800
+ if (button.dataset["wired"]) continue;
3801
+ button.dataset["wired"] = "1";
3802
+ const part = button.closest(".aparte-tool-part");
3803
+ wireCopyButton(button, () => part?.querySelector(".aparte-tool-part-body")?.textContent ?? "");
3804
+ }
3805
+ }
3806
+ const JSON_PART_LIMIT = 2e5;
3807
+ function jsonStructureOf(text) {
3808
+ const trimmed = text.trim();
3809
+ if (trimmed.length > JSON_PART_LIMIT || !(trimmed.startsWith("{") || trimmed.startsWith("["))) return void 0;
3810
+ try {
3811
+ const value = JSON.parse(trimmed);
3812
+ return value !== null && typeof value === "object" ? value : void 0;
3813
+ } catch {
3814
+ return void 0;
3815
+ }
3816
+ }
3817
+ function displayText(raw) {
3818
+ const value = jsonStructureOf(raw);
3819
+ return value === void 0 ? raw : JSON.stringify(value, null, 2);
3566
3820
  }
3567
3821
  function highlightParts(element) {
3568
3822
  const cfg = contextConfig();
3569
3823
  for (const body of element.querySelectorAll(".aparte-tool-part-body")) {
3570
3824
  const text = body.querySelector("code")?.textContent ?? "";
3571
- if (!text) continue;
3825
+ if (!text || jsonStructureOf(text) === void 0) continue;
3572
3826
  void cfg.highlightCode(text, "json").then((html) => {
3573
3827
  if ((body.querySelector("code")?.textContent ?? "") !== text) return;
3574
3828
  body.innerHTML = html;
@@ -3577,14 +3831,7 @@ function highlightParts(element) {
3577
3831
  }
3578
3832
  }
3579
3833
  function toolTarget(segment) {
3580
- const summary = contextConfig().getToolSummary(segment.toolCall?.name ?? "");
3581
- if (!summary) return "";
3582
- try {
3583
- const text = summary(segment);
3584
- return typeof text === "string" ? text.trim() : "";
3585
- } catch {
3586
- return "";
3587
- }
3834
+ return toolTargetOf(segment, contextConfig());
3588
3835
  }
3589
3836
  function setTarget(element, segment) {
3590
3837
  const el2 = element.querySelector(".aparte-tool-target");
@@ -3593,54 +3840,79 @@ function setTarget(element, segment) {
3593
3840
  if (el2.textContent !== text) el2.textContent = text;
3594
3841
  el2.toggleAttribute("hidden", !text);
3595
3842
  }
3596
- function pillMarkup(segment, name, status) {
3843
+ function nameOf(segment) {
3844
+ return toolNameOf(segment, contextConfig());
3845
+ }
3846
+ function iconOf(segment) {
3847
+ const cfg = contextConfig();
3848
+ const key = cfg.getToolDisplay(segment.toolCall?.name ?? "")?.icon;
3849
+ return (key ? cfg.getIcon(key) : void 0) || cfg.getIcon("tool");
3850
+ }
3851
+ function setName(element, segment) {
3852
+ const el2 = element.querySelector(".aparte-tool-name");
3853
+ if (!el2) return;
3854
+ const { text, isLabel } = nameOf(segment);
3855
+ if (el2.textContent !== text) el2.textContent = text;
3856
+ el2.classList.toggle("aparte-tool-name--label", isLabel);
3857
+ }
3858
+ function pillMarkup(segment, status) {
3597
3859
  const badge = stateBadge(segment);
3860
+ const icon = iconOf(segment);
3598
3861
  const target = toolTarget(segment);
3862
+ const note = noteOf(segment);
3863
+ const name = nameOf(segment);
3599
3864
  return `
3600
3865
  <span class="aparte-tool-label">
3601
- <span class="aparte-tool-icon">${contextConfig().getIcon("tool")}</span>
3602
- <span class="aparte-tool-name">${escapeHtml(name)}</span>
3866
+ <span class="aparte-tool-icon">${icon}</span>
3867
+ <span class="aparte-tool-name${name.isLabel ? " aparte-tool-name--label" : ""}">${escapeHtml(name.text)}</span>
3603
3868
  <span class="aparte-tool-target"${target ? "" : " hidden"}>${escapeHtml(target)}</span>
3604
3869
  </span>
3605
3870
  <span class="aparte-spinner aparte-tool-spinner" aria-hidden="true"${status === "pending" ? "" : " hidden"}></span>
3606
- <span class="aparte-tool-state">${badge}</span>`;
3871
+ <span class="aparte-tool-state">${badge}</span>
3872
+ <span class="aparte-tool-note"${note ? ` title="${escapeAttr(note)}"` : " hidden"}>${escapeHtml(note)}</span>`;
3607
3873
  }
3874
+ function placeholderMarkup(segment) {
3875
+ 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
+ }
3877
+ const isPlaceholder = (element) => element.hidden && element.classList.contains("aparte-segment-tool-call") && element.childElementCount === 0;
3608
3878
  const toolCallRenderer = {
3609
3879
  type: "tool_call",
3610
3880
  render: (segment) => {
3611
3881
  const name = segment.toolCall?.name ?? "tool";
3612
3882
  const status = segment.status ?? "pending";
3613
3883
  const toolCallId = segment.toolCall?.id ?? "";
3614
- const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
3884
+ if (contextConfig().isToolHidden(name)) return placeholderMarkup(segment);
3885
+ const customRenderer = contextConfig().getToolRenderer(name);
3615
3886
  if (customRenderer) {
3616
- injectToolRendererStyles(segment.toolCall?.name ?? "", customRenderer);
3617
- const out = customRenderer.render(segment);
3618
- if (out) return out;
3887
+ injectToolRendererStyles(name, customRenderer);
3888
+ return customRenderer.render(segment) || placeholderMarkup(segment);
3619
3889
  }
3620
- const pill = pillMarkup(segment, name, status);
3890
+ const pill = pillMarkup(segment, status);
3621
3891
  const toggle = contextConfig().getIcon("expand");
3622
3892
  const input = describeInput(segment);
3623
- const result = segment.result;
3624
- if (!input && !result) {
3893
+ const outcome = outcomeOf(segment);
3894
+ if (!input && !outcome) {
3625
3895
  return `
3626
- <div class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}">${pill}
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}
3627
3897
  </div>
3628
3898
  `;
3629
3899
  }
3630
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;
3631
3903
  return `
3632
- <details class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}">
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" : ""}>
3633
3905
  <summary class="aparte-tool-summary"><span class="aparte-tool-toggle">${toggle}</span>${pill}</summary>
3634
3906
  <div class="aparte-tool-detail">
3635
3907
  ${input ? `
3636
3908
  <div class="aparte-tool-part" data-part="input">
3637
- <span class="aparte-tool-part-label">${escapeHtml(cfg.t("toolInput"))}</span>
3638
- <div class="aparte-tool-part-body"><pre><code>${escapeHtml(input)}</code></pre></div>
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>
3639
3911
  </div>` : ""}
3640
- ${result ? `
3912
+ ${outcome ? `
3641
3913
  <div class="aparte-tool-part" data-part="output">
3642
- <span class="aparte-tool-part-label">${escapeHtml(cfg.t("toolOutput"))}</span>
3643
- <div class="aparte-tool-part-body"><pre><code>${escapeHtml(result)}</code></pre></div>
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>
3644
3916
  </div>` : ""}
3645
3917
  </div>
3646
3918
  </details>
@@ -3657,6 +3929,7 @@ const toolCallRenderer = {
3657
3929
  * No child node is added or removed, per the relabel contract.
3658
3930
  */
3659
3931
  relabel: (element, segment) => {
3932
+ if (isPlaceholder(element)) return;
3660
3933
  const cfg = contextConfig();
3661
3934
  const custom = cfg.getToolRenderer(segment.toolCall?.name);
3662
3935
  if (custom) {
@@ -3664,8 +3937,18 @@ const toolCallRenderer = {
3664
3937
  return;
3665
3938
  }
3666
3939
  const icon = element.querySelector(".aparte-tool-icon");
3667
- if (icon) icon.innerHTML = cfg.getIcon("tool");
3940
+ if (icon) icon.innerHTML = iconOf(segment);
3941
+ setName(element, segment);
3668
3942
  setTarget(element, segment);
3943
+ setNote(element, segment);
3944
+ const inputLabel = element.querySelector('[data-part="input"] .aparte-tool-part-label');
3945
+ if (inputLabel) inputLabel.textContent = cfg.t("toolInput");
3946
+ element.querySelector('[data-part="input"] .aparte-tool-part-body')?.setAttribute("aria-label", cfg.t("toolInput"));
3947
+ const outcome = outcomeOf(segment);
3948
+ const outputLabel = element.querySelector('[data-part="output"] .aparte-tool-part-label');
3949
+ if (outputLabel && outcome) outputLabel.textContent = cfg.t(outcome.key);
3950
+ if (outcome) element.querySelector('[data-part="output"] .aparte-tool-part-body')?.setAttribute("aria-label", cfg.t(outcome.key));
3951
+ for (const button of element.querySelectorAll(".aparte-tool-copy")) relabelCopyButton(button);
3669
3952
  const statusEl = element.querySelector(".aparte-tool-state");
3670
3953
  if (statusEl) statusEl.innerHTML = stateBadge(segment);
3671
3954
  },
@@ -3684,14 +3967,20 @@ const toolCallRenderer = {
3684
3967
  */
3685
3968
  update: (element, segment) => {
3686
3969
  const status = segment.status ?? "pending";
3970
+ const hidden = contextConfig().isToolHidden(segment.toolCall?.name ?? "tool");
3971
+ if (hidden && isPlaceholder(element)) {
3972
+ element.setAttribute("data-status", status);
3973
+ return;
3974
+ }
3687
3975
  const input = describeInput(segment);
3688
- const wantsDetail = !!input || !!segment.result;
3976
+ const outcome = outcomeOf(segment);
3977
+ const wantsDetail = !!input || !!outcome;
3689
3978
  const custom = contextConfig().getToolRenderer(segment.toolCall?.name);
3690
- if (custom?.update) {
3979
+ if (custom?.update && !hidden && !isPlaceholder(element)) {
3691
3980
  custom.update(element, segment);
3692
3981
  return;
3693
3982
  }
3694
- if (custom || wantsDetail !== (element.tagName === "DETAILS")) {
3983
+ if (custom || hidden || isPlaceholder(element) || wantsDetail !== (element.tagName === "DETAILS")) {
3695
3984
  const out = toolCallRenderer.render(segment);
3696
3985
  const tpl = document.createElement("template");
3697
3986
  tpl.innerHTML = typeof out === "string" ? out.trim() : "";
@@ -3706,12 +3995,16 @@ const toolCallRenderer = {
3706
3995
  const badge = element.querySelector(".aparte-tool-state");
3707
3996
  if (badge) badge.innerHTML = stateBadge(segment);
3708
3997
  element.querySelector(".aparte-tool-spinner")?.toggleAttribute("hidden", status !== "pending");
3998
+ setName(element, segment);
3709
3999
  setTarget(element, segment);
3710
- setPart(element, "input", input);
3711
- setPart(element, "output", segment.result ?? "");
4000
+ setNote(element, segment);
4001
+ setPart(element, "input", input, "toolInput");
4002
+ setPart(element, "output", outcome?.text ?? "", outcome?.key ?? "toolOutput");
3712
4003
  highlightParts(element);
3713
4004
  },
3714
4005
  setup: (element, segment) => {
4006
+ if (isPlaceholder(element)) return;
4007
+ wireParts(element);
3715
4008
  highlightParts(element);
3716
4009
  const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
3717
4010
  customRenderer?.setup?.(element, segment);
@@ -3784,11 +4077,16 @@ function injectRendererStyles(config = contextConfig()) {
3784
4077
  }
3785
4078
  if (added) styleElement.textContent = Array.from(injectedRendererStyles).join("\n");
3786
4079
  }
4080
+ const toolRendererSheetIds = /* @__PURE__ */ new WeakMap();
3787
4081
  function injectToolRendererStyles(toolName, renderer) {
3788
4082
  if (typeof document === "undefined") return;
3789
4083
  const css = renderer.getStyles?.();
3790
4084
  if (!css) return;
3791
- const id = `aparte-tool-renderer-${toolName}`;
4085
+ let id = toolRendererSheetIds.get(renderer);
4086
+ if (!id) {
4087
+ id = `aparte-tool-renderer-${toolName}`;
4088
+ toolRendererSheetIds.set(renderer, id);
4089
+ }
3792
4090
  if (document.getElementById(id)) return;
3793
4091
  const el2 = document.createElement("style");
3794
4092
  el2.id = id;
@@ -6291,7 +6589,16 @@ class AparteClient {
6291
6589
  */
6292
6590
  message: () => {
6293
6591
  const loc = this._config.getLocale();
6294
- return (loc.approvalAsk ?? "Run {tool}?").replace("{tool}", () => event.name);
6592
+ const chat = resolveConfig(target);
6593
+ const call = {
6594
+ id: `tool-${event.id}`,
6595
+ type: "tool_call",
6596
+ status: "awaiting-approval",
6597
+ toolCall: { id: event.id, name: event.name, input: event.input }
6598
+ };
6599
+ const name = toolNameOf(call, chat).text;
6600
+ const what = toolTargetOf(call, chat);
6601
+ return (loc.approvalAsk ?? "Allow {tool}?").replace("{tool}", () => what ? `${name} ${what}` : name);
6295
6602
  },
6296
6603
  // A plain string, not a function: the arguments are the model's — its JSON, or
6297
6604
  // the tool's own formatter's reading of it — and do not change with the
@@ -6308,11 +6615,11 @@ class AparteClient {
6308
6615
  signal,
6309
6616
  target
6310
6617
  });
6311
- if (answer.action !== "accept") return { approved: false };
6618
+ if (answer.action !== "accept") return { approved: false, by: "person" };
6312
6619
  const content = answer.content;
6313
6620
  const instruction = content?.instruction?.trim();
6314
- if (instruction) return { approved: false, instruction };
6315
- return { approved: content?.option === "allow" };
6621
+ if (instruction) return { approved: false, instruction, by: "person" };
6622
+ return content?.option === "allow" ? { approved: true } : { approved: false, by: "person" };
6316
6623
  }
6317
6624
  /**
6318
6625
  * Resolve the auth for a provider: `options.keyResolver` takes precedence,
@@ -7132,7 +7439,7 @@ ${content}
7132
7439
  const ruling = gatedElsewhere ? this._config.getApprovalPolicy()?.(call, this._config.getTools().find((t) => t.name === call.name)) ?? { verdict: "ask" } : this._config.ruleOnToolCall(call);
7133
7440
  if (ruling.verdict === "allow") return { approved: true };
7134
7441
  if (ruling.verdict === "deny") {
7135
- return { approved: false, reason: ruling.reason?.trim() || "Tool execution was refused by the approval policy." };
7442
+ return { approved: false, reason: ruling.reason?.trim() || "Tool execution was refused by the approval policy.", by: "policy" };
7136
7443
  }
7137
7444
  return this._askForApproval(call, sig, targetElement);
7138
7445
  });
@@ -7150,7 +7457,7 @@ ${content}
7150
7457
  const resolver = this._approvalResolverFor(targetElement, true);
7151
7458
  const refuseTheRest = (after) => {
7152
7459
  for (const rest of pending.slice(after + 1)) {
7153
- outcomes[rest.toolCallId] = { kind: "decision", approved: false, reason: NOT_RUN_AFTER_A_REFUSAL };
7460
+ outcomes[rest.toolCallId] = { kind: "decision", approved: false, reason: NOT_RUN_AFTER_A_REFUSAL, by: "earlier-refusal" };
7154
7461
  }
7155
7462
  };
7156
7463
  for (const [index, p] of pending.entries()) {
@@ -7367,7 +7674,7 @@ ${content}
7367
7674
  id: `error-${uuid()}`,
7368
7675
  type: "error",
7369
7676
  content: error.message,
7370
- details: error.code
7677
+ code: error.code
7371
7678
  };
7372
7679
  if (target.updateMessage) {
7373
7680
  const rendered = target.getMessage?.(messageId)?.segments ?? target.getMessages?.()?.find((m) => m.id === messageId)?.segments ?? [];
@@ -7864,24 +8171,25 @@ export {
7864
8171
  segmentDuration as a4,
7865
8172
  segmentTiming as a5,
7866
8173
  subscribeConfigChange as a6,
7867
- unregisterSegmentRenderer as a7,
7868
- uuid as a8,
7869
- eventOrigin as a9,
7870
- activeElementIn as aa,
7871
- gateGlide as ab,
7872
- renderingVersionOf as ac,
7873
- mergeSegmentUpdate as ad,
7874
- writeStreamedMarkdown as ae,
7875
- stampSegmentActivity as af,
7876
- stampSegmentOnInsert as ag,
7877
- stampSegmentOnUpdate as ah,
7878
- renumberSegments as ai,
7879
- openSegmentIds as aj,
7880
- isTerminalStatus as ak,
7881
- adoptMessageSegments as al,
7882
- APARTE_DERIVED_SEGMENTS as am,
7883
- chatBoundaryOf as an,
7884
- rootOf as ao,
8174
+ toolRefusal as a7,
8175
+ unregisterSegmentRenderer as a8,
8176
+ uuid as a9,
8177
+ eventOrigin as aa,
8178
+ activeElementIn as ab,
8179
+ gateGlide as ac,
8180
+ renderingVersionOf as ad,
8181
+ mergeSegmentUpdate as ae,
8182
+ writeStreamedMarkdown as af,
8183
+ stampSegmentActivity as ag,
8184
+ stampSegmentOnInsert as ah,
8185
+ stampSegmentOnUpdate as ai,
8186
+ renumberSegments as aj,
8187
+ openSegmentIds as ak,
8188
+ isTerminalStatus as al,
8189
+ adoptMessageSegments as am,
8190
+ APARTE_DERIVED_SEGMENTS as an,
8191
+ chatBoundaryOf as ao,
8192
+ rootOf as ap,
7885
8193
  APARTE_DEFAULT_BUBBLE_ACTIONS as b,
7886
8194
  APARTE_DEFAULT_HOST_HANDLERS as c,
7887
8195
  APARTE_DEFAULT_ICON_FALLBACKS as d,
@@ -7908,4 +8216,4 @@ export {
7908
8216
  collectRendererStyles as y,
7909
8217
  contentToText as z
7910
8218
  };
7911
- //# sourceMappingURL=dialog-triggers-3Rk7PuZa.js.map
8219
+ //# sourceMappingURL=dialog-triggers-3G9TqG9g.js.map