@agent-native/toolkit 0.213.1 → 0.214.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 (198) hide show
  1. package/agent-native.eject.json +9 -0
  2. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/components.js +10 -4
  4. package/dist/app/agentkit/react/components.js.map +1 -1
  5. package/dist/app/agentkit/react/context.d.ts +13 -1
  6. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  7. package/dist/app/agentkit/react/context.js +3 -1
  8. package/dist/app/agentkit/react/context.js.map +1 -1
  9. package/dist/app/agentkit/react/index.d.ts +1 -0
  10. package/dist/app/agentkit/react/index.d.ts.map +1 -1
  11. package/dist/app/agentkit/react/index.js +1 -0
  12. package/dist/app/agentkit/react/index.js.map +1 -1
  13. package/dist/app/agentkit/react/message-content-order.d.ts +21 -0
  14. package/dist/app/agentkit/react/message-content-order.d.ts.map +1 -0
  15. package/dist/app/agentkit/react/message-content-order.js +173 -0
  16. package/dist/app/agentkit/react/message-content-order.js.map +1 -0
  17. package/dist/app/agentkit/react/root.d.ts +3 -1
  18. package/dist/app/agentkit/react/root.d.ts.map +1 -1
  19. package/dist/app/agentkit/react/root.js +2 -2
  20. package/dist/app/agentkit/react/root.js.map +1 -1
  21. package/dist/app/agentkit/react/styles.css +2 -2
  22. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  23. package/dist/app/chat/AgentKitAssistantChat.js +7 -2
  24. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  25. package/dist/app/chat/AgentPanel.d.ts +14 -2
  26. package/dist/app/chat/AgentPanel.d.ts.map +1 -1
  27. package/dist/app/chat/AgentPanel.js +22 -5
  28. package/dist/app/chat/AgentPanel.js.map +1 -1
  29. package/dist/app/chat/chat/surface-types.d.ts +8 -0
  30. package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
  31. package/dist/app/chat/chat/surface-types.js.map +1 -1
  32. package/dist/app/chat/chat/widgets/DataChartRenderer.d.ts.map +1 -1
  33. package/dist/app/chat/chat/widgets/DataChartRenderer.js +127 -30
  34. package/dist/app/chat/chat/widgets/DataChartRenderer.js.map +1 -1
  35. package/dist/app/chat/chat/widgets/DataChartWidget.d.ts +3 -2
  36. package/dist/app/chat/chat/widgets/DataChartWidget.d.ts.map +1 -1
  37. package/dist/app/chat/chat/widgets/DataChartWidget.js +14 -9
  38. package/dist/app/chat/chat/widgets/DataChartWidget.js.map +1 -1
  39. package/dist/app/chat/chat/widgets/DataInsightsWidget.js +1 -1
  40. package/dist/app/chat/chat/widgets/DataInsightsWidget.js.map +1 -1
  41. package/dist/app/chat/chat/widgets/DataTableWidget.d.ts +3 -2
  42. package/dist/app/chat/chat/widgets/DataTableWidget.d.ts.map +1 -1
  43. package/dist/app/chat/chat/widgets/DataTableWidget.js +10 -66
  44. package/dist/app/chat/chat/widgets/DataTableWidget.js.map +1 -1
  45. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts +11 -0
  46. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts.map +1 -0
  47. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js +131 -0
  48. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js.map +1 -0
  49. package/dist/app/chat/chat/widgets/builtin-tool-renderers.js +2 -2
  50. package/dist/app/chat/chat/widgets/builtin-tool-renderers.js.map +1 -1
  51. package/dist/app/chat/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  52. package/dist/app/chat/mcp-apps/McpAppRenderer.js +6 -5
  53. package/dist/app/chat/mcp-apps/McpAppRenderer.js.map +1 -1
  54. package/dist/app/chat/mcp-apps/mcp-app-theme.d.ts +2 -0
  55. package/dist/app/chat/mcp-apps/mcp-app-theme.d.ts.map +1 -0
  56. package/dist/app/chat/mcp-apps/mcp-app-theme.js +11 -0
  57. package/dist/app/chat/mcp-apps/mcp-app-theme.js.map +1 -0
  58. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  59. package/dist/app/i18n/catalogs/ar-SA.js +8 -0
  60. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  61. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  62. package/dist/app/i18n/catalogs/de-DE.js +8 -0
  63. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  64. package/dist/app/i18n/catalogs/en-US.d.ts +8 -0
  65. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  66. package/dist/app/i18n/catalogs/en-US.js +8 -0
  67. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  68. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  69. package/dist/app/i18n/catalogs/es-ES.js +8 -0
  70. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  71. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  72. package/dist/app/i18n/catalogs/fr-FR.js +8 -0
  73. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  74. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  75. package/dist/app/i18n/catalogs/hi-IN.js +8 -0
  76. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  77. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  78. package/dist/app/i18n/catalogs/ja-JP.js +8 -0
  79. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  80. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  81. package/dist/app/i18n/catalogs/ko-KR.js +8 -0
  82. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  83. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  84. package/dist/app/i18n/catalogs/pt-BR.js +8 -0
  85. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  86. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  87. package/dist/app/i18n/catalogs/zh-CN.js +8 -0
  88. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  89. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  90. package/dist/app/i18n/catalogs/zh-TW.js +8 -0
  91. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  92. package/dist/app/providers/AppProviders.d.ts +2 -1
  93. package/dist/app/providers/AppProviders.d.ts.map +1 -1
  94. package/dist/app/providers/AppProviders.js +4 -3
  95. package/dist/app/providers/AppProviders.js.map +1 -1
  96. package/dist/app/providers/ThemeFamilySync.d.ts +2 -0
  97. package/dist/app/providers/ThemeFamilySync.d.ts.map +1 -0
  98. package/dist/app/providers/ThemeFamilySync.js +6 -0
  99. package/dist/app/providers/ThemeFamilySync.js.map +1 -0
  100. package/dist/app/settings/account/PreferencesSettings.d.ts.map +1 -1
  101. package/dist/app/settings/account/PreferencesSettings.js +2 -1
  102. package/dist/app/settings/account/PreferencesSettings.js.map +1 -1
  103. package/dist/app/settings/account/ThemeFamilySettings.d.ts +2 -0
  104. package/dist/app/settings/account/ThemeFamilySettings.d.ts.map +1 -0
  105. package/dist/app/settings/account/ThemeFamilySettings.js +31 -0
  106. package/dist/app/settings/account/ThemeFamilySettings.js.map +1 -0
  107. package/dist/app/settings/account/search-entries.d.ts.map +1 -1
  108. package/dist/app/settings/account/search-entries.js +6 -0
  109. package/dist/app/settings/account/search-entries.js.map +1 -1
  110. package/dist/app/shared/DefaultSpinner.d.ts.map +1 -1
  111. package/dist/app/shared/DefaultSpinner.js +4 -5
  112. package/dist/app/shared/DefaultSpinner.js.map +1 -1
  113. package/dist/app/shared/ErrorBoundary.d.ts +1 -0
  114. package/dist/app/shared/ErrorBoundary.d.ts.map +1 -1
  115. package/dist/app/shared/ErrorBoundary.js +27 -12
  116. package/dist/app/shared/ErrorBoundary.js.map +1 -1
  117. package/dist/app/shared/PoweredByBadge.d.ts.map +1 -1
  118. package/dist/app/shared/PoweredByBadge.js +8 -49
  119. package/dist/app/shared/PoweredByBadge.js.map +1 -1
  120. package/dist/app/styles/agent-conversation.css +0 -23
  121. package/dist/app/styles/agent-native.css +1 -1
  122. package/dist/app/styles/theme-families.css +66 -0
  123. package/dist/composer/ComposerContextBanner.d.ts +22 -0
  124. package/dist/composer/ComposerContextBanner.d.ts.map +1 -0
  125. package/dist/composer/ComposerContextBanner.js +9 -0
  126. package/dist/composer/ComposerContextBanner.js.map +1 -0
  127. package/dist/composer/ComposerModePicker.d.ts +26 -0
  128. package/dist/composer/ComposerModePicker.d.ts.map +1 -0
  129. package/dist/composer/ComposerModePicker.js +14 -0
  130. package/dist/composer/ComposerModePicker.js.map +1 -0
  131. package/dist/composer/index.d.ts +2 -0
  132. package/dist/composer/index.d.ts.map +1 -1
  133. package/dist/composer/index.js +2 -0
  134. package/dist/composer/index.js.map +1 -1
  135. package/dist/styles.css +1 -0
  136. package/package.json +3 -3
  137. package/src/app/agentkit/react/components.spec.tsx +12 -0
  138. package/src/app/agentkit/react/components.tsx +41 -22
  139. package/src/app/agentkit/react/context.tsx +17 -0
  140. package/src/app/agentkit/react/index.ts +1 -0
  141. package/src/app/agentkit/react/lifecycle.spec.tsx +116 -1
  142. package/src/app/agentkit/react/message-content-order.spec.ts +190 -0
  143. package/src/app/agentkit/react/message-content-order.ts +246 -0
  144. package/src/app/agentkit/react/root.tsx +4 -0
  145. package/src/app/agentkit/react/styles.css +2 -2
  146. package/src/app/chat/AgentKitAssistantChat.spec.tsx +19 -0
  147. package/src/app/chat/AgentKitAssistantChat.tsx +10 -1
  148. package/src/app/chat/AgentPanel.header.spec.ts +72 -14
  149. package/src/app/chat/AgentPanel.tsx +97 -53
  150. package/src/app/chat/chat/surface-types.ts +8 -0
  151. package/src/app/chat/chat/widgets/DataChartRenderer.spec.tsx +217 -0
  152. package/src/app/chat/chat/widgets/DataChartRenderer.tsx +306 -68
  153. package/src/app/chat/chat/widgets/DataChartWidget.spec.tsx +329 -0
  154. package/src/app/chat/chat/widgets/DataChartWidget.ssr.spec.tsx +25 -0
  155. package/src/app/chat/chat/widgets/DataChartWidget.tsx +48 -33
  156. package/src/app/chat/chat/widgets/DataInsightsWidget.tsx +2 -2
  157. package/src/app/chat/chat/widgets/DataTableWidget.spec.tsx +37 -3
  158. package/src/app/chat/chat/widgets/DataTableWidget.tsx +22 -86
  159. package/src/app/chat/chat/widgets/DataWidgetDisplayControls.tsx +301 -0
  160. package/src/app/chat/chat/widgets/builtin-tool-renderers.tsx +5 -5
  161. package/src/app/chat/mcp-apps/McpAppRenderer.tsx +10 -5
  162. package/src/app/chat/mcp-apps/mcp-app-theme.spec.ts +30 -0
  163. package/src/app/chat/mcp-apps/mcp-app-theme.ts +11 -0
  164. package/src/app/i18n/catalogs/ar-SA.ts +9 -0
  165. package/src/app/i18n/catalogs/de-DE.ts +9 -0
  166. package/src/app/i18n/catalogs/en-US.ts +9 -0
  167. package/src/app/i18n/catalogs/es-ES.ts +10 -0
  168. package/src/app/i18n/catalogs/fr-FR.ts +10 -0
  169. package/src/app/i18n/catalogs/hi-IN.ts +9 -0
  170. package/src/app/i18n/catalogs/ja-JP.ts +9 -0
  171. package/src/app/i18n/catalogs/ko-KR.ts +9 -0
  172. package/src/app/i18n/catalogs/pt-BR.ts +9 -0
  173. package/src/app/i18n/catalogs/zh-CN.ts +8 -0
  174. package/src/app/i18n/catalogs/zh-TW.ts +9 -0
  175. package/src/app/providers/AppProviders.spec.tsx +71 -0
  176. package/src/app/providers/AppProviders.tsx +10 -0
  177. package/src/app/providers/ThemeFamilySync.tsx +6 -0
  178. package/src/app/settings/account/PreferencesSettings.spec.tsx +13 -2
  179. package/src/app/settings/account/PreferencesSettings.tsx +8 -0
  180. package/src/app/settings/account/ThemeFamilySettings.tsx +61 -0
  181. package/src/app/settings/account/search-entries.ts +6 -0
  182. package/src/app/shared/DefaultSpinner.spec.tsx +14 -0
  183. package/src/app/shared/DefaultSpinner.tsx +4 -5
  184. package/src/app/shared/ErrorBoundary.spec.tsx +45 -2
  185. package/src/app/shared/ErrorBoundary.tsx +37 -12
  186. package/src/app/shared/PoweredByBadge.spec.tsx +16 -0
  187. package/src/app/shared/PoweredByBadge.tsx +8 -50
  188. package/src/app/styles/agent-conversation.css +0 -23
  189. package/src/app/styles/agent-native.css +1 -1
  190. package/src/app/styles/agent-native.spec.ts +18 -0
  191. package/src/app/styles/theme-families.css +66 -0
  192. package/src/app/styles/theme-families.spec.ts +29 -0
  193. package/src/composer/ComposerContextBanner.spec.tsx +74 -0
  194. package/src/composer/ComposerContextBanner.tsx +72 -0
  195. package/src/composer/ComposerModePicker.spec.tsx +101 -0
  196. package/src/composer/ComposerModePicker.tsx +98 -0
  197. package/src/composer/index.ts +9 -0
  198. package/src/styles.css +1 -0
