@agent-native/toolkit 0.216.0 → 0.218.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 (261) hide show
  1. package/agent-native.eject.json +26 -0
  2. package/dist/app/agent-page/AgentContextTab.d.ts.map +1 -1
  3. package/dist/app/agent-page/AgentContextTab.js +1 -0
  4. package/dist/app/agent-page/AgentContextTab.js.map +1 -1
  5. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  6. package/dist/app/agentkit/react/components.js +1 -1
  7. package/dist/app/agentkit/react/components.js.map +1 -1
  8. package/dist/app/agentkit/react/context.d.ts +4 -1
  9. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  10. package/dist/app/agentkit/react/context.js +3 -1
  11. package/dist/app/agentkit/react/context.js.map +1 -1
  12. package/dist/app/agentkit/react/data-chart-layout.d.ts +30 -0
  13. package/dist/app/agentkit/react/data-chart-layout.d.ts.map +1 -0
  14. package/dist/app/agentkit/react/data-chart-layout.js +2 -0
  15. package/dist/app/agentkit/react/data-chart-layout.js.map +1 -0
  16. package/dist/app/agentkit/react/index.d.ts +1 -0
  17. package/dist/app/agentkit/react/index.d.ts.map +1 -1
  18. package/dist/app/agentkit/react/index.js +1 -0
  19. package/dist/app/agentkit/react/index.js.map +1 -1
  20. package/dist/app/agentkit/react/root.d.ts +3 -1
  21. package/dist/app/agentkit/react/root.d.ts.map +1 -1
  22. package/dist/app/agentkit/react/root.js +2 -2
  23. package/dist/app/agentkit/react/root.js.map +1 -1
  24. package/dist/app/agentkit/react/styles.css +18 -0
  25. package/dist/app/agentkit/react/transcript-layout.d.ts +2 -0
  26. package/dist/app/agentkit/react/transcript-layout.d.ts.map +1 -1
  27. package/dist/app/agentkit/react/transcript-layout.js.map +1 -1
  28. package/dist/app/auth/AuthPage.d.ts.map +1 -1
  29. package/dist/app/auth/AuthPage.js +33 -44
  30. package/dist/app/auth/AuthPage.js.map +1 -1
  31. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  32. package/dist/app/chat/AgentKitAssistantChat.js +2 -2
  33. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  34. package/dist/app/chat/MultiTabAssistantChat.d.ts.map +1 -1
  35. package/dist/app/chat/MultiTabAssistantChat.js +51 -93
  36. package/dist/app/chat/MultiTabAssistantChat.js.map +1 -1
  37. package/dist/app/chat/agentkit-chat/action-widget.d.ts.map +1 -1
  38. package/dist/app/chat/agentkit-chat/action-widget.js +3 -1
  39. package/dist/app/chat/agentkit-chat/action-widget.js.map +1 -1
  40. package/dist/app/chat/agentkit-chat/rail.d.ts +1 -1
  41. package/dist/app/chat/agentkit-chat/rail.d.ts.map +1 -1
  42. package/dist/app/chat/agentkit-chat/rail.js +1 -1
  43. package/dist/app/chat/agentkit-chat/rail.js.map +1 -1
  44. package/dist/app/chat/chat/index.d.ts +2 -1
  45. package/dist/app/chat/chat/index.d.ts.map +1 -1
  46. package/dist/app/chat/chat/index.js +1 -1
  47. package/dist/app/chat/chat/index.js.map +1 -1
  48. package/dist/app/chat/chat/surface-types.d.ts +3 -0
  49. package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
  50. package/dist/app/chat/chat/surface-types.js.map +1 -1
  51. package/dist/app/chat/chat/tool-call-display.d.ts +3 -1
  52. package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
  53. package/dist/app/chat/chat/tool-call-display.js +4 -3
  54. package/dist/app/chat/chat/tool-call-display.js.map +1 -1
  55. package/dist/app/chat/chat/tool-render-registry.d.ts +2 -0
  56. package/dist/app/chat/chat/tool-render-registry.d.ts.map +1 -1
  57. package/dist/app/chat/chat/tool-render-registry.js.map +1 -1
  58. package/dist/app/chat/chat/widgets/DataChartRenderer.d.ts +3 -1
  59. package/dist/app/chat/chat/widgets/DataChartRenderer.d.ts.map +1 -1
  60. package/dist/app/chat/chat/widgets/DataChartRenderer.js +116 -26
  61. package/dist/app/chat/chat/widgets/DataChartRenderer.js.map +1 -1
  62. package/dist/app/chat/chat/widgets/DataChartWidget.d.ts +3 -1
  63. package/dist/app/chat/chat/widgets/DataChartWidget.d.ts.map +1 -1
  64. package/dist/app/chat/chat/widgets/DataChartWidget.js +21 -2
  65. package/dist/app/chat/chat/widgets/DataChartWidget.js.map +1 -1
  66. package/dist/app/chat/chat/widgets/DataInsightsWidget.d.ts +3 -1
  67. package/dist/app/chat/chat/widgets/DataInsightsWidget.d.ts.map +1 -1
  68. package/dist/app/chat/chat/widgets/DataInsightsWidget.js +2 -2
  69. package/dist/app/chat/chat/widgets/DataInsightsWidget.js.map +1 -1
  70. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts +1 -1
  71. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts.map +1 -1
  72. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js +3 -1
  73. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js.map +1 -1
  74. package/dist/app/chat/chat/widgets/builtin-tool-renderers.d.ts.map +1 -1
  75. package/dist/app/chat/chat/widgets/builtin-tool-renderers.js +2 -2
  76. package/dist/app/chat/chat/widgets/builtin-tool-renderers.js.map +1 -1
  77. package/dist/app/chat/chat-first/chat-history.d.ts +13 -2
  78. package/dist/app/chat/chat-first/chat-history.d.ts.map +1 -1
  79. package/dist/app/chat/chat-first/chat-history.js +24 -3
  80. package/dist/app/chat/chat-first/chat-history.js.map +1 -1
  81. package/dist/app/chat/ownership-history/ChatOwnerAvatar.d.ts +6 -0
  82. package/dist/app/chat/ownership-history/ChatOwnerAvatar.d.ts.map +1 -0
  83. package/dist/app/chat/ownership-history/ChatOwnerAvatar.js +9 -0
  84. package/dist/app/chat/ownership-history/ChatOwnerAvatar.js.map +1 -0
  85. package/dist/app/chat/ownership-history/index.d.ts +11 -0
  86. package/dist/app/chat/ownership-history/index.d.ts.map +1 -0
  87. package/dist/app/chat/ownership-history/index.js +22 -0
  88. package/dist/app/chat/ownership-history/index.js.map +1 -0
  89. package/dist/app/chat/ownership-history/rows.d.ts +27 -0
  90. package/dist/app/chat/ownership-history/rows.d.ts.map +1 -0
  91. package/dist/app/chat/ownership-history/rows.js +101 -0
  92. package/dist/app/chat/ownership-history/rows.js.map +1 -0
  93. package/dist/app/chat/ownership-history/useOwnershipChatHistory.d.ts +52 -0
  94. package/dist/app/chat/ownership-history/useOwnershipChatHistory.d.ts.map +1 -0
  95. package/dist/app/chat/ownership-history/useOwnershipChatHistory.js +497 -0
  96. package/dist/app/chat/ownership-history/useOwnershipChatHistory.js.map +1 -0
  97. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  98. package/dist/app/i18n/catalogs/ar-SA.js +3 -0
  99. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  100. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  101. package/dist/app/i18n/catalogs/de-DE.js +3 -0
  102. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  103. package/dist/app/i18n/catalogs/en-US.d.ts +3 -0
  104. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  105. package/dist/app/i18n/catalogs/en-US.js +3 -0
  106. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  107. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  108. package/dist/app/i18n/catalogs/es-ES.js +3 -0
  109. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  110. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  111. package/dist/app/i18n/catalogs/fr-FR.js +3 -0
  112. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  113. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  114. package/dist/app/i18n/catalogs/hi-IN.js +3 -0
  115. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  116. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  117. package/dist/app/i18n/catalogs/ja-JP.js +3 -0
  118. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  119. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  120. package/dist/app/i18n/catalogs/ko-KR.js +3 -0
  121. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  122. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  123. package/dist/app/i18n/catalogs/pt-BR.js +3 -0
  124. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  125. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  126. package/dist/app/i18n/catalogs/zh-CN.js +3 -0
  127. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  128. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  129. package/dist/app/i18n/catalogs/zh-TW.js +3 -0
  130. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  131. package/dist/app/onboarding/GettingStartedCard.d.ts +35 -0
  132. package/dist/app/onboarding/GettingStartedCard.d.ts.map +1 -0
  133. package/dist/app/onboarding/GettingStartedCard.js +38 -0
  134. package/dist/app/onboarding/GettingStartedCard.js.map +1 -0
  135. package/dist/app/onboarding/index.d.ts +1 -0
  136. package/dist/app/onboarding/index.d.ts.map +1 -1
  137. package/dist/app/onboarding/index.js +1 -0
  138. package/dist/app/onboarding/index.js.map +1 -1
  139. package/dist/app/providers/AppProviders.d.ts.map +1 -1
  140. package/dist/app/providers/AppProviders.js +3 -1
  141. package/dist/app/providers/AppProviders.js.map +1 -1
  142. package/dist/app/settings/AccountSettingsCard.d.ts.map +1 -1
  143. package/dist/app/settings/AccountSettingsCard.js +2 -1
  144. package/dist/app/settings/AccountSettingsCard.js.map +1 -1
  145. package/dist/app/settings/index.d.ts +1 -0
  146. package/dist/app/settings/index.d.ts.map +1 -1
  147. package/dist/app/settings/index.js +1 -0
  148. package/dist/app/settings/index.js.map +1 -1
  149. package/dist/app/settings/shell/SettingsShell.d.ts +40 -1
  150. package/dist/app/settings/shell/SettingsShell.d.ts.map +1 -1
  151. package/dist/app/settings/shell/SettingsShell.js +87 -20
  152. package/dist/app/settings/shell/SettingsShell.js.map +1 -1
  153. package/dist/app/settings/shell/bridge.d.ts +5 -3
  154. package/dist/app/settings/shell/bridge.d.ts.map +1 -1
  155. package/dist/app/settings/shell/bridge.js +8 -5
  156. package/dist/app/settings/shell/bridge.js.map +1 -1
  157. package/dist/app/settings/shell/index.d.ts +2 -2
  158. package/dist/app/settings/shell/index.d.ts.map +1 -1
  159. package/dist/app/settings/shell/index.js.map +1 -1
  160. package/dist/app/settings/shell/registry.d.ts +9 -1
  161. package/dist/app/settings/shell/registry.d.ts.map +1 -1
  162. package/dist/app/settings/shell/registry.js +16 -0
  163. package/dist/app/settings/shell/registry.js.map +1 -1
  164. package/dist/app/settings/shell/routing.d.ts +5 -4
  165. package/dist/app/settings/shell/routing.d.ts.map +1 -1
  166. package/dist/app/settings/shell/routing.js +38 -10
  167. package/dist/app/settings/shell/routing.js.map +1 -1
  168. package/dist/app/shared/CommandMenu.d.ts +86 -6
  169. package/dist/app/shared/CommandMenu.d.ts.map +1 -1
  170. package/dist/app/shared/CommandMenu.js +303 -41
  171. package/dist/app/shared/CommandMenu.js.map +1 -1
  172. package/dist/app/shared/ConsentBanner.d.ts +9 -0
  173. package/dist/app/shared/ConsentBanner.d.ts.map +1 -0
  174. package/dist/app/shared/ConsentBanner.js +35 -0
  175. package/dist/app/shared/ConsentBanner.js.map +1 -0
  176. package/dist/app/sharing/ShareButton.d.ts.map +1 -1
  177. package/dist/app/sharing/ShareButton.js +9 -8
  178. package/dist/app/sharing/ShareButton.js.map +1 -1
  179. package/dist/chat-history/ChatHistoryList.d.ts +16 -3
  180. package/dist/chat-history/ChatHistoryList.d.ts.map +1 -1
  181. package/dist/chat-history/ChatHistoryList.js +25 -15
  182. package/dist/chat-history/ChatHistoryList.js.map +1 -1
  183. package/dist/chat-history.css +109 -9
  184. package/dist/ui/sonner.d.ts +2 -2
  185. package/dist/ui/sonner.d.ts.map +1 -1
  186. package/dist/ui/sonner.js +2 -2
  187. package/dist/ui/sonner.js.map +1 -1
  188. package/package.json +10 -3
  189. package/src/app/agent-page/AgentContextTab.spec.ts +50 -2
  190. package/src/app/agent-page/AgentContextTab.tsx +1 -0
  191. package/src/app/agentkit/react/components.interaction.spec.tsx +2 -0
  192. package/src/app/agentkit/react/components.spec.tsx +4 -1
  193. package/src/app/agentkit/react/components.tsx +3 -0
  194. package/src/app/agentkit/react/context.tsx +6 -0
  195. package/src/app/agentkit/react/data-chart-layout.ts +25 -0
  196. package/src/app/agentkit/react/index.ts +1 -0
  197. package/src/app/agentkit/react/root.tsx +4 -0
  198. package/src/app/agentkit/react/styles.css +18 -0
  199. package/src/app/agentkit/react/transcript-layout.ts +2 -0
  200. package/src/app/auth/AuthPage.interaction.spec.tsx +74 -0
  201. package/src/app/auth/AuthPage.tsx +32 -39
  202. package/src/app/chat/AgentKitAssistantChat.tsx +2 -0
  203. package/src/app/chat/MultiTabAssistantChat.spec.tsx +270 -3
  204. package/src/app/chat/MultiTabAssistantChat.tsx +79 -156
  205. package/src/app/chat/agentkit-chat/action-widget.tsx +3 -0
  206. package/src/app/chat/agentkit-chat/rail.ts +11 -0
  207. package/src/app/chat/chat/index.ts +10 -0
  208. package/src/app/chat/chat/surface-types.ts +3 -0
  209. package/src/app/chat/chat/tool-call-display.spec.tsx +29 -0
  210. package/src/app/chat/chat/tool-call-display.tsx +7 -0
  211. package/src/app/chat/chat/tool-render-registry.tsx +3 -0
  212. package/src/app/chat/chat/widgets/DataChartRenderer.spec.tsx +377 -17
  213. package/src/app/chat/chat/widgets/DataChartRenderer.tsx +232 -63
  214. package/src/app/chat/chat/widgets/DataChartWidget.spec.tsx +67 -0
  215. package/src/app/chat/chat/widgets/DataChartWidget.tsx +39 -5
  216. package/src/app/chat/chat/widgets/DataInsightsWidget.tsx +13 -2
  217. package/src/app/chat/chat/widgets/DataWidgetDisplayControls.tsx +6 -4
  218. package/src/app/chat/chat/widgets/builtin-tool-renderers.tsx +5 -1
  219. package/src/app/chat/chat-first/chat-history.spec.tsx +79 -0
  220. package/src/app/chat/chat-first/chat-history.tsx +75 -22
  221. package/src/app/chat/ownership-history/ChatOwnerAvatar.tsx +27 -0
  222. package/src/app/chat/ownership-history/index.tsx +67 -0
  223. package/src/app/chat/ownership-history/ownership-history.spec.tsx +3146 -0
  224. package/src/app/chat/ownership-history/rows.ts +156 -0
  225. package/src/app/chat/ownership-history/useOwnershipChatHistory.ts +709 -0
  226. package/src/app/i18n/catalogs/ar-SA.ts +3 -0
  227. package/src/app/i18n/catalogs/de-DE.ts +3 -0
  228. package/src/app/i18n/catalogs/en-US.ts +3 -0
  229. package/src/app/i18n/catalogs/es-ES.ts +3 -0
  230. package/src/app/i18n/catalogs/fr-FR.ts +3 -0
  231. package/src/app/i18n/catalogs/hi-IN.ts +3 -0
  232. package/src/app/i18n/catalogs/ja-JP.ts +3 -0
  233. package/src/app/i18n/catalogs/ko-KR.ts +3 -0
  234. package/src/app/i18n/catalogs/pt-BR.ts +3 -0
  235. package/src/app/i18n/catalogs/zh-CN.ts +3 -0
  236. package/src/app/i18n/catalogs/zh-TW.ts +3 -0
  237. package/src/app/onboarding/GettingStartedCard.tsx +203 -0
  238. package/src/app/onboarding/index.ts +1 -0
  239. package/src/app/providers/AppProviders.tsx +7 -1
  240. package/src/app/settings/AccountSettingsCard.spec.tsx +31 -0
  241. package/src/app/settings/AccountSettingsCard.tsx +8 -0
  242. package/src/app/settings/index.ts +1 -0
  243. package/src/app/settings/shell/SettingsShell.spec.tsx +472 -1
  244. package/src/app/settings/shell/SettingsShell.tsx +245 -31
  245. package/src/app/settings/shell/bridge.spec.ts +12 -0
  246. package/src/app/settings/shell/bridge.ts +17 -4
  247. package/src/app/settings/shell/index.ts +6 -1
  248. package/src/app/settings/shell/registry.ts +33 -1
  249. package/src/app/settings/shell/routing.spec.ts +92 -1
  250. package/src/app/settings/shell/routing.ts +52 -9
  251. package/src/app/shared/CommandMenu.spec.tsx +1662 -1
  252. package/src/app/shared/CommandMenu.tsx +763 -164
  253. package/src/app/shared/ConsentBanner.spec.tsx +141 -0
  254. package/src/app/shared/ConsentBanner.tsx +141 -0
  255. package/src/app/sharing/ShareButton.tsx +12 -10
  256. package/src/chat-history/ChatHistoryList.spec.tsx +51 -0
  257. package/src/chat-history/ChatHistoryList.tsx +87 -26
  258. package/src/chat-history/ChatHistoryRailSections.spec.tsx +12 -4
  259. package/src/chat-history.css +109 -9
  260. package/src/ui/sonner.spec.ts +10 -0
  261. package/src/ui/sonner.tsx +2 -2
