@agent-native/toolkit 0.214.0 → 0.217.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 (203) hide show
  1. package/agent-native.eject.json +5 -0
  2. package/dist/app/agentkit/react/chat.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/chat.js +2 -2
  4. package/dist/app/agentkit/react/chat.js.map +1 -1
  5. package/dist/app/agentkit/react/components.d.ts +4 -1
  6. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  7. package/dist/app/agentkit/react/components.js +69 -15
  8. package/dist/app/agentkit/react/components.js.map +1 -1
  9. package/dist/app/agentkit/react/context.d.ts +7 -1
  10. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  11. package/dist/app/agentkit/react/context.js +5 -1
  12. package/dist/app/agentkit/react/context.js.map +1 -1
  13. package/dist/app/agentkit/react/index.d.ts +1 -0
  14. package/dist/app/agentkit/react/index.d.ts.map +1 -1
  15. package/dist/app/agentkit/react/index.js +1 -0
  16. package/dist/app/agentkit/react/index.js.map +1 -1
  17. package/dist/app/agentkit/react/root.d.ts +4 -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 +43 -6
  22. package/dist/app/agentkit/react/transcript-layout.d.ts +51 -0
  23. package/dist/app/agentkit/react/transcript-layout.d.ts.map +1 -0
  24. package/dist/app/agentkit/react/transcript-layout.js +34 -0
  25. package/dist/app/agentkit/react/transcript-layout.js.map +1 -0
  26. package/dist/app/auth/AuthPage.d.ts.map +1 -1
  27. package/dist/app/auth/AuthPage.js +33 -44
  28. package/dist/app/auth/AuthPage.js.map +1 -1
  29. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  30. package/dist/app/chat/AgentKitAssistantChat.js +3 -3
  31. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  32. package/dist/app/chat/AgentPanel.d.ts +2 -0
  33. package/dist/app/chat/AgentPanel.d.ts.map +1 -1
  34. package/dist/app/chat/AgentPanel.js +6 -2
  35. package/dist/app/chat/AgentPanel.js.map +1 -1
  36. package/dist/app/chat/chat/index.d.ts +2 -0
  37. package/dist/app/chat/chat/index.d.ts.map +1 -1
  38. package/dist/app/chat/chat/index.js.map +1 -1
  39. package/dist/app/chat/chat/surface-types.d.ts +7 -0
  40. package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
  41. package/dist/app/chat/chat/surface-types.js.map +1 -1
  42. package/dist/app/chat/chat/tool-call-display.d.ts +7 -2
  43. package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
  44. package/dist/app/chat/chat/tool-call-display.js +10 -9
  45. package/dist/app/chat/chat/tool-call-display.js.map +1 -1
  46. package/dist/app/chat/chat/widgets/DataChartRenderer.d.ts.map +1 -1
  47. package/dist/app/chat/chat/widgets/DataChartRenderer.js +196 -26
  48. package/dist/app/chat/chat/widgets/DataChartRenderer.js.map +1 -1
  49. package/dist/app/chat/chat/widgets/DataChartWidget.d.ts.map +1 -1
  50. package/dist/app/chat/chat/widgets/DataChartWidget.js +5 -1
  51. package/dist/app/chat/chat/widgets/DataChartWidget.js.map +1 -1
  52. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts +2 -2
  53. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts.map +1 -1
  54. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js +6 -4
  55. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js.map +1 -1
  56. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  57. package/dist/app/i18n/catalogs/ar-SA.js +1 -0
  58. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  59. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  60. package/dist/app/i18n/catalogs/de-DE.js +1 -0
  61. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  62. package/dist/app/i18n/catalogs/en-US.d.ts +1 -0
  63. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  64. package/dist/app/i18n/catalogs/en-US.js +1 -0
  65. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  66. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  67. package/dist/app/i18n/catalogs/es-ES.js +1 -0
  68. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  69. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  70. package/dist/app/i18n/catalogs/fr-FR.js +1 -0
  71. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  72. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  73. package/dist/app/i18n/catalogs/hi-IN.js +1 -0
  74. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  75. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  76. package/dist/app/i18n/catalogs/ja-JP.js +1 -0
  77. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  78. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  79. package/dist/app/i18n/catalogs/ko-KR.js +1 -0
  80. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  81. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  82. package/dist/app/i18n/catalogs/pt-BR.js +1 -0
  83. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  84. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  85. package/dist/app/i18n/catalogs/zh-CN.js +1 -0
  86. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  87. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  88. package/dist/app/i18n/catalogs/zh-TW.js +1 -0
  89. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  90. package/dist/app/onboarding/GettingStartedCard.d.ts +35 -0
  91. package/dist/app/onboarding/GettingStartedCard.d.ts.map +1 -0
  92. package/dist/app/onboarding/GettingStartedCard.js +38 -0
  93. package/dist/app/onboarding/GettingStartedCard.js.map +1 -0
  94. package/dist/app/onboarding/index.d.ts +1 -0
  95. package/dist/app/onboarding/index.d.ts.map +1 -1
  96. package/dist/app/onboarding/index.js +1 -0
  97. package/dist/app/onboarding/index.js.map +1 -1
  98. package/dist/app/providers/AppProviders.d.ts.map +1 -1
  99. package/dist/app/providers/AppProviders.js +3 -1
  100. package/dist/app/providers/AppProviders.js.map +1 -1
  101. package/dist/app/settings/AccountSettingsCard.d.ts.map +1 -1
  102. package/dist/app/settings/AccountSettingsCard.js +2 -1
  103. package/dist/app/settings/AccountSettingsCard.js.map +1 -1
  104. package/dist/app/settings/index.d.ts +1 -0
  105. package/dist/app/settings/index.d.ts.map +1 -1
  106. package/dist/app/settings/index.js +1 -0
  107. package/dist/app/settings/index.js.map +1 -1
  108. package/dist/app/settings/shell/SettingsShell.d.ts +40 -1
  109. package/dist/app/settings/shell/SettingsShell.d.ts.map +1 -1
  110. package/dist/app/settings/shell/SettingsShell.js +87 -20
  111. package/dist/app/settings/shell/SettingsShell.js.map +1 -1
  112. package/dist/app/settings/shell/bridge.d.ts +5 -3
  113. package/dist/app/settings/shell/bridge.d.ts.map +1 -1
  114. package/dist/app/settings/shell/bridge.js +8 -5
  115. package/dist/app/settings/shell/bridge.js.map +1 -1
  116. package/dist/app/settings/shell/index.d.ts +2 -2
  117. package/dist/app/settings/shell/index.d.ts.map +1 -1
  118. package/dist/app/settings/shell/index.js.map +1 -1
  119. package/dist/app/settings/shell/registry.d.ts +9 -1
  120. package/dist/app/settings/shell/registry.d.ts.map +1 -1
  121. package/dist/app/settings/shell/registry.js +16 -0
  122. package/dist/app/settings/shell/registry.js.map +1 -1
  123. package/dist/app/settings/shell/routing.d.ts +5 -4
  124. package/dist/app/settings/shell/routing.d.ts.map +1 -1
  125. package/dist/app/settings/shell/routing.js +38 -10
  126. package/dist/app/settings/shell/routing.js.map +1 -1
  127. package/dist/app/shared/CommandMenu.d.ts +86 -6
  128. package/dist/app/shared/CommandMenu.d.ts.map +1 -1
  129. package/dist/app/shared/CommandMenu.js +303 -41
  130. package/dist/app/shared/CommandMenu.js.map +1 -1
  131. package/dist/app/shared/ConsentBanner.d.ts +9 -0
  132. package/dist/app/shared/ConsentBanner.d.ts.map +1 -0
  133. package/dist/app/shared/ConsentBanner.js +35 -0
  134. package/dist/app/shared/ConsentBanner.js.map +1 -0
  135. package/dist/composer/ComposerContextBanner.d.ts +3 -1
  136. package/dist/composer/ComposerContextBanner.d.ts.map +1 -1
  137. package/dist/composer/ComposerContextBanner.js +2 -2
  138. package/dist/composer/ComposerContextBanner.js.map +1 -1
  139. package/dist/ui/sonner.d.ts +2 -2
  140. package/dist/ui/sonner.d.ts.map +1 -1
  141. package/dist/ui/sonner.js +2 -2
  142. package/dist/ui/sonner.js.map +1 -1
  143. package/package.json +3 -3
  144. package/src/app/agentkit/react/chat.tsx +2 -0
  145. package/src/app/agentkit/react/components.interaction.spec.tsx +107 -0
  146. package/src/app/agentkit/react/components.spec.tsx +28 -1
  147. package/src/app/agentkit/react/components.tsx +153 -17
  148. package/src/app/agentkit/react/context.tsx +12 -0
  149. package/src/app/agentkit/react/index.ts +1 -0
  150. package/src/app/agentkit/react/lifecycle.spec.tsx +186 -16
  151. package/src/app/agentkit/react/root.tsx +7 -0
  152. package/src/app/agentkit/react/styles.css +43 -6
  153. package/src/app/agentkit/react/transcript-layout.spec.ts +72 -0
  154. package/src/app/agentkit/react/transcript-layout.ts +97 -0
  155. package/src/app/auth/AuthPage.attribution.spec.tsx +6 -0
  156. package/src/app/auth/AuthPage.interaction.spec.tsx +74 -0
  157. package/src/app/auth/AuthPage.tsx +32 -39
  158. package/src/app/chat/AgentKitAssistantChat.spec.tsx +39 -0
  159. package/src/app/chat/AgentKitAssistantChat.tsx +5 -0
  160. package/src/app/chat/AgentPanel.page-header-share.spec.tsx +101 -0
  161. package/src/app/chat/AgentPanel.tsx +14 -2
  162. package/src/app/chat/chat/index.ts +2 -0
  163. package/src/app/chat/chat/surface-types.ts +10 -0
  164. package/src/app/chat/chat/tool-call-display.spec.tsx +83 -0
  165. package/src/app/chat/chat/tool-call-display.tsx +40 -8
  166. package/src/app/chat/chat/widgets/DataChartRenderer.spec.tsx +632 -0
  167. package/src/app/chat/chat/widgets/DataChartRenderer.tsx +308 -51
  168. package/src/app/chat/chat/widgets/DataChartWidget.spec.tsx +126 -23
  169. package/src/app/chat/chat/widgets/DataChartWidget.tsx +10 -0
  170. package/src/app/chat/chat/widgets/DataWidgetDisplayControls.tsx +26 -6
  171. package/src/app/i18n/catalogs/ar-SA.ts +1 -0
  172. package/src/app/i18n/catalogs/de-DE.ts +2 -0
  173. package/src/app/i18n/catalogs/en-US.ts +2 -0
  174. package/src/app/i18n/catalogs/es-ES.ts +2 -0
  175. package/src/app/i18n/catalogs/fr-FR.ts +2 -0
  176. package/src/app/i18n/catalogs/hi-IN.ts +1 -0
  177. package/src/app/i18n/catalogs/ja-JP.ts +2 -0
  178. package/src/app/i18n/catalogs/ko-KR.ts +1 -0
  179. package/src/app/i18n/catalogs/pt-BR.ts +2 -0
  180. package/src/app/i18n/catalogs/zh-CN.ts +1 -0
  181. package/src/app/i18n/catalogs/zh-TW.ts +1 -0
  182. package/src/app/onboarding/GettingStartedCard.tsx +203 -0
  183. package/src/app/onboarding/index.ts +1 -0
  184. package/src/app/providers/AppProviders.tsx +7 -1
  185. package/src/app/settings/AccountSettingsCard.spec.tsx +31 -0
  186. package/src/app/settings/AccountSettingsCard.tsx +8 -0
  187. package/src/app/settings/index.ts +1 -0
  188. package/src/app/settings/shell/SettingsShell.spec.tsx +472 -1
  189. package/src/app/settings/shell/SettingsShell.tsx +245 -31
  190. package/src/app/settings/shell/bridge.spec.ts +12 -0
  191. package/src/app/settings/shell/bridge.ts +17 -4
  192. package/src/app/settings/shell/index.ts +6 -1
  193. package/src/app/settings/shell/registry.ts +33 -1
  194. package/src/app/settings/shell/routing.spec.ts +92 -1
  195. package/src/app/settings/shell/routing.ts +52 -9
  196. package/src/app/shared/CommandMenu.spec.tsx +1662 -1
  197. package/src/app/shared/CommandMenu.tsx +763 -164
  198. package/src/app/shared/ConsentBanner.spec.tsx +141 -0
  199. package/src/app/shared/ConsentBanner.tsx +141 -0
  200. package/src/composer/ComposerContextBanner.spec.tsx +31 -5
  201. package/src/composer/ComposerContextBanner.tsx +7 -2
  202. package/src/ui/sonner.spec.ts +10 -0
  203. package/src/ui/sonner.tsx +2 -2