@@ -0,0 +1,329 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { AgentNativeI18nProvider } from "@agent-native/core/client/i18n";
4
+ import React, { act } from "react";
5
+ import { createRoot, type Root } from "react-dom/client";
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
+
8
+ import { createToolkitI18nCatalog } from "../../../i18n.js";
9
+ import type { DataWidgetAction } from "./data-widget-types.js";
10
+ import { DataChartWidget } from "./DataChartWidget.js";
11
+
12
+ const mocks = vi.hoisted(() => ({
13
+ writeClipboardText: vi.fn(),
14
+ toast: vi.fn(),
15
+ }));
16
+
17
+ vi.mock("@agent-native/toolkit/clipboard", () => ({
18
+ writeClipboardText: mocks.writeClipboardText,
19
+ }));
20
+
21
+ vi.mock("../../../../hooks/use-toast.js", () => ({ toast: mocks.toast }));
22
+
23
+ vi.mock("./DataChartRenderer.js", () => ({
24
+ DataChartRenderer: ({
25
+ chart,
26
+ }: {
27
+ chart: {
28
+ contentHeight?: number;
29
+ contentPadding?: number;
30
+ labelFontFamily?: string;
31
+ labelFontSize?: number;
32
+ };
33
+ }) => (
34
+ <div
35
+ data-testid="chart-surface"
36
+ data-content-height={chart.contentHeight}
37
+ data-content-padding={chart.contentPadding}
38
+ data-label-font-family={chart.labelFontFamily}
39
+ data-label-font-size={chart.labelFontSize}
40
+ />
41
+ ),
42
+ }));
43
+
44
+ const toolkitI18nCatalog = createToolkitI18nCatalog({ messages: {} });
45
+ const initialBasePath = process.env.VITE_APP_BASE_PATH;
46
+ const roots: Root[] = [];
47
+
48
+ async function renderWidget(chartActions?: DataWidgetAction[]) {
49
+ const container = document.createElement("div");
50
+ document.body.appendChild(container);
51
+ const root = createRoot(container);
52
+ roots.push(root);
53
+
54
+ await act(async () => {
55
+ root.render(
56
+ <AgentNativeI18nProvider
57
+ catalog={toolkitI18nCatalog}
58
+ persistPreference={false}
59
+ >
60
+ <DataChartWidget
61
+ chart={{
62
+ type: "bar",
63
+ title: "Responses by day",
64
+ xKey: "day",
65
+ series: [{ key: "count", label: "Responses" }],
66
+ data: [{ day: "Mon", count: 2 }],
67
+ actions: chartActions,
68
+ }}
69
+ display={{
70
+ primaryAction: {
71
+ label: "Open report",
72
+ href: "/reports/1?tab=chart",
73
+ },
74
+ sources: [
75
+ {
76
+ id: "warehouse",
77
+ label: "Warehouse",
78
+ href: "/sources/warehouse",
79
+ },
80
+ { id: "local", label: "Local data" },
81
+ ],
82
+ actions: [
83
+ {
84
+ id: "details",
85
+ type: "link",
86
+ label: "Open details",
87
+ href: "/reports/1/details",
88
+ },
89
+ {
90
+ id: "query",
91
+ type: "copy",
92
+ label: "Copy query",
93
+ text: "select count(*)",
94
+ },
95
+ {
96
+ id: "unsafe",
97
+ type: "link",
98
+ label: "Unsafe",
99
+ href: "javascript:alert(1)",
100
+ },
101
+ ],
102
+ }}
103
+ />
104
+ </AgentNativeI18nProvider>,
105
+ );
106
+ });
107
+
108
+ return container;
109
+ }
110
+
111
+ async function openActionsMenu(container: HTMLElement) {
112
+ const trigger = container.querySelector<HTMLButtonElement>(
113
+ 'button[aria-label="Chart actions"]',
114
+ );
115
+ expect(trigger).not.toBeNull();
116
+
117
+ await act(async () => {
118
+ trigger?.dispatchEvent(
119
+ new PointerEvent("pointerdown", {
120
+ bubbles: true,
121
+ button: 0,
122
+ pointerType: "mouse",
123
+ }),
124
+ );
125
+ await Promise.resolve();
126
+ });
127
+
128
+ return document.querySelector<HTMLElement>("[role=menu]");
129
+ }
130
+
131
+ describe("DataChartWidget display contract", () => {
132
+ beforeEach(() => {
133
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
134
+ process.env.VITE_APP_BASE_PATH = "/mounted";
135
+ window.history.replaceState({}, "", "/chat");
136
+ mocks.writeClipboardText.mockResolvedValue(true);
137
+ });
138
+
139
+ afterEach(() => {
140
+ for (const root of roots.splice(0)) {
141
+ act(() => root.unmount());
142
+ }
143
+ document.body.innerHTML = "";
144
+ window.history.replaceState({}, "", "/");
145
+ process.env.VITE_APP_BASE_PATH = initialBasePath;
146
+ mocks.writeClipboardText.mockReset();
147
+ mocks.toast.mockReset();
148
+ vi.unstubAllGlobals();
149
+ });
150
+
151
+ it("renders source chips and navigates safe chart links", async () => {
152
+ const container = await renderWidget();
153
+ const primary = Array.from(container.querySelectorAll("a")).find((link) =>
154
+ link.textContent?.includes("Open report"),
155
+ );
156
+ const source = Array.from(container.querySelectorAll("a")).find((link) =>
157
+ link.textContent?.includes("Warehouse"),
158
+ );
159
+
160
+ expect(primary?.getAttribute("href")).toBe("/mounted/reports/1?tab=chart");
161
+ expect(source?.getAttribute("href")).toBe("/mounted/sources/warehouse");
162
+ expect(container.textContent).toContain("Local data");
163
+
164
+ await act(async () => {
165
+ primary?.dispatchEvent(
166
+ new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }),
167
+ );
168
+ });
169
+
170
+ expect(window.location.pathname).toBe("/mounted/reports/1");
171
+ expect(window.location.search).toBe("?tab=chart");
172
+ });
173
+
174
+ it("opens overflow links and copies the requested text", async () => {
175
+ const container = await renderWidget();
176
+ const menu = await openActionsMenu(container);
177
+ expect(menu).not.toBeNull();
178
+ expect(menu?.textContent).toContain("Open details");
179
+ expect(menu?.textContent).toContain("Copy query");
180
+ expect(menu?.textContent).not.toContain("Unsafe");
181
+
182
+ const details = Array.from(menu?.querySelectorAll("a") ?? []).find((link) =>
183
+ link.textContent?.includes("Open details"),
184
+ );
185
+ expect(details?.getAttribute("href")).toBe("/mounted/reports/1/details");
186
+
187
+ await act(async () => {
188
+ details?.dispatchEvent(
189
+ new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }),
190
+ );
191
+ });
192
+ expect(window.location.pathname).toBe("/mounted/reports/1/details");
193
+
194
+ const secondContainer = await renderWidget();
195
+ const secondMenu = await openActionsMenu(secondContainer);
196
+ const copy = Array.from(
197
+ secondMenu?.querySelectorAll<HTMLElement>("[role=menuitem]") ?? [],
198
+ ).find((item) => item.textContent?.includes("Copy query"));
199
+
200
+ await act(async () => {
201
+ copy?.click();
202
+ await Promise.resolve();
203
+ });
204
+
205
+ expect(mocks.writeClipboardText).toHaveBeenCalledWith("select count(*)");
206
+ expect(mocks.toast).toHaveBeenCalledWith({ title: "Copied" });
207
+ });
208
+
209
+ it("renders actions attached to the chart payload", async () => {
210
+ const container = await renderWidget([
211
+ {
212
+ label: "Explore results",
213
+ href: "/reports/1/explore",
214
+ icon: "explore",
215
+ },
216
+ {
217
+ label: "Download image",
218
+ href: "/reports/1/image",
219
+ icon: "camera",
220
+ },
221
+ {
222
+ label: "Save to notebook",
223
+ href: "/notebooks/new",
224
+ icon: "notebook",
225
+ },
226
+ ]);
227
+ const menu = await openActionsMenu(container);
228
+
229
+ expect(menu?.textContent).toContain("Explore results");
230
+ expect(menu?.textContent).toContain("Download image");
231
+ expect(menu?.textContent).toContain("Save to notebook");
232
+ expect(
233
+ menu?.querySelector('a[href="/mounted/reports/1/explore"] svg'),
234
+ ).not.toBeNull();
235
+ expect(
236
+ menu?.querySelector('a[href="/mounted/reports/1/image"] svg'),
237
+ ).not.toBeNull();
238
+ expect(
239
+ menu?.querySelector('a[href="/mounted/notebooks/new"] svg'),
240
+ ).not.toBeNull();
241
+ });
242
+
243
+ it("keeps chart actions as links when payload metadata includes a type", async () => {
244
+ const container = await renderWidget([
245
+ {
246
+ type: "copy",
247
+ label: "Explore results",
248
+ href: "/reports/1/explore",
249
+ } as DataWidgetAction,
250
+ ]);
251
+ const menu = await openActionsMenu(container);
252
+
253
+ expect(
254
+ menu?.querySelector('a[href="/mounted/reports/1/explore"]'),
255
+ ).not.toBeNull();
256
+ });
257
+
258
+ it("does not render executable primary-action URLs", async () => {
259
+ const container = document.createElement("div");
260
+ document.body.appendChild(container);
261
+ const root = createRoot(container);
262
+ roots.push(root);
263
+
264
+ await act(async () => {
265
+ root.render(
266
+ <AgentNativeI18nProvider
267
+ catalog={toolkitI18nCatalog}
268
+ persistPreference={false}
269
+ >
270
+ <DataChartWidget
271
+ chart={{
272
+ type: "bar",
273
+ xKey: "day",
274
+ series: [{ key: "count", label: "Count" }],
275
+ data: [],
276
+ }}
277
+ display={{
278
+ primaryAction: { label: "Unsafe", href: "javascript:alert(1)" },
279
+ }}
280
+ />
281
+ </AgentNativeI18nProvider>,
282
+ );
283
+ });
284
+
285
+ expect(container.querySelector("a")).toBeNull();
286
+ });
287
+
288
+ it("uses the chart-selected content height without adding body padding", async () => {
289
+ const container = document.createElement("div");
290
+ document.body.appendChild(container);
291
+ const root = createRoot(container);
292
+ roots.push(root);
293
+
294
+ await act(async () => {
295
+ root.render(
296
+ <AgentNativeI18nProvider
297
+ catalog={toolkitI18nCatalog}
298
+ persistPreference={false}
299
+ >
300
+ <DataChartWidget
301
+ chart={{
302
+ type: "bar",
303
+ xKey: "day",
304
+ contentHeight: 400,
305
+ contentPadding: 16,
306
+ labelFontFamily: "sans",
307
+ labelFontSize: 14,
308
+ series: [{ key: "count", label: "Count" }],
309
+ data: [],
310
+ }}
311
+ />
312
+ </AgentNativeI18nProvider>,
313
+ );
314
+ });
315
+
316
+ const frame = container.querySelector(".my-4");
317
+ const header = frame?.querySelector(".h-12");
318
+ const chartSurface = frame?.querySelector<HTMLElement>(
319
+ "[data-testid=chart-surface]",
320
+ );
321
+ expect(frame?.className).toContain("rounded-[3px]");
322
+ expect(header?.className).toContain("border-b");
323
+ expect(chartSurface?.dataset.contentHeight).toBe("400");
324
+ expect(chartSurface?.dataset.contentPadding).toBe("16");
325
+ expect(chartSurface?.dataset.labelFontFamily).toBe("sans");
326
+ expect(chartSurface?.dataset.labelFontSize).toBe("14");
327
+ expect(frame?.querySelector(".p-3")).toBeNull();
328
+ });
329
+ });
@@ -18,11 +18,36 @@ describe("DataChartWidget SSR", () => {
18
18
  series: [{ key: "count", label: "Count" }],
19
19
  data: [{ day: "2026-06-17", count: 3 }],
20
20
  }}