@@ -29,7 +29,11 @@ vi.mock("@agent-native/core/client/feature-flags/use-feature-flag", () => ({
29
29
  useFeatureFlags: () => ({}),
30
30
  }));
31
31
  vi.mock("../../chat/AgentSidebar.js", () => ({
32
- AgentToggleButton: () => <button type="button">agent</button>,
32
+ AgentToggleButton: () => (
33
+ <button type="button" data-testid="shared-agent-toggle">
34
+ agent
35
+ </button>
36
+ ),
33
37
  }));
34
38
 
35
39
  const appState = vi.hoisted(() => ({
@@ -52,7 +56,10 @@ import { useSettingsPageHeader, useSettingsShell } from "./context.js";
52
56
  import { CORE_SETTINGS_PAGES } from "./core-pages.js";
53
57
  import {
54
58
  _resetSettingsPagesForTests,
59
+ getSettingsPages,
60
+ mergeAppSettingsPages,
55
61
  registerSettingsPages,
62
+ type AppSettingsPageDefinition,
56
63
  type SettingsPageProps,
57
64
  } from "./registry.js";
58
65
  import {
@@ -83,6 +90,42 @@ function StubPage({ pageId, sub }: SettingsPageProps) {
83
90
  );
84
91
  }
85
92
 
93
+ function AppSettingsPage({ pageId, sub }: SettingsPageProps) {
94
+ const { navigate } = useSettingsShell();
95
+ return (
96
+ <div data-testid="app-settings-page">
97
+ page:{pageId}
98
+ {sub ? ` sub:${sub}` : ""}
99
+ <label>
100
+ App control
101
+ <input aria-label="App control" type="checkbox" />
102
+ </label>
103
+ <button
104
+ type="button"
105
+ onClick={() => navigate(pageId, "delivery")}
106
+ data-testid="open-app-subpage"
107
+ >
108
+ Delivery
109
+ </button>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ function appPage(
115
+ id: string,
116
+ label: string,
117
+ order = 25,
118
+ component: AppSettingsPageDefinition["component"] = StubPage,
119
+ ): AppSettingsPageDefinition {
120
+ return {
121
+ id,
122
+ label,
123
+ order,
124
+ icon: CORE_SETTINGS_PAGES[0]!.icon,
125
+ component,
126
+ };
127
+ }
128
+
86
129
  async function flush() {
87
130
  await act(async () => {
88
131
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -209,6 +252,395 @@ describe("SettingsShell", () => {
209
252
  expect(container.textContent).toContain("Back to Clips");
210
253
  });
211
254
 
255
+ it("adds ordered app-owned pages and renders their custom controls", async () => {
256
+ await render({
257
+ notifications: <p>Email notifications</p>,
258
+ appSettingsPages: [
259
+ appPage(
260
+ "custom-preferences",
261
+ "Custom preferences",
262
+ 25,
263
+ AppSettingsPage,
264
+ ),
265
+ appPage("custom-options", "Custom options", 27),
266
+ ],
267
+ });
268
+
269
+ expect(groupLabels().find((group) => group.id === "app")?.pages).toEqual([
270
+ "app",
271
+ "notifications",
272
+ "custom-preferences",
273
+ "custom-options",
274
+ "automations",
275
+ "channels",
276
+ "mcp",
277
+ "labs",
278
+ ]);
279
+ clickPage("custom-preferences");
280
+ await waitFor(() =>
281
+ Boolean(container.querySelector('[data-testid="app-settings-page"]')),
282
+ );
283
+ expect(
284
+ container.querySelector('[data-testid="app-settings-page"]')?.textContent,
285
+ ).toContain("page:custom-preferences");
286
+ expect(
287
+ container.querySelector('[aria-label="App control"]'),
288
+ ).not.toBeNull();
289
+ expect(
290
+ getSettingsPages().some((page) => page.id === "mail-preferences"),
291
+ ).toBe(false);
292
+ });
293
+
294
+ it("renders ordered app sections with page, link, and action entries", async () => {
295
+ const onAddWorkspace = vi.fn();
296
+ const customPage = {
297
+ ...appPage("workspace-alpha", "Workspace Alpha", 25, AppSettingsPage),
298
+ subpages: [{ id: "settings", label: "Workspace settings" }],
299
+ };
300
+
301
+ await render({
302
+ appSettingsPages: [customPage],
303
+ appNavigationSections: [
304
+ {
305
+ id: "workspaces",
306
+ label: "Workspaces",
307
+ items: [
308
+ {
309
+ id: "workspace-overview",
310
+ type: "page",
311
+ pageId: "workspace-alpha",
312
+ label: "Workspace Alpha",
313
+ },
314
+ {
315
+ id: "workspace-settings",
316
+ type: "page",
317
+ pageId: "workspace-alpha",
318
+ sub: "settings",
319
+ label: "Workspace settings",
320
+ },
321
+ {
322
+ id: "workspace-help",
323
+ type: "link",
324
+ label: "Workspace help",
325
+ href: "/help",
326
+ visibleWhen: () => false,
327
+ },
328
+ {
329
+ id: "add-workspace",
330
+ type: "action",
331
+ label: "Add workspace",
332
+ onSelect: onAddWorkspace,
333
+ },
334
+ ],
335
+ },
336
+ {
337
+ id: "administration",
338
+ label: "Administration",
339
+ visibleWhen: (context) => context.isAdmin,
340
+ items: [
341
+ {
342
+ id: "invite-member",
343
+ type: "action",
344
+ label: "Invite member",
345
+ onSelect: () => undefined,
346
+ },
347
+ ],
348
+ },
349
+ ],
350
+ });
351
+
352
+ expect(
353
+ [...rail().querySelectorAll<HTMLElement>("[data-settings-group]")].map(
354
+ (group) => group.dataset.settingsGroup,
355
+ ),
356
+ ).toEqual(["workspaces"]);
357
+ expect(
358
+ rail().querySelector('[data-settings-link="workspace-help"]'),
359
+ ).toBeNull();
360
+ expect(
361
+ rail()
362
+ .querySelector('[data-settings-nav-item="workspace-settings"]')
363
+ ?.getAttribute("href"),
364
+ ).toBe("/settings/workspace-alpha/settings");
365
+
366
+ act(() => {
367
+ rail()
368
+ .querySelector<HTMLButtonElement>(
369
+ '[data-settings-action="add-workspace"]',
370
+ )
371
+ ?.click();
372
+ });
373
+ expect(onAddWorkspace).toHaveBeenCalled();
374
+
375
+ const subpageLink = rail().querySelector<HTMLElement>(
376
+ '[data-settings-nav-item="workspace-settings"]',
377
+ );
378
+ expect(subpageLink?.getAttribute("href")).toBe(
379
+ "/settings/workspace-alpha/settings",
380
+ );
381
+ act(() => {
382
+ subpageLink?.dispatchEvent(
383
+ new MouseEvent("click", { bubbles: true, cancelable: true }),
384
+ );
385
+ });
386
+ expect(
387
+ rail().querySelector(
388
+ '[data-settings-nav-item="workspace-overview"][aria-current="page"]',
389
+ ),
390
+ ).toBeNull();
391
+ expect(
392
+ rail().querySelector(
393
+ '[data-settings-nav-item="workspace-settings"][aria-current="page"]',
394
+ ),
395
+ ).not.toBeNull();
396
+ await waitFor(() =>
397
+ Boolean(container.querySelector('[data-testid="app-settings-page"]')),
398
+ );
399
+ });
400
+
401
+ it("keeps controlled template tabs ahead of a same-id app page", async () => {
402
+ const extraTab = {
403
+ id: "workspace-alpha",
404
+ label: "Template workspace",
405
+ content: "Template workspace content",
406
+ };
407
+ const customPage = appPage(
408
+ "workspace-alpha",
409
+ "Workspace alpha",
410
+ 5,
411
+ AppSettingsPage,
412
+ );
413
+
414
+ await render({
415
+ value: "profile",
416
+ initialValue: "profile",
417
+ extraTabs: [extraTab],
418
+ appSettingsPages: [customPage],
419
+ });
420
+ await act(async () => {
421
+ root.render(
422
+ renderWithToolkitI18n(
423
+ <SettingsShell
424
+ appName="Clips"
425
+ value="workspace-alpha"
426
+ initialValue="profile"
427
+ extraTabs={[extraTab]}
428
+ appSettingsPages={[customPage]}
429
+ />,
430
+ ),
431
+ );
432
+ });
433
+ await waitFor(() =>
434
+ window.location.pathname.endsWith("/app-workspace-alpha"),
435
+ );
436
+ });
437
+
438
+ it("mounts an app chat panel and replaces the shared toggle by default", async () => {
439
+ await render({
440
+ agentPanel: <div data-testid="app-agent-panel">Local chat</div>,
441
+ agentToggle: (
442
+ <button type="button" data-testid="app-agent-toggle">
443
+ Open chat
444
+ </button>
445
+ ),
446
+ });
447
+
448
+ const shell = container.querySelector<HTMLElement>("[data-settings-shell]");
449
+ const panel = shell?.querySelector('[data-testid="app-agent-panel"]');
450
+ expect(panel).not.toBeNull();
451
+ expect(panel?.parentElement).toBe(shell);
452
+ expect(
453
+ shell?.querySelector('[data-testid="app-agent-toggle"]'),
454
+ ).not.toBeNull();
455
+ expect(
456
+ shell?.querySelector('[data-testid="shared-agent-toggle"]'),
457
+ ).toBeNull();
458
+ });
459
+
460
+ it("renders an opted-in app page widget slot after the page content", async () => {
461
+ window.history.replaceState(null, "", "/settings/app-help");
462
+ await render({
463
+ appSettingsPages: [
464
+ {
465
+ ...appPage("app-help", "App help"),
466
+ widgetSlot: <div data-testid="settings-help-widget">Help</div>,
467
+ },
468
+ ],
469
+ initialValue: "app-help",
470
+ value: "app-help",
471
+ });
472
+
473
+ const widget = container.querySelector('[data-settings-widget-slot=""]');
474
+ expect(widget?.textContent).toBe("Help");
475
+ expect(widget?.previousElementSibling?.getAttribute("data-testid")).toBe(
476
+ "page",
477
+ );
478
+ });
479
+
480
+ it("does not render an app page widget slot when it is omitted", async () => {
481
+ window.history.replaceState(null, "", "/settings/app-help");
482
+ await render({
483
+ appSettingsPages: [appPage("app-help", "App help")],
484
+ initialValue: "app-help",
485
+ value: "app-help",
486
+ });
487
+
488
+ expect(
489
+ container.querySelector('[data-settings-widget-slot=""]'),
490
+ ).toBeNull();
491
+ });
492
+
493
+ it("keeps template tabs separate when an app page uses the same id", async () => {
494
+ await render({
495
+ appSettingsPages: [
496
+ appPage("custom-controls", "Custom controls", 25, AppSettingsPage),
497
+ ],
498
+ extraTabs: [
499
+ { id: "custom-controls", label: "Template controls", content: "tabs" },
500
+ ],
501
+ });
502
+
503
+ expect(groupLabels().find((group) => group.id === "app")?.pages).toEqual(
504
+ expect.arrayContaining(["custom-controls", "app-custom-controls"]),
505
+ );
506
+ clickPage("custom-controls");
507
+ await waitFor(() =>
508
+ Boolean(container.querySelector('[data-testid="app-settings-page"]')),
509
+ );
510
+ expect(
511
+ container.querySelector('[data-testid="app-settings-page"]')?.textContent,
512
+ ).toContain("page:custom-controls");
513
+ });
514
+
515
+ it("opens an app page when its id matches an app area", async () => {
516
+ await render({
517
+ appAreas: [{ id: "recordings", label: "Recordings", content: null }],
518
+ appSettingsPages: [
519
+ appPage("recordings", "Recordings settings", 25, AppSettingsPage),
520
+ ],
521
+ });
522
+
523
+ clickPage("recordings");
524
+ await waitFor(() =>
525
+ Boolean(container.querySelector('[data-testid="app-settings-page"]')),
526
+ );
527
+
528
+ expect(
529
+ container.querySelector('[data-testid="app-settings-page"]')?.textContent,
530
+ ).toContain("page:recordings");
531
+ expect(window.location.pathname).toBe("/settings/recordings");
532
+ });
533
+
534
+ it("rejects app page ids that would replace an existing page", () => {
535
+ expect(() =>
536
+ mergeAppSettingsPages(CORE_SETTINGS_PAGES, [
537
+ appPage("profile", "Replacement profile"),
538
+ ]),
539
+ ).toThrow(
540
+ 'App settings page id "profile" conflicts with an existing page id.',
541
+ );
542
+ expect(() =>
543
+ mergeAppSettingsPages(
544
+ [],
545
+ [
546
+ appPage("custom-preferences", "First"),
547
+ appPage("custom-preferences", "Second"),
548
+ ],
549
+ ),
550
+ ).toThrow('Duplicate app settings page id "custom-preferences".');
551
+ });
552
+
553
+ it("keeps app pages and search entries isolated between shell instances", async () => {
554
+ const firstRouter = createMemoryRouter(
555
+ [
556
+ {
557
+ path: "/settings/*",
558
+ element: (
559
+ <SettingsShell
560
+ appName="First app"
561
+ appSettingsPages={[
562
+ {
563
+ ...appPage("first-settings", "First settings", 25),
564
+ keywords: "first-only",
565
+ },
566
+ ]}
567
+ />
568
+ ),
569
+ },
570
+ ],
571
+ { initialEntries: ["/settings/first-settings"] },
572
+ );
573
+ const secondRouter = createMemoryRouter(
574
+ [
575
+ {
576
+ path: "/settings/*",
577
+ element: (
578
+ <SettingsShell
579
+ appName="Second app"
580
+ appSettingsPages={[
581
+ {
582
+ ...appPage("second-settings", "Second settings", 25),
583
+ keywords: "second-only",
584
+ },
585
+ ]}
586
+ />
587
+ ),
588
+ },
589
+ ],
590
+ { initialEntries: ["/settings/second-settings"] },
591
+ );
592
+
593
+ await act(async () => {
594
+ root.render(
595
+ renderWithToolkitI18n(
596
+ <>
597
+ <RouterProvider router={firstRouter} />
598
+ <RouterProvider router={secondRouter} />
599
+ </>,
600
+ ),
601
+ );
602
+ });
603
+ await flush();
604
+
605
+ const shells = [
606
+ ...container.querySelectorAll<HTMLElement>("[data-settings-shell]"),
607
+ ];
608
+ expect(shells).toHaveLength(2);
609
+ expect(shells[0]?.querySelector('[data-testid="page"]')?.textContent).toBe(
610
+ "page:first-settings",
611
+ );
612
+ expect(shells[1]?.querySelector('[data-testid="page"]')?.textContent).toBe(
613
+ "page:second-settings",
614
+ );
615
+ expect(
616
+ shells[0]?.querySelector('[data-settings-page="second-settings"]'),
617
+ ).toBeNull();
618
+ expect(
619
+ shells[1]?.querySelector('[data-settings-page="first-settings"]'),
620
+ ).toBeNull();
621
+
622
+ const inputs = shells.map(
623
+ (shell) => shell.querySelector<HTMLInputElement>('input[type="search"]')!,
624
+ );
625
+ const setValue = Object.getOwnPropertyDescriptor(
626
+ HTMLInputElement.prototype,
627
+ "value",
628
+ )!.set!;
629
+ act(() => {
630
+ setValue.call(inputs[0], "first-only");
631
+ inputs[0]!.dispatchEvent(new Event("input", { bubbles: true }));
632
+ setValue.call(inputs[1], "first-only");
633
+ inputs[1]!.dispatchEvent(new Event("input", { bubbles: true }));
634
+ });
635
+ expect(shells[0]?.querySelector('[role="option"]')?.textContent).toContain(
636
+ "First settings",
637
+ );
638
+ expect(shells[1]?.querySelector('[role="option"]')).toBeNull();
639
+ expect(getSettingsPages().map((page) => page.id)).not.toEqual(
640
+ expect.arrayContaining(["first-settings", "second-settings"]),
641
+ );
642
+ });
643
+
212
644
  it("shows Notifications and What's new when the app passes them", async () => {
213
645
  await render({
214
646
  notifications: <div>email</div>,
@@ -530,6 +962,45 @@ describe("SettingsShell", () => {
530
962
  );
531
963
  });
532
964
 
965
+ it("routes an app page search entry to its nested page", async () => {
966
+ const customPage: AppSettingsPageDefinition = {
967
+ ...appPage("custom-controls", "Custom controls", 25, AppSettingsPage),
968
+ subpages: [{ id: "delivery", label: "Delivery settings" }],
969
+ searchEntries: [
970
+ {
971
+ id: "delivery-rules",
972
+ label: "Delivery rules",
973
+ keywords: "exceptions",
974
+ sub: "delivery",
975
+ },
976
+ ],
977
+ };
978
+ await render({ appSettingsPages: [customPage] });
979
+
980
+ clickPage("custom-controls");
981
+ await waitFor(() =>
982
+ Boolean(container.querySelector('[data-testid="app-settings-page"]')),
983
+ );
984
+ expect(
985
+ container.querySelector('[aria-label="App control"]'),
986
+ ).not.toBeNull();
987
+
988
+ expect(search("delivery exceptions")[0]).toContain("Delivery rules");
989
+ press("Enter");
990
+ await flush();
991
+ expect(window.location.pathname).toBe(
992
+ "/settings/custom-controls/delivery",
993
+ );
994
+ expect(
995
+ container.querySelector('header nav [aria-current="page"]')
996
+ ?.textContent,
997
+ ).toBe("Delivery settings");
998
+ expect(
999
+ container.querySelector('[data-testid="app-settings-page"]')
1000
+ ?.textContent,
1001
+ ).toContain("sub:delivery");
1002
+ });
1003
+
533
1004
  it("focuses on / and clears on Escape", async () => {
534
1005
  await render();
535
1006
  act(() => {