@@ -247,6 +247,80 @@ describe("AuthPage interactions", () => {
247
247
  }
248
248
  });
249
249
 
250
+ it("stores no visitor id on the sign-in page before analytics consent", async () => {
251
+ const originalHref = window.location.href;
252
+ const sendBeacon = vi.fn(() => true);
253
+ Object.defineProperty(navigator, "sendBeacon", {
254
+ value: sendBeacon,
255
+ configurable: true,
256
+ });
257
+ window.happyDOM.setURL("https://clips.agent-native.com/signup");
258
+ // Earlier renders in this file already settled the region.
259
+ Reflect.deleteProperty(
260
+ globalThis,
261
+ Symbol.for("agent-native.client.consent"),
262
+ );
263
+ window.localStorage.removeItem("agent-native.anonymous_id");
264
+ document.cookie = "an_aid=; path=/; max-age=0";
265
+ window.sessionStorage.setItem("an_consent_region", "consent");
266
+ (
267
+ window as Window & { __AGENT_NATIVE_CONFIG__?: Record<string, string> }
268
+ ).__AGENT_NATIVE_CONFIG__ = {
269
+ agentNativeAnalyticsPublicKey: "anpk_test",
270
+ agentNativeAnalyticsEndpoint: "https://analytics.agent-native.com/track",
271
+ };
272
+
273
+ try {
274
+ await act(async () => {
275
+ root.render(
276
+ <AuthPage
277
+ {...propsFromHtml(
278
+ getOnboardingHtml({
279
+ requestHost: "clips.agent-native.com",
280
+ requestPath: "/signup",
281
+ }),
282
+ )}
283
+ initialView="signup"
284
+ />,
285
+ );
286
+ await Promise.resolve();
287
+ });
288
+
289
+ const email = container.querySelector<HTMLInputElement>("#s-email");
290
+ await act(async () => {
291
+ Object.getOwnPropertyDescriptor(
292
+ HTMLInputElement.prototype,
293
+ "value",
294
+ )!.set!.call(email, "person@example.com");
295
+ email!.dispatchEvent(new Event("input", { bubbles: true }));
296
+ });
297
+
298
+ expect(sendBeacon).toHaveBeenCalled();
299
+ for (const [, body] of sendBeacon.mock.calls as unknown as [
300
+ string,
301
+ string,
302
+ ][]) {
303
+ const sent = JSON.parse(body);
304
+ expect(sent.anonymousId).toBeUndefined();
305
+ expect(sent.properties.analytics_consent).toBe("withheld");
306
+ }
307
+ expect(window.localStorage.getItem("agent-native.anonymous_id")).toBe(
308
+ null,
309
+ );
310
+ expect(document.cookie).not.toMatch(/an_aid=[^;]/);
311
+ } finally {
312
+ delete (window as Window & { __AGENT_NATIVE_CONFIG__?: unknown })
313
+ .__AGENT_NATIVE_CONFIG__;
314
+ window.sessionStorage.removeItem("an_consent_region");
315
+ Reflect.deleteProperty(
316
+ globalThis,
317
+ Symbol.for("agent-native.client.consent"),
318
+ );
319
+ Reflect.deleteProperty(navigator, "sendBeacon");
320
+ window.happyDOM.setURL(originalHref);
321
+ }
322
+ });
323
+
250
324
  it("sends a beta app's sign-up events to beta Analytics", async () => {
251
325
  const originalHref = window.location.href;
252
326
  const sendBeacon = vi.fn(() => true);
@@ -2,6 +2,7 @@
2
2
 
3
3
  import {
4
4
  captureAttribution,
5
+ getAnalyticsAnonymousId,
5
6
  getAnalyticsSessionId,
6
7
  } from "@agent-native/core/client/analytics";
7
8
  import { frameworkRoutePrefix } from "@agent-native/core/client/api-path";
@@ -12,6 +13,7 @@ import {
12
13
  normalizeOAuthReturnPath,
13
14
  shouldRetryAuthSessionProbe,
14
15
  } from "@agent-native/core/client/auth/auth-page-helpers";
16
+ import { whenConsentDecided } from "@agent-native/core/client/consent";
15
17
  import { openOAuthPopup } from "@agent-native/core/client/oauth-popup";
16
18
  import { navigateForSession } from "@agent-native/core/client/use-session";
17
19
  import { normalizeLocaleCode } from "@agent-native/core/localization/shared";
@@ -65,7 +67,6 @@ type AuthRequestResult = {
65
67
  const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
66
68
  const TAB_STORAGE_KEY = "an.onboarding.tab";
67
69
  const PENDING_SIGNUP_EMAIL_STORAGE_KEY = "an.onboarding.pendingSignupEmail";
68
- const ANALYTICS_ANONYMOUS_ID_KEY = "agent-native.anonymous_id";
69
70
  const GOOGLE_AUTH_URL_PATH = "/_agent-native/google/auth-url";
70
71
  const BUILDER_DESKTOP_RETURN_ORIGIN = "http://127.0.0.1:8080";
71
72
  const useIsomorphicLayoutEffect =
@@ -261,15 +262,6 @@ async function requestJson(
261
262
  return { response, data, readable };
262
263
  }
263
264
 
264
- function generateAnonymousId(): string {
265
- try {
266
- if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
267
- } catch {
268
- // coercion-ok: the browser fallback remains usable when crypto is unavailable.
269
- }
270
- return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
271
- }
272
-
273
265
  function trackAuth(
274
266
  app: string,
275
267
  name: string,
@@ -298,9 +290,8 @@ function trackAuth(
298
290
  }
299
291
  ).__AGENT_NATIVE_CONFIG__;
300
292
  if (!config?.agentNativeAnalyticsPublicKey) return;
301
- const anonymousId = readStorage(ANALYTICS_ANONYMOUS_ID_KEY);
302
- if (!anonymousId) return;
303
293
  const sessionId = getAnalyticsSessionId();
294
+ const timestamp = new Date().toISOString();
304
295
  const endpoint = resolveLaneEndpoint(
305
296
  config.agentNativeAnalyticsEndpoint ??
306
297
  "https://analytics.agent-native.com/track",
@@ -313,23 +304,33 @@ function trackAuth(
313
304
  }> = [{ name, properties: legacyProperties }];
314
305
  const canonical = canonicalTrackingEvent(name, legacyProperties);
315
306
  if (canonical) events.push(canonical);
316
- for (const event of events) {
317
- const body = JSON.stringify({
318
- publicKey: config.agentNativeAnalyticsPublicKey,
319
- event: event.name,
320
- properties: event.properties,
321
- anonymousId,
322
- sessionId,
323
- timestamp: new Date().toISOString(),
324
- });
325
- if (navigator.sendBeacon?.(endpoint, body)) continue;
326
- void fetch(endpoint, {
327
- method: "POST",
328
- body,
329
- keepalive: true,
330
- headers: { "Content-Type": "text/plain;charset=UTF-8" },
331
- }).catch(() => undefined);
332
- }
307
+ const publicKey = config.agentNativeAnalyticsPublicKey;
308
+ // The visitor id waits for analytics consent, as in the app bundle.
309
+ whenConsentDecided((decided) => {
310
+ const anonymousId = decided ? getAnalyticsAnonymousId() : undefined;
311
+ for (const event of events) {
312
+ const body = JSON.stringify({
313
+ publicKey,
314
+ event: event.name,
315
+ properties: anonymousId
316
+ ? event.properties
317
+ : {
318
+ ...event.properties,
319
+ analytics_consent: decided ? "withheld" : "unknown",
320
+ },
321
+ anonymousId,
322
+ sessionId,
323
+ timestamp,
324
+ });
325
+ if (navigator.sendBeacon?.(endpoint, body)) continue;
326
+ void fetch(endpoint, {
327
+ method: "POST",
328
+ body,
329
+ keepalive: true,
330
+ headers: { "Content-Type": "text/plain;charset=UTF-8" },
331
+ }).catch(() => undefined);
332
+ }
333
+ });
333
334
  } catch {
334
335
  // coercion-ok: analytics is best effort and cannot block authentication.
335
336
  }
@@ -1036,16 +1037,8 @@ export function AuthPage(props: AuthPageProps) {
1036
1037
  }, [apiPath, redirectToSignedInApp, runtimeBasePathResolved, view]);
1037
1038
 
1038
1039
  React.useEffect(() => {
1039
- let anonymousId = readStorage(ANALYTICS_ANONYMOUS_ID_KEY);
1040
- if (!/^[A-Za-z0-9_-]{1,128}$/.test(anonymousId)) {
1041
- anonymousId = generateAnonymousId();
1042
- writeStorage(ANALYTICS_ANONYMOUS_ID_KEY, anonymousId);
1043
- }
1044
- try {
1045
- document.cookie = `an_aid=${encodeURIComponent(anonymousId)}; path=/; max-age=2592000; SameSite=Lax`;
1046
- } catch {
1047
- // coercion-ok: attribution cookies are optional for authentication.
1048
- }
1040
+ // Stores `an_aid` only with analytics consent, and asks for the region.
1041
+ getAnalyticsAnonymousId();
1049
1042
  // Capture is once per page load, so it must wait for a workspace's
1050
1043
  // inferred base path or `landing_path` falls back to the wrong home.
1051
1044
  if (runtimeBasePathResolved) {
@@ -1054,6 +1054,27 @@ describe("AgentKitAssistantChat host behavior", () => {
1054
1054
  expect(chatMocks.composerProps.isReferenceTarget).toBe(true);
1055
1055
  });
1056
1056
 
1057
+ it("passes the worked-for row layout into the shared AgentKit root", async () => {
1058
+ const layout = {
1059
+ chevronSize: 12,
1060
+ iconGap: 8,
1061
+ fontSize: 13,
1062
+ lineHeight: 16,
1063
+ paddingBlock: 2,
1064
+ paddingInline: 4,
1065
+ };
1066
+ const formatWorkedForDuration = (durationMs: number) =>
1067
+ `${Math.round(durationMs / 1000)} seconds`;
1068
+ await mount(
1069
+ baseProps({ workedForRowLayout: layout, formatWorkedForDuration }),
1070
+ );
1071
+
1072
+ expect(chatMocks.rootProps.workedForRowLayout).toEqual(layout);
1073
+ expect(chatMocks.rootProps.formatWorkedForDuration).toBe(
1074
+ formatWorkedForDuration,
1075
+ );
1076
+ });
1077
+
1057
1078
  it("reports whether a composer can hold a prefill with its current context", async () => {
1058
1079
  const ref = createRef<AssistantChatHandle>();
1059
1080
  await mount(baseProps(), ref);
@@ -3271,6 +3292,24 @@ describe("AgentKitAssistantChat host behavior", () => {
3271
3292
  ).not.toBeNull();
3272
3293
  });
3273
3294
 
3295
+ it("forwards transcript layout options to the shared chat component", async () => {
3296
+ const transcriptLayout = {
3297
+ topInset: 5,
3298
+ messageLaneWidth: 730,
3299
+ messageLaneOffsetInline: -33,
3300
+ messageGap: 12,
3301
+ messageLineHeight: 20,
3302
+ fontFamily: "IBM Plex Sans",
3303
+ userBubbleLineHeight: 18,
3304
+ userBubbleMaxWidth: 600,
3305
+ userBubbleOffsetInline: 10,
3306
+ userBubblePadding: { block: 8, inline: 13 },
3307
+ };
3308
+ await mount(baseProps({ transcriptLayout }));
3309
+
3310
+ expect(chatMocks.chatProps.transcriptLayout).toBe(transcriptLayout);
3311
+ });
3312
+
3274
3313
  it("anchors the composer at the bottom while restoring a persisted thread", async () => {
3275
3314
  await mount(
3276
3315
  baseProps({
@@ -2107,6 +2107,8 @@ export const AgentKitAssistantChat = forwardRef<
2107
2107
  branchNavigation={props.branchNavigation}
2108
2108
  loadRunUsage={loadRunUsage}
2109
2109
  buildFeedbackReport={formatFeedbackReport}
2110
+ workedForRowLayout={props.workedForRowLayout}
2111
+ formatWorkedForDuration={props.formatWorkedForDuration}
2110
2112
  onThreadForked={(thread) => props.onForkedThread?.(thread.id)}
2111
2113
  onCopyMessage={({ text }) => {
2112
2114
  const html = renderMarkdownToClipboardHtml(text);
@@ -5054,6 +5056,7 @@ const AgentKitAssistantChatBody = forwardRef<
5054
5056
  className={props.className}
5055
5057
  composerProps={{ attachmentsEnabled: fileStorageConfigured }}
5056
5058
  hasRenderedMessages={hasRenderedMessages}
5059
+ transcriptLayout={props.transcriptLayout}
5057
5060
  emptyComposerPlacement={
5058
5061
  props.centerComposerWhenEmpty && !isRestoring ? "center" : "bottom"
5059
5062
  }
@@ -6470,6 +6473,8 @@ function AgentKitTool({ value, active }: AgentKitRenderProps<AgentToolCall>) {
6470
6473
  chatUI={asRecord(metadata.chatUI) as never}
6471
6474
  activity={metadata.activity === true}
6472
6475
  isActiveTail={active === true}
6476
+ workedForRowLayout={surface.props.workedForRowLayout}
6477
+ formatWorkedForDuration={surface.props.formatWorkedForDuration}
6473
6478
  />
6474
6479
  </SuppressInlineOpenAppContext.Provider>
6475
6480
  </ChatRunningContext.Provider>
@@ -0,0 +1,101 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import React, { act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { MemoryRouter } from "react-router";
6
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
+
8
+ import { AgentPanel } from "./AgentPanel.js";
9
+
10
+ type MockHeaderProps = {
11
+ renderOverlay?: (props: never) => React.ReactNode;
12
+ };
13
+
14
+ vi.mock("./MultiTabAssistantChat.js", () => ({
15
+ MultiTabAssistantChat: ({ renderOverlay }: MockHeaderProps) =>
16
+ renderOverlay?.({
17
+ tabs: [{ id: "thread-1", label: "Current thread", status: "completed" }],
18
+ activeTabId: "thread-1",
19
+ activeTabMessageCount: 1,
20
+ activeTabIsPersisted: true,
21
+ setActiveTabId: vi.fn(),
22
+ addTab: vi.fn(),
23
+ closeTab: vi.fn(),
24
+ closeOtherTabs: vi.fn(),
25
+ closeAllTabs: vi.fn(),
26
+ clearActiveTab: vi.fn(),
27
+ showHistory: false,
28
+ toggleHistory: vi.fn(),
29
+ tabCount: 1,
30
+ } as never) ?? null,
31
+ }));
32
+
33
+ vi.mock("../sharing/index.js", () => ({
34
+ ShareButton: ({
35
+ triggerContent,
36
+ triggerClassName,
37
+ }: {
38
+ triggerContent?: React.ReactNode;
39
+ triggerClassName?: string;
40
+ }) => (
41
+ <button data-testid="share-trigger" data-trigger-class={triggerClassName}>
42
+ {triggerContent ?? "Share"}
43
+ </button>
44
+ ),
45
+ }));
46
+
47
+ describe("AgentPanel page header share trigger", () => {
48
+ let root: Root | null = null;
49
+ let container: HTMLDivElement | null = null;
50
+
51
+ afterEach(async () => {
52
+ if (root) await act(async () => root?.unmount());
53
+ container?.remove();
54
+ root = null;
55
+ container = null;
56
+ localStorage.clear();
57
+ });
58
+
59
+ it("renders the default icon and host-provided content through ShareButton", async () => {
60
+ container = document.createElement("div");
61
+ document.body.append(container);
62
+ root = createRoot(container);
63
+
64
+ const renderPanel = async (triggerContent?: React.ReactNode) =>
65
+ act(async () => {
66
+ root!.render(
67
+ <MemoryRouter>
68
+ <AgentPanel
69
+ storageKey="page-header-share-test"
70
+ showHeader={false}
71
+ showPageHeader
72
+ pageHeaderShareTriggerContent={triggerContent}
73
+ />
74
+ </MemoryRouter>,
75
+ );
76
+ });
77
+
78
+ await renderPanel();
79
+ const defaultButton = container.querySelector<HTMLButtonElement>(
80
+ '[data-testid="share-trigger"]',
81
+ );
82
+ expect(defaultButton?.querySelector("svg")).not.toBeNull();
83
+ expect(defaultButton?.dataset.triggerClass).toContain("size-8");
84
+
85
+ await renderPanel(
86
+ <span className="flex items-center gap-1.5">
87
+ <svg data-testid="custom-share-icon" aria-hidden="true" />
88
+ <span>Share</span>
89
+ </span>,
90
+ );
91
+ const customButton = container.querySelector<HTMLButtonElement>(
92
+ '[data-testid="share-trigger"]',
93
+ );
94
+ expect(customButton?.textContent).toBe("Share");
95
+ expect(
96
+ customButton?.querySelector('[data-testid="custom-share-icon"]'),
97
+ ).not.toBeNull();
98
+ expect(customButton?.dataset.triggerClass).toContain("w-auto");
99
+ expect(customButton?.dataset.triggerClass).toContain("px-2");
100
+ });
101
+ });
@@ -610,6 +610,8 @@ export interface AgentPanelProps extends Omit<
610
610
  /** Hide the default page title menu while keeping the shared page actions. */
611
611
  showPageTitleMenu?: boolean;
612
612
  pageHeaderLeadingSlot?: React.ReactNode;
613
+ /** Override the shared page header share button's icon and label contents. */
614
+ pageHeaderShareTriggerContent?: React.ReactNode;
613
615
  /** Render host page actions alongside Share, with the active chat controls. */
614
616
  pageToolbarSlot?:
615
617
  | React.ReactNode
@@ -809,6 +811,7 @@ function AgentPanelInner({
809
811
  showPageHeaderWhenEmpty = false,
810
812
  showPageTitleMenu = true,
811
813
  pageHeaderLeadingSlot,
814
+ pageHeaderShareTriggerContent,
812
815
  pageToolbarSlot,
813
816
  onPageHeaderVisibilityChange,
814
817
  chatOnly = false,
@@ -1836,9 +1839,17 @@ function AgentPanelInner({
1836
1839
  resourceTitle={activeTab.label || t("agentPanel.chat")}
1837
1840
  shareUrl={getChatThreadShareUrl(activeTab.id)}
1838
1841
  triggerContent={
1839
- <IconShare3 size={15} aria-hidden="true" />
1842
+ pageHeaderShareTriggerContent ?? (
1843
+ <IconShare3 size={15} aria-hidden="true" />
1844
+ )
1840
1845
  }
1841
- triggerClassName="size-8 p-0 border-0 bg-transparent shadow-none hover:bg-accent/60"
1846
+ triggerClassName={cn(
1847
+ "h-8 border-0 bg-transparent shadow-none hover:bg-accent/60",
1848
+ pageHeaderShareTriggerContent === undefined ||
1849
+ pageHeaderShareTriggerContent === null
1850
+ ? "size-8 p-0"
1851
+ : "w-auto px-2",
1852
+ )}
1842
1853
  />
1843
1854
  ) : null}
1844
1855
  {toolbarSlot}
@@ -1869,6 +1880,7 @@ function AgentPanelInner({
1869
1880
  headerMenuOpen,
1870
1881
  onPageHeaderVisibilityChange,
1871
1882
  pageHeaderLeadingSlot,
1883
+ pageHeaderShareTriggerContent,
1872
1884
  pageToolbarSlot,
1873
1885
  pendingHeaderOverlayRef,
1874
1886
  setHeaderMenuOpen,
@@ -53,6 +53,8 @@ export type {
53
53
  AssistantChatSubmitResult,
54
54
  AgentChatSurfaceKind,
55
55
  } from "./surface-types.js";
56
+ export type { AgentChatTranscriptLayout } from "../../agentkit/react/transcript-layout.js";
57
+ export type { AgentChatWorkedForRowLayout } from "../../agentkit/react/transcript-layout.js";
56
58
  export type {
57
59
  MultiTabAssistantChatProps,
58
60
  MultiTabAssistantChatHeaderProps,
@@ -28,6 +28,10 @@ import type * as React from "react";
28
28
 
29
29
  import type { AgentKitWorkedForSummaryRenderProps } from "../../agentkit/react/context.js";
30
30
  import type { AgentMessageContentOrderResolver } from "../../agentkit/react/message-content-order.js";
31
+ import type {
32
+ AgentChatTranscriptLayout,
33
+ AgentChatWorkedForRowLayout,
34
+ } from "../../agentkit/react/transcript-layout.js";
31
35
  import type { AssistantChatHistoryConfig } from "./history-types.js";
32
36
  import type { BuilderSetupCardLayout } from "./run-recovery.js";
33
37
 
@@ -238,6 +242,12 @@ export interface AssistantChatProps {
238
242
  /** Optional content rendered in the empty state, below the suggestion
239
243
  * buttons. Unlike `threadFooterSlot` this never survives the first message. */
240
244
  emptyStateFooter?: React.ReactNode;
245
+ /** Optional transcript geometry. Omitted values keep the shared defaults. */
246
+ transcriptLayout?: AgentChatTranscriptLayout;
247
+ /** Optional elapsed-work row geometry. Omitted values keep the shared defaults. */
248
+ workedForRowLayout?: AgentChatWorkedForRowLayout;
249
+ /** Optional duration text for the shared completed-work row. */
250
+ formatWorkedForDuration?: (durationMs: number) => string;
241
251
  /** Whether to show the header bar. Default: true */
242
252
  showHeader?: boolean;
243
253
  /** CSS class for the outer container */
@@ -2698,9 +2698,17 @@ describe("WorkedForSummary", () => {
2698
2698
 
2699
2699
  beforeEach(() => {
2700
2700
  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
2701
+ builderHandoffMocks.useAgentChatContext.mockReturnValue({ items: [] });
2701
2702
  container = document.createElement("div");
2702
2703
  document.body.appendChild(container);
2703
2704
  root = createRoot(container);
2705
+ const render = root.render.bind(root);
2706
+ root.render = (children: ReactNode) =>
2707
+ render(
2708
+ <AgentNativeI18nProvider persistPreference={false}>
2709
+ {children}
2710
+ </AgentNativeI18nProvider>,
2711
+ );
2704
2712
  });
2705
2713
 
2706
2714
  afterEach(() => {
@@ -2740,6 +2748,17 @@ describe("WorkedForSummary", () => {
2740
2748
  response: ["The query returned a 12% increase."],
2741
2749
  },
2742
2750
  }}
2751
+ workedForRowLayout={{
2752
+ chevronSize: 16,
2753
+ iconGap: 4,
2754
+ fontSize: 14,
2755
+ lineHeight: 20,
2756
+ paddingBlock: 12,
2757
+ paddingInline: 12,
2758
+ }}
2759
+ formatWorkedForDuration={(durationMs) =>
2760
+ `${durationMs / 1000} seconds`
2761
+ }
2743
2762
  />,
2744
2763
  );
2745
2764
  });
@@ -2748,6 +2767,8 @@ describe("WorkedForSummary", () => {
2748
2767
  container.querySelectorAll<HTMLButtonElement>("button[aria-expanded]"),
2749
2768
  ).find((button) => button.textContent?.includes("Worked for"));
2750
2769
  expect(workedFor).not.toBeNull();
2770
+ expect(workedFor?.style.fontSize).toBe("14px");
2771
+ expect(workedFor?.textContent).toContain("Worked for 5 seconds");
2751
2772
  act(() => workedFor?.click());
2752
2773
 
2753
2774
  expect(container.textContent).not.toContain("This private thought");
@@ -2795,6 +2816,68 @@ describe("WorkedForSummary", () => {
2795
2816
  expect(container.textContent).toContain("Worked for 5m");
2796
2817
  });
2797
2818
 
2819
+ it("keeps the disclosure chevron before the duration label", () => {
2820
+ act(() => {
2821
+ root.render(
2822
+ <WorkedForSummary durationMs={5_000}>
2823
+ <div>Details</div>
2824
+ </WorkedForSummary>,
2825
+ );
2826
+ });
2827
+
2828
+ const button = container.querySelector("button[aria-expanded]");
2829
+ expect(button?.children[0]?.tagName.toLowerCase()).toBe("svg");
2830
+ expect(button?.children[1]?.textContent).toBe("Worked for 5s");
2831
+ const buttonClasses = button?.className.split(/\s+/) ?? [];
2832
+ expect(buttonClasses).toContain("gap-1");
2833
+ expect(buttonClasses).toContain("p-3");
2834
+ expect(buttonClasses).toContain("text-start");
2835
+ expect(buttonClasses).not.toContain("text-left");
2836
+ const chevron = button?.children[0];
2837
+ expect(chevron?.getAttribute("class")?.split(/\s+/)).toContain(
2838
+ "rtl:-scale-x-100",
2839
+ );
2840
+
2841
+ act(() => button?.click());
2842
+
2843
+ expect(chevron?.getAttribute("class")?.split(/\s+/)).toContain(
2844
+ "rtl:-rotate-90",
2845
+ );
2846
+ });
2847
+
2848
+ it("applies optional geometry to the completed-work disclosure row", () => {
2849
+ act(() => {
2850
+ root.render(
2851
+ <WorkedForSummary
2852
+ durationMs={5_000}
2853
+ layout={{
2854
+ chevronSize: 16,
2855
+ iconGap: 4,
2856
+ fontSize: 14,
2857
+ lineHeight: 20,
2858
+ paddingBlock: 12,
2859
+ paddingInline: 12,
2860
+ }}
2861
+ >
2862
+ <div>Details</div>
2863
+ </WorkedForSummary>,
2864
+ );
2865
+ });
2866
+
2867
+ const button = container.querySelector<HTMLButtonElement>(
2868
+ "button[aria-expanded]",
2869
+ );
2870
+ const icon = button?.querySelector<SVGElement>("svg");
2871
+ expect(button?.style.gap).toBe("4px");
2872
+ expect(button?.style.fontSize).toBe("14px");
2873
+ expect(button?.style.lineHeight).toBe("20px");
2874
+ expect(button?.style.paddingBlock).toBe("12px");
2875
+ expect(button?.style.paddingInline).toBe("12px");
2876
+ expect(button?.style.fontFamily).toBe("inherit");
2877
+ expect(icon?.style.width).toBe("16px");
2878
+ expect(icon?.style.height).toBe("16px");
2879
+ });
2880
+
2798
2881
  it("applies the running shimmer while the work summary is live", () => {
2799
2882
  act(() => {
2800
2883
  root.render(