21
+ display={{
22
+ primaryAction: {
23
+ label: "Open report",
24
+ href: "https://reports.example.com/one",
25
+ },
26
+ }}
21
27
  />,
22
28
  );
23
29
 
24
30
  expect(html).toContain("Submissions");
25
31
  expect(html).toContain("Chart");
26
32
  expect(html).not.toContain("Loading chart");
33
+ expect(html).toContain('href="https://reports.example.com/one"');
34
+
35
+ const mailtoHtml = renderToString(
36
+ <DataChartWidget
37
+ chart={{
38
+ type: "bar",
39
+ xKey: "day",
40
+ series: [{ key: "count", label: "Count" }],
41
+ data: [],
42
+ }}
43
+ display={{
44
+ primaryAction: {
45
+ label: "Email report",
46
+ href: "mailto:reports@example.com",
47
+ },
48
+ }}
49
+ />,
50
+ );
51
+ expect(mailtoHtml).toContain('href="mailto:reports@example.com"');
27
52
  });
28
53
  });
@@ -1,12 +1,14 @@
1
- import {
2
- DEFAULT_LOCALE,
3
- useOptionalLocale,
4
- useT,
5
- } from "@agent-native/core/client/i18n";
6
- import { IconChartBar } from "@tabler/icons-react";
1
+ import { useT } from "@agent-native/core/client/i18n";
7
2
  import { lazy, Suspense, useEffect, useState } from "react";
