@copilotkit/vue 1.59.1 → 1.59.2

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.
@@ -1,6 +1,7 @@
1
1
  import { defineComponent, h, ref } from "vue";
2
2
  import type { WatchSource } from "vue";
3
3
  import type { Component, VNodeChild } from "vue";
4
+ import { ToolCallStatus } from "@copilotkit/core";
4
5
  import { useRenderTool } from "./use-render-tool";
5
6
 
6
7
  type DefaultRenderProps = {
@@ -11,6 +12,98 @@ type DefaultRenderProps = {
11
12
  result: string | undefined;
12
13
  };
13
14
 
15
+ /**
16
+ * Module-level dedup set so an unknown status value only emits a console
17
+ * warning the FIRST time we encounter it. Otherwise a stuck/unmapped status
18
+ * would log on every re-render (potentially many per second).
19
+ */
20
+ const warnedUnknownStatuses = new Set<string>();
21
+
22
+ /**
23
+ * Map a {@link ToolCallStatus} enum value to the documented string-union
24
+ * status the {@link DefaultRenderProps} contract exposes. Unknown / future
25
+ * enum members log a warning (once per distinct value) and fall back to
26
+ * `"inProgress"`.
27
+ */
28
+ function mapToolCallStatus(
29
+ status: ToolCallStatus,
30
+ ): DefaultRenderProps["status"] {
31
+ switch (status) {
32
+ case ToolCallStatus.Complete:
33
+ return "complete";
34
+ case ToolCallStatus.Executing:
35
+ return "executing";
36
+ case ToolCallStatus.InProgress:
37
+ return "inProgress";
38
+ default: {
39
+ const key = String(status);
40
+ if (!warnedUnknownStatuses.has(key)) {
41
+ warnedUnknownStatuses.add(key);
42
+ console.warn(
43
+ `[CopilotKit] Unknown ToolCallStatus "${key}" in default tool-call renderer; falling back to "inProgress".`,
44
+ );
45
+ }
46
+ return "inProgress";
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Convert framework-internal raw renderer props (`args`, enum status) to the
53
+ * documented DefaultRenderProps shape. Idempotent on already-documented input
54
+ * — if the caller passes `parameters` and a string-union `status`, those win.
55
+ */
56
+ type AdaptInput = {
57
+ name?: unknown;
58
+ toolCallId?: unknown;
59
+ args?: unknown;
60
+ parameters?: unknown;
61
+ status?: unknown;
62
+ result?: unknown;
63
+ };
64
+
65
+ function adaptRendererProps(raw: AdaptInput): DefaultRenderProps {
66
+ const parameters = raw.parameters !== undefined ? raw.parameters : raw.args;
67
+ const rawStatus = raw.status;
68
+ const status: DefaultRenderProps["status"] =
69
+ rawStatus === "inProgress" ||
70
+ rawStatus === "executing" ||
71
+ rawStatus === "complete"
72
+ ? rawStatus
73
+ : mapToolCallStatus(rawStatus as ToolCallStatus);
74
+ return {
75
+ name: raw.name as string,
76
+ toolCallId: raw.toolCallId as string,
77
+ parameters,
78
+ status,
79
+ result: raw.result as string | undefined,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Guarded JSON.stringify for the expanded `<pre>` blocks. A circular reference
85
+ * would otherwise crash the Vue render.
86
+ */
87
+ function safeStringifyForPre(value: unknown): string {
88
+ try {
89
+ return JSON.stringify(value, null, 2);
90
+ } catch (err) {
91
+ console.warn(
92
+ "[CopilotKit] Failed to JSON.stringify tool-call payload for default renderer; falling back to String():",
93
+ err,
94
+ );
95
+ try {
96
+ return String(value);
97
+ } catch (innerErr) {
98
+ console.warn(
99
+ "[CopilotKit] safeStringifyForPre: value could not be stringified:",
100
+ innerErr,
101
+ );
102
+ return "[unserializable]";
103
+ }
104
+ }
105
+ }
106
+
14
107
  const DefaultToolCallRenderer = defineComponent({
15
108
  props: {
16
109
  name: {
@@ -31,7 +124,11 @@ const DefaultToolCallRenderer = defineComponent({
31
124
  required: true,
32
125
  },
33
126
  result: {
34
- type: String,
127
+ // Typeless on purpose: the renderer body handles both string results
128
+ // and structured (object) results via `safeStringifyForPre`. Declaring
129
+ // `type: String` would trip Vue's dev-mode prop-type warning on every
130
+ // non-string result and make the defensive branch unreachable.
131
+ type: null,
35
132
  required: false,
36
133
  default: undefined,
37
134
  },
@@ -49,60 +146,114 @@ const DefaultToolCallRenderer = defineComponent({
49
146
  ? "Done"
50
147
  : props.status;
51
148
 
52
- return h("div", { style: { marginTop: "8px", paddingBottom: "8px" } }, [
53
- h(
54
- "div",
55
- {
56
- style: {
57
- borderRadius: "12px",
58
- border: "1px solid #e4e4e7",
59
- backgroundColor: "#fafafa",
60
- padding: "14px 16px",
149
+ return h(
150
+ "div",
151
+ {
152
+ "data-testid": "copilot-tool-render",
153
+ "data-tool-name": props.name,
154
+ "data-tool-call-id": props.toolCallId,
155
+ "data-status": props.status,
156
+ "data-args": safeStringifyForAttr(props.parameters),
157
+ "data-result": safeStringifyForAttr(props.result),
158
+ style: { marginTop: "8px", paddingBottom: "8px" },
159
+ },
160
+ [
161
+ h(
162
+ "div",
163
+ {
164
+ style: {
165
+ borderRadius: "12px",
166
+ border: "1px solid #e4e4e7",
167
+ backgroundColor: "#fafafa",
168
+ padding: "14px 16px",
169
+ },
61
170
  },
62
- },
63
- [
64
- h(
65
- "button",
66
- {
67
- type: "button",
68
- onClick: () => {
69
- isExpanded.value = !isExpanded.value;
70
- },
71
- style: {
72
- width: "100%",
73
- display: "flex",
74
- alignItems: "center",
75
- justifyContent: "space-between",
76
- gap: "10px",
77
- cursor: "pointer",
78
- border: "none",
79
- padding: 0,
80
- margin: 0,
81
- background: "transparent",
82
- textAlign: "left",
171
+ [
172
+ h(
173
+ "button",
174
+ {
175
+ type: "button",
176
+ "aria-expanded": String(isExpanded.value),
177
+ onClick: () => {
178
+ isExpanded.value = !isExpanded.value;
179
+ },
180
+ style: {
181
+ width: "100%",
182
+ display: "flex",
183
+ alignItems: "center",
184
+ justifyContent: "space-between",
185
+ gap: "10px",
186
+ cursor: "pointer",
187
+ border: "none",
188
+ padding: 0,
189
+ margin: 0,
190
+ background: "transparent",
191
+ textAlign: "left",
192
+ },
83
193
  },
84
- },
85
- [
86
- h("span", { style: { fontWeight: "600" } }, props.name),
87
- h("span", statusLabel),
88
- ],
89
- ),
90
- isExpanded.value
91
- ? h("div", { style: { marginTop: "12px" } }, [
92
- h("div", "Arguments"),
93
- h("pre", JSON.stringify(props.parameters ?? {}, null, 2)),
94
- props.result !== undefined
95
- ? h("div", [h("div", "Result"), h("pre", props.result)])
96
- : null,
97
- ])
98
- : null,
99
- ],
100
- ),
101
- ]);
194
+ [
195
+ h(
196
+ "span",
197
+ {
198
+ "data-testid": "copilot-tool-render-name",
199
+ style: { fontWeight: "600" },
200
+ },
201
+ props.name,
202
+ ),
203
+ h(
204
+ "span",
205
+ { "data-testid": "copilot-tool-render-status" },
206
+ statusLabel,
207
+ ),
208
+ ],
209
+ ),
210
+ isExpanded.value
211
+ ? h("div", { style: { marginTop: "12px" } }, [
212
+ h("div", "Arguments"),
213
+ h("pre", safeStringifyForPre(props.parameters ?? {})),
214
+ props.result !== undefined
215
+ ? h("div", [
216
+ h("div", "Result"),
217
+ h(
218
+ "pre",
219
+ typeof props.result === "string"
220
+ ? props.result
221
+ : safeStringifyForPre(props.result),
222
+ ),
223
+ ])
224
+ : null,
225
+ ])
226
+ : null,
227
+ ],
228
+ ),
229
+ ],
230
+ );
102
231
  };
103
232
  },
104
233
  });
105
234
 
235
+ function safeStringifyForAttr(value: unknown): string {
236
+ if (value === undefined || value === null) return "";
237
+ if (typeof value === "string") return value;
238
+ try {
239
+ return JSON.stringify(value);
240
+ } catch (err) {
241
+ console.warn(
242
+ "[CopilotKit] Failed to JSON.stringify tool-call payload for data-* attribute; falling back to String():",
243
+ err,
244
+ );
245
+ try {
246
+ return String(value);
247
+ } catch (innerErr) {
248
+ console.warn(
249
+ "[CopilotKit] safeStringifyForAttr: value could not be stringified:",
250
+ innerErr,
251
+ );
252
+ return "";
253
+ }
254
+ }
255
+ }
256
+
106
257
  export function useDefaultRenderTool(
107
258
  config?: {
108
259
  render?:
@@ -111,19 +262,56 @@ export function useDefaultRenderTool(
111
262
  },
112
263
  deps?: WatchSource<unknown>[],
113
264
  ): void {
265
+ const userRender = config?.render;
266
+
267
+ // When the user supplies a function render, wrap it so they receive the
268
+ // documented {@link DefaultRenderProps} shape regardless of whether the
269
+ // call site passes `args + enum status` (CopilotChatToolCallsView's core
270
+ // path) or `parameters + string status` (an already-adapted call site).
271
+ // Component-typed renders are also wrapped — Vue would bind whatever attrs
272
+ // the call site passes, which means a component-typed render would receive
273
+ // the raw `{ args, status: <enum> }` shape instead of the documented
274
+ // `{ parameters, status: <string-union> }` shape. Wrap so the user
275
+ // component sees `DefaultRenderProps`.
276
+ let registeredRender:
277
+ | ((props: DefaultRenderProps) => VNodeChild)
278
+ | Component<DefaultRenderProps>;
279
+
280
+ if (typeof userRender === "function") {
281
+ const fn = userRender as (props: DefaultRenderProps) => VNodeChild;
282
+ registeredRender = ((rawProps: AdaptInput) => {
283
+ const adapted = adaptRendererProps(rawProps);
284
+ return fn(adapted);
285
+ }) as (props: DefaultRenderProps) => VNodeChild;
286
+ } else if (userRender) {
287
+ const userComponent = userRender;
288
+ registeredRender = ((rawProps: AdaptInput) => {
289
+ const adapted = adaptRendererProps(rawProps);
290
+ return h(userComponent as Component, {
291
+ name: adapted.name,
292
+ toolCallId: adapted.toolCallId,
293
+ parameters: adapted.parameters,
294
+ status: adapted.status,
295
+ result: adapted.result,
296
+ });
297
+ }) as (props: DefaultRenderProps) => VNodeChild;
298
+ } else {
299
+ registeredRender = ((rawProps: AdaptInput) => {
300
+ const adapted = adaptRendererProps(rawProps);
301
+ return h(DefaultToolCallRenderer, {
302
+ name: adapted.name,
303
+ toolCallId: adapted.toolCallId,
304
+ parameters: adapted.parameters,
305
+ status: adapted.status,
306
+ result: adapted.result,
307
+ });
308
+ }) as (props: DefaultRenderProps) => VNodeChild;
309
+ }
310
+
114
311
  useRenderTool(
115
312
  {
116
313
  name: "*",
117
- render:
118
- config?.render ??
119
- ((props: DefaultRenderProps) =>
120
- h(DefaultToolCallRenderer, {
121
- name: props.name,
122
- toolCallId: props.toolCallId,
123
- parameters: props.parameters,
124
- status: props.status,
125
- result: props.result,
126
- })),
314
+ render: registeredRender,
127
315
  },
128
316
  deps,
129
317
  );