8
3
 
9
- import type { DataChartWidget as DataChartWidgetData } from "./data-widget-types.js";
4
+ import type {
5
+ DataChartWidget as DataChartWidgetData,
6
+ DataWidgetDisplay,
7
+ } from "./data-widget-types.js";
8
+ import {
9
+ DataWidgetDisplayControls,
10
+ DataWidgetSourceChips,
11
+ } from "./DataWidgetDisplayControls.js";
10
12
 
11
13
  const LazyDataChartRenderer = lazy(() =>
12
14
  import("./DataChartRenderer.js").then((module) => ({
@@ -14,47 +16,60 @@ const LazyDataChartRenderer = lazy(() =>
14
16
  })),
15
17
  );
16
18
 
17
- export function DataChartWidget({ chart }: { chart: DataChartWidgetData }) {
19
+ export function DataChartWidget({
20
+ chart,
21
+ display,
22
+ }: {
23
+ chart: DataChartWidgetData;
24
+ display?: DataWidgetDisplay;
25
+ }) {
18
26
  const t = useT();
19
- const locale = useOptionalLocale()?.locale ?? DEFAULT_LOCALE;
20
27
  const [mounted, setMounted] = useState(false);
28
+ const contentHeight = chart.contentHeight;
29
+ const contentPadding = chart.contentPadding ?? 0;
21
30
 
22
31
  useEffect(() => {
23
32
  setMounted(true);
24
33
  }, []);
25
34
 
26
35
  const fallback = (
27
- <div className="flex h-60 items-center justify-center rounded-md bg-muted/30 text-xs text-muted-foreground">
36
+ <div
37
+ className={`box-border flex w-full items-center justify-center rounded-md bg-muted/30 text-xs text-muted-foreground ${contentHeight ? "" : "aspect-[16/9]"}`}
38
+ style={
39
+ contentHeight !== undefined || contentPadding > 0
40
+ ? {
41
+ ...(contentHeight !== undefined
42
+ ? { height: `${contentHeight}px` }
43
+ : {}),
44
+ padding: `${contentPadding}px`,
45
+ }
46
+ : undefined
47
+ }
48
+ >
28
49
  {t("agentChat.widget.chart")}
29
50
  </div>
30
51
  );
31
52
 
32
53
  return (
33
- <div className="my-1.5 overflow-hidden rounded-lg border border-border bg-background text-foreground shadow-sm">
34
- <div className="flex items-center gap-2 border-b border-border px-3 py-2">
35
- <IconChartBar className="h-4 w-4 shrink-0 text-muted-foreground" />
36
- <div className="min-w-0 flex-1">
37
- <div className="truncate text-sm font-medium">
38
- {chart.title ?? t("agentChat.widget.dataChart")}
39
- </div>
40
- <div className="text-[11px] text-muted-foreground">
41
- {t("agentChat.widget.points", {
42
- count: chart.data.length,
43
- formattedCount: chart.data.length.toLocaleString(locale),
44
- })}
45
- {chart.sampled ? ` ${t("agentChat.widget.sampled")}` : ""}
46
- </div>
54
+ <div className="my-4 overflow-hidden rounded-[3px] border border-border bg-background text-foreground shadow-sm">
55
+ <div className="flex h-12 items-center gap-2 border-b border-border px-3">
56
+ <div className="min-w-0 flex-1 truncate text-sm font-medium">
57
+ {chart.title ?? t("agentChat.widget.dataChart")}
47
58
  </div>
59
+ <DataWidgetDisplayControls
60
+ display={display}
61
+ chartActions={chart.actions}
62
+ variant="secondary"
63
+ />
48
64
  </div>
49
- <div className="p-3">
50
- {!mounted ? (
51
- fallback
52
- ) : (
53
- <Suspense fallback={fallback}>
54
- <LazyDataChartRenderer chart={chart} />
55
- </Suspense>
56
- )}
57
- </div>
65
+ {!mounted ? (
66
+ fallback
67
+ ) : (
68
+ <Suspense fallback={fallback}>
69
+ <LazyDataChartRenderer chart={chart} />
70
+ </Suspense>
71
+ )}
72
+ <DataWidgetSourceChips sources={display?.sources ?? []} />
58
73
  </div>
59
74
  );
60
75
  }
@@ -49,12 +49,12 @@ export function DataInsightsWidget({ result }: { result: DataWidgetResult }) {
49
49
  </div>
50
50
  </div>
51
51
  {result.chartSeries ? (
52
- <DataChartWidget chart={result.chartSeries} />
52
+ <DataChartWidget chart={result.chartSeries} display={result.display} />
53
53
  ) : null}
54
54
  {result.table ? (
55
55
  <DataTableWidget
56
56
  table={result.table}
57
- action={result.display?.primaryAction}
57
+ display={result.chartSeries ? undefined : result.display}
58
58
  />
59
59
  ) : null}
60
60
  </div>
@@ -3,7 +3,7 @@
3
3
  import { AgentNativeI18nProvider } from "@agent-native/core/client/i18n";
4
4
  import React, { act } from "react";
5
5
  import { createRoot, type Root } from "react-dom/client";
6
- import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
7
 
8
8
  import { downloadFile } from "../../../db-admin/export-utils.js";
9
9
  import { createToolkitI18nCatalog } from "../../../i18n.js";
@@ -23,7 +23,16 @@ vi.mock("../../../db-admin/export-utils.js", async (importOriginal) => {
23
23
  const roots: Root[] = [];
24
24
  const initialBasePath = process.env.VITE_APP_BASE_PATH;
25
25
 
26
- async function renderWidget(action: { label: string; href: string }) {
26
+ async function renderWidget(
27
+ action: { label: string; href: string },
28
+ display?: {
29
+ sources?: Array<{ id?: string; label: string; href?: string }>;
30
+ actions?: Array<
31
+ | { id?: string; type: "link"; label: string; href: string }
32
+ | { id?: string; type: "copy"; label: string; text: string }
33
+ >;
34
+ },
35
+ ) {
27
36
  const container = document.createElement("div");
28
37
  document.body.appendChild(container);
29
38
  const root = createRoot(container);
@@ -36,6 +45,7 @@ async function renderWidget(action: { label: string; href: string }) {
36
45
  >
37
46
  <DataTableWidget
38
47
  action={action}
48
+ display={display}
39
49
  table={{
40
50
  title: "Responses",
41
51
  columns: [{ key: "name", label: "Name" }],
@@ -49,13 +59,18 @@ async function renderWidget(action: { label: string; href: string }) {
49
59
  }
50
60
 
51
61
  describe("DataTableWidget", () => {
62
+ beforeEach(() => {
63
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
64
+ });
65
+
52
66
  afterEach(() => {
53
67
  for (const root of roots.splice(0)) {
54
- root.unmount();
68
+ act(() => root.unmount());
55
69
  }
56
70
  document.body.innerHTML = "";
57
71
  process.env.VITE_APP_BASE_PATH = initialBasePath;
58
72
  vi.mocked(downloadFile).mockClear();
73
+ vi.unstubAllGlobals();
59
74
  });
60
75
 
61
76
  it("does not render executable action URLs", async () => {
@@ -99,6 +114,25 @@ describe("DataTableWidget", () => {
99
114
  ).toBeNull();
100
115
  });
101
116
 
117
+ it("renders table source chips and overflow actions from display metadata", async () => {
118
+ const container = await renderWidget(
119
+ { label: "Open responses", href: "/forms/form-1" },
120
+ {
121
+ sources: [{ label: "Responses", href: "/sources/responses" }],
122
+ actions: [{ type: "copy", label: "Copy query", text: "select 1" }],
123
+ },
124
+ );
125
+
126
+ expect(
127
+ container.querySelector('button[aria-label="Table actions"]'),
128
+ ).not.toBeNull();
129
+ expect(
130
+ Array.from(container.querySelectorAll("a")).some((link) =>
131
+ link.textContent?.includes("Responses"),
132
+ ),
133
+ ).toBe(true);
134
+ });
135
+
102
136
  it("exports visible table rows from the visible CSV download", async () => {
103
137
  const container = document.createElement("div");
104
138
  document.body.appendChild(container);