@agent-native/toolkit 0.214.0 → 0.216.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 (128) hide show
  1. package/dist/app/agentkit/react/chat.d.ts.map +1 -1
  2. package/dist/app/agentkit/react/chat.js +2 -2
  3. package/dist/app/agentkit/react/chat.js.map +1 -1
  4. package/dist/app/agentkit/react/components.d.ts +4 -1
  5. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  6. package/dist/app/agentkit/react/components.js +69 -15
  7. package/dist/app/agentkit/react/components.js.map +1 -1
  8. package/dist/app/agentkit/react/context.d.ts +7 -1
  9. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  10. package/dist/app/agentkit/react/context.js +5 -1
  11. package/dist/app/agentkit/react/context.js.map +1 -1
  12. package/dist/app/agentkit/react/index.d.ts +1 -0
  13. package/dist/app/agentkit/react/index.d.ts.map +1 -1
  14. package/dist/app/agentkit/react/index.js +1 -0
  15. package/dist/app/agentkit/react/index.js.map +1 -1
  16. package/dist/app/agentkit/react/root.d.ts +4 -1
  17. package/dist/app/agentkit/react/root.d.ts.map +1 -1
  18. package/dist/app/agentkit/react/root.js +2 -2
  19. package/dist/app/agentkit/react/root.js.map +1 -1
  20. package/dist/app/agentkit/react/styles.css +43 -6
  21. package/dist/app/agentkit/react/transcript-layout.d.ts +51 -0
  22. package/dist/app/agentkit/react/transcript-layout.d.ts.map +1 -0
  23. package/dist/app/agentkit/react/transcript-layout.js +34 -0
  24. package/dist/app/agentkit/react/transcript-layout.js.map +1 -0
  25. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  26. package/dist/app/chat/AgentKitAssistantChat.js +3 -3
  27. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  28. package/dist/app/chat/AgentPanel.d.ts +2 -0
  29. package/dist/app/chat/AgentPanel.d.ts.map +1 -1
  30. package/dist/app/chat/AgentPanel.js +6 -2
  31. package/dist/app/chat/AgentPanel.js.map +1 -1
  32. package/dist/app/chat/chat/index.d.ts +2 -0
  33. package/dist/app/chat/chat/index.d.ts.map +1 -1
  34. package/dist/app/chat/chat/index.js.map +1 -1
  35. package/dist/app/chat/chat/surface-types.d.ts +7 -0
  36. package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
  37. package/dist/app/chat/chat/surface-types.js.map +1 -1
  38. package/dist/app/chat/chat/tool-call-display.d.ts +7 -2
  39. package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
  40. package/dist/app/chat/chat/tool-call-display.js +10 -9
  41. package/dist/app/chat/chat/tool-call-display.js.map +1 -1
  42. package/dist/app/chat/chat/widgets/DataChartRenderer.d.ts.map +1 -1
  43. package/dist/app/chat/chat/widgets/DataChartRenderer.js +196 -26
  44. package/dist/app/chat/chat/widgets/DataChartRenderer.js.map +1 -1
  45. package/dist/app/chat/chat/widgets/DataChartWidget.d.ts.map +1 -1
  46. package/dist/app/chat/chat/widgets/DataChartWidget.js +5 -1
  47. package/dist/app/chat/chat/widgets/DataChartWidget.js.map +1 -1
  48. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts +2 -2
  49. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts.map +1 -1
  50. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js +6 -4
  51. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js.map +1 -1
  52. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  53. package/dist/app/i18n/catalogs/ar-SA.js +1 -0
  54. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  55. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  56. package/dist/app/i18n/catalogs/de-DE.js +1 -0
  57. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  58. package/dist/app/i18n/catalogs/en-US.d.ts +1 -0
  59. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  60. package/dist/app/i18n/catalogs/en-US.js +1 -0
  61. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  62. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  63. package/dist/app/i18n/catalogs/es-ES.js +1 -0
  64. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  65. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  66. package/dist/app/i18n/catalogs/fr-FR.js +1 -0
  67. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  68. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  69. package/dist/app/i18n/catalogs/hi-IN.js +1 -0
  70. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  71. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  72. package/dist/app/i18n/catalogs/ja-JP.js +1 -0
  73. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  74. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  75. package/dist/app/i18n/catalogs/ko-KR.js +1 -0
  76. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  77. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  78. package/dist/app/i18n/catalogs/pt-BR.js +1 -0
  79. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  80. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  81. package/dist/app/i18n/catalogs/zh-CN.js +1 -0
  82. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  83. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  84. package/dist/app/i18n/catalogs/zh-TW.js +1 -0
  85. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  86. package/dist/composer/ComposerContextBanner.d.ts +3 -1
  87. package/dist/composer/ComposerContextBanner.d.ts.map +1 -1
  88. package/dist/composer/ComposerContextBanner.js +2 -2
  89. package/dist/composer/ComposerContextBanner.js.map +1 -1
  90. package/package.json +3 -3
  91. package/src/app/agentkit/react/chat.tsx +2 -0
  92. package/src/app/agentkit/react/components.interaction.spec.tsx +107 -0
  93. package/src/app/agentkit/react/components.spec.tsx +28 -1
  94. package/src/app/agentkit/react/components.tsx +153 -17
  95. package/src/app/agentkit/react/context.tsx +12 -0
  96. package/src/app/agentkit/react/index.ts +1 -0
  97. package/src/app/agentkit/react/lifecycle.spec.tsx +186 -16
  98. package/src/app/agentkit/react/root.tsx +7 -0
  99. package/src/app/agentkit/react/styles.css +43 -6
  100. package/src/app/agentkit/react/transcript-layout.spec.ts +72 -0
  101. package/src/app/agentkit/react/transcript-layout.ts +97 -0
  102. package/src/app/auth/AuthPage.attribution.spec.tsx +6 -0
  103. package/src/app/chat/AgentKitAssistantChat.spec.tsx +39 -0
  104. package/src/app/chat/AgentKitAssistantChat.tsx +5 -0
  105. package/src/app/chat/AgentPanel.page-header-share.spec.tsx +101 -0
  106. package/src/app/chat/AgentPanel.tsx +14 -2
  107. package/src/app/chat/chat/index.ts +2 -0
  108. package/src/app/chat/chat/surface-types.ts +10 -0
  109. package/src/app/chat/chat/tool-call-display.spec.tsx +83 -0
  110. package/src/app/chat/chat/tool-call-display.tsx +40 -8
  111. package/src/app/chat/chat/widgets/DataChartRenderer.spec.tsx +632 -0
  112. package/src/app/chat/chat/widgets/DataChartRenderer.tsx +308 -51
  113. package/src/app/chat/chat/widgets/DataChartWidget.spec.tsx +126 -23
  114. package/src/app/chat/chat/widgets/DataChartWidget.tsx +10 -0
  115. package/src/app/chat/chat/widgets/DataWidgetDisplayControls.tsx +26 -6
  116. package/src/app/i18n/catalogs/ar-SA.ts +1 -0
  117. package/src/app/i18n/catalogs/de-DE.ts +2 -0
  118. package/src/app/i18n/catalogs/en-US.ts +2 -0
  119. package/src/app/i18n/catalogs/es-ES.ts +2 -0
  120. package/src/app/i18n/catalogs/fr-FR.ts +2 -0
  121. package/src/app/i18n/catalogs/hi-IN.ts +1 -0
  122. package/src/app/i18n/catalogs/ja-JP.ts +2 -0
  123. package/src/app/i18n/catalogs/ko-KR.ts +1 -0
  124. package/src/app/i18n/catalogs/pt-BR.ts +2 -0
  125. package/src/app/i18n/catalogs/zh-CN.ts +1 -0
  126. package/src/app/i18n/catalogs/zh-TW.ts +1 -0
  127. package/src/composer/ComposerContextBanner.spec.tsx +31 -5
  128. package/src/composer/ComposerContextBanner.tsx +7 -2
@@ -669,11 +669,18 @@ body:has(> [data-agent-native-composer-popover="true"]) > .fixed.inset-0 {
669
669
  }
670
670
 
671
671
  .agentkit-transcript-content {
672
+ position: relative;
672
673
  box-sizing: border-box;
673
- inline-size: min(100%, 47rem);
674
+ inline-size: min(100%, var(--agentkit-transcript-lane-width, 47rem));
674
675
  min-inline-size: 0;
675
676
  margin-inline: auto;
677
+ inset-inline-start: clamp(
678
+ calc((100% - min(100%, var(--agentkit-transcript-lane-width, 47rem))) / -2),
679
+ var(--agentkit-transcript-lane-offset-inline, 0px),
680
+ calc((100% - min(100%, var(--agentkit-transcript-lane-width, 47rem))) / 2)
681
+ );
676
682
  padding: 1.5rem 1rem;
683
+ padding-block-start: var(--agentkit-transcript-top-inset, 1.5rem);
677
684
  }
678
685
 
679
686
  .agentkit-scroll-to-bottom-control {
@@ -710,13 +717,21 @@ body:has(> [data-agent-native-composer-popover="true"]) > .fixed.inset-0 {
710
717
  display: flex;
711
718
  flex-direction: column;
712
719
  align-items: flex-start;
713
- margin-block: 0 1.25rem;
720
+ margin-block: 0 var(--agentkit-transcript-message-gap, 1.25rem);
714
721
  font-size: 0.875rem;
715
- line-height: 1.52;
722
+ font-family: var(
723
+ --agentkit-transcript-font-family,
724
+ var(--font-sans, inherit)
725
+ );
726
+ line-height: var(--agentkit-transcript-message-line-height, 1.52);
716
727
  }
717
728
 
718
729
  .agentkit-message[data-role="user"] {
719
730
  align-items: flex-end;
731
+ line-height: var(
732
+ --agentkit-user-bubble-line-height,
733
+ var(--agentkit-transcript-message-line-height, 1.52)
734
+ );
720
735
  }
721
736
 
722
737
  .agentkit-message-content {
@@ -726,8 +741,11 @@ body:has(> [data-agent-native-composer-popover="true"]) > .fixed.inset-0 {
726
741
  }
727
742
 
728
743
  .agentkit-message[data-role="user"] .agentkit-message-content {
729
- max-inline-size: min(88%, 38rem);
730
- padding: 0.625rem 0.8125rem;
744
+ position: relative;
745
+ inset-inline-start: var(--agentkit-user-bubble-offset-inline, 0px);
746
+ max-inline-size: min(88%, var(--agentkit-user-bubble-max-width, 38rem));
747
+ padding: var(--agentkit-user-bubble-padding-block, 0.625rem)
748
+ var(--agentkit-user-bubble-padding-inline, 0.8125rem);
731
749
  border-radius: var(--agentkit-radius-inner);
732
750
  background: var(--agentkit-subtle);
733
751
  }
@@ -845,6 +863,25 @@ body:has(> [data-agent-native-composer-popover="true"]) > .fixed.inset-0 {
845
863
  background: var(--agentkit-canvas);
846
864
  }
847
865
 
866
+ .agentkit-message-content [data-post-widget-text="true"] {
867
+ display: contents;
868
+ color: var(--agentkit-transcript-post-widget-text-color, inherit);
869
+ }
870
+
871
+ .agentkit-message-content [data-post-widget-text="true"] > p,
872
+ .agentkit-message-content [data-post-widget-text="true"] .agentkit-markdown p {
873
+ padding-block: var(
874
+ --agentkit-transcript-post-widget-paragraph-padding-block,
875
+ 0px
876
+ );
877
+ }
878
+
879
+ .agentkit-message-content
880
+ > [data-post-widget-text="true"]:last-child
881
+ > :last-child {
882
+ margin-block-end: 0;
883
+ }
884
+
848
885
  .agentkit-image-preview img {
849
886
  max-inline-size: 92vw;
850
887
  max-block-size: 90vh;
@@ -2133,6 +2170,6 @@ button.agentkit-object {
2133
2170
  }
2134
2171
 
2135
2172
  .agentkit-message[data-role="user"] .agentkit-message-content {
2136
- max-inline-size: 94%;
2173
+ max-inline-size: min(94%, var(--agentkit-user-bubble-max-width, 38rem));
2137
2174
  }
2138
2175
  }
@@ -0,0 +1,72 @@
1
+ import type { AgentKitController } from "@agent-native/agentkit/client";
2
+ import { createElement } from "react";
3
+ import { renderToStaticMarkup } from "react-dom/server";
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ import { useAgentKit } from "./context.js";
7
+ import { AgentKitRoot } from "./root.js";
8
+ import {
9
+ workedForRowChevronStyle,
10
+ workedForRowSummaryStyle,
11
+ } from "./transcript-layout.js";
12
+
13
+ function WorkedForLayoutProbe() {
14
+ const { workedForRowLayout, formatWorkedForDuration } = useAgentKit();
15
+ return createElement("span", {
16
+ "data-icon-gap": workedForRowLayout?.iconGap,
17
+ "data-duration": formatWorkedForDuration?.(16_000),
18
+ });
19
+ }
20
+
21
+ describe("worked-for row layout", () => {
22
+ it("applies shared dimensions to the summary and leading chevron", () => {
23
+ const layout = {
24
+ chevronSize: 12,
25
+ iconGap: 8,
26
+ fontSize: 13,
27
+ lineHeight: 16,
28
+ paddingBlock: 2,
29
+ paddingInline: 4,
30
+ };
31
+
32
+ expect(workedForRowSummaryStyle(layout)).toEqual({
33
+ gap: "8px",
34
+ fontSize: "13px",
35
+ lineHeight: "16px",
36
+ paddingBlock: "2px",
37
+ paddingInline: "4px",
38
+ fontFamily: "inherit",
39
+ });
40
+ expect(workedForRowChevronStyle(layout)).toEqual({
41
+ width: "12px",
42
+ height: "12px",
43
+ });
44
+ });
45
+
46
+ it("rejects invalid CSS dimensions", () => {
47
+ expect(() => workedForRowSummaryStyle({ fontSize: 0 })).toThrow(
48
+ "workedForRowLayout.fontSize must be a valid CSS length",
49
+ );
50
+ });
51
+
52
+ it("carries the layout from the AgentKit root into shared context", () => {
53
+ const layout = { iconGap: 8 };
54
+ const formatWorkedForDuration = (durationMs: number) =>
55
+ `${Math.round(durationMs / 1000)} seconds`;
56
+ const markup = renderToStaticMarkup(
57
+ createElement(
58
+ AgentKitRoot,
59
+ {
60
+ controller: {} as AgentKitController,
61
+ threadId: "thread",
62
+ workedForRowLayout: layout,
63
+ formatWorkedForDuration,
64
+ },
65
+ createElement(WorkedForLayoutProbe),
66
+ ),
67
+ );
68
+
69
+ expect(markup).toContain('data-icon-gap="8"');
70
+ expect(markup).toContain('data-duration="16 seconds"');
71
+ });
72
+ });
@@ -0,0 +1,97 @@
1
+ import type { CSSProperties } from "react";
2
+
3
+ export interface AgentChatTranscriptLayout {
4
+ /** Top padding above transcript messages, in CSS pixels. */
5
+ topInset?: number;
6
+ /** Maximum centered message lane width, in CSS pixels. */
7
+ messageLaneWidth?: number;
8
+ /** Signed inline offset for the centered message lane, in CSS pixels. */
9
+ messageLaneOffsetInline?: number;
10
+ /** Space after each message, in CSS pixels. */
11
+ messageGap?: number;
12
+ /** Assistant message line height, in CSS pixels. */
13
+ messageLineHeight?: number;
14
+ /** Font family for transcript messages. Omitted uses the shared semantic font. */
15
+ fontFamily?: string;
16
+ /** Optional appearance for assistant text displayed after a widget. */
17
+ postWidgetText?: {
18
+ tone?: "muted";
19
+ paragraphPaddingBlock?: number;
20
+ };
21
+ /** User bubble line height, in CSS pixels. */
22
+ userBubbleLineHeight?: number;
23
+ /** Maximum user bubble width, in CSS pixels. */
24
+ userBubbleMaxWidth?: number;
25
+ /** Signed inline offset for the user bubble, in CSS pixels. */
26
+ userBubbleOffsetInline?: number;
27
+ /** User bubble padding on the block and inline axes, in CSS pixels. */
28
+ userBubblePadding?: { block?: number; inline?: number };
29
+ }
30
+
31
+ /** Optional geometry for the completed-work disclosure row in chat. */
32
+ export interface AgentChatWorkedForRowLayout {
33
+ /** Chevron width and height, in CSS pixels. */
34
+ chevronSize?: number;
35
+ /** Space between the chevron and label, in CSS pixels. */
36
+ iconGap?: number;
37
+ /** Signed margin before the row, in CSS pixels. */
38
+ marginBlockStart?: number;
39
+ /** Label font size, in CSS pixels. */
40
+ fontSize?: number;
41
+ /** Label line height, in CSS pixels. */
42
+ lineHeight?: number;
43
+ /** Padding on the block axis, in CSS pixels. */
44
+ paddingBlock?: number;
45
+ /** Padding on the inline axis, in CSS pixels. */
46
+ paddingInline?: number;
47
+ }
48
+
49
+ function workedForRowLength(
50
+ value: number | undefined,
51
+ name: keyof AgentChatWorkedForRowLayout,
52
+ positive = false,
53
+ ) {
54
+ if (value === undefined) return undefined;
55
+ if (!Number.isFinite(value) || value < 0 || (positive && value === 0)) {
56
+ throw new RangeError(
57
+ `workedForRowLayout.${name} must be a valid CSS length`,
58
+ );
59
+ }
60
+ return `${value}px`;
61
+ }
62
+
63
+ function workedForRowOffset(
64
+ value: number | undefined,
65
+ name: keyof AgentChatWorkedForRowLayout,
66
+ ) {
67
+ if (value === undefined) return undefined;
68
+ if (!Number.isFinite(value)) {
69
+ throw new RangeError(`workedForRowLayout.${name} must be finite`);
70
+ }
71
+ return `${value}px`;
72
+ }
73
+
74
+ export function workedForRowSummaryStyle(
75
+ layout?: AgentChatWorkedForRowLayout,
76
+ ): CSSProperties | undefined {
77
+ if (!layout) return undefined;
78
+ return {
79
+ gap: workedForRowLength(layout.iconGap, "iconGap"),
80
+ marginBlockStart: workedForRowOffset(
81
+ layout.marginBlockStart,
82
+ "marginBlockStart",
83
+ ),
84
+ fontSize: workedForRowLength(layout.fontSize, "fontSize", true),
85
+ lineHeight: workedForRowLength(layout.lineHeight, "lineHeight", true),
86
+ paddingBlock: workedForRowLength(layout.paddingBlock, "paddingBlock"),
87
+ paddingInline: workedForRowLength(layout.paddingInline, "paddingInline"),
88
+ fontFamily: "inherit",
89
+ };
90
+ }
91
+
92
+ export function workedForRowChevronStyle(
93
+ layout?: AgentChatWorkedForRowLayout,
94
+ ): CSSProperties | undefined {
95
+ const size = workedForRowLength(layout?.chevronSize, "chevronSize", true);
96
+ return size ? { width: size, height: size } : undefined;
97
+ }
@@ -129,6 +129,12 @@ async function recordBoth(
129
129
 
130
130
  describe("AuthPage attribution", () => {
131
131
  beforeEach(() => {
132
+ // Attribution storage depends on the visitor's time zone, so pin it.
133
+ const realOptions = new Intl.DateTimeFormat().resolvedOptions();
134
+ vi.spyOn(Intl.DateTimeFormat.prototype, "resolvedOptions").mockReturnValue({
135
+ ...realOptions,
136
+ timeZone: "America/Los_Angeles",
137
+ });
132
138
  vi.useFakeTimers({ toFake: ["Date"] });
133
139
  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
134
140
  vi.stubGlobal(
@@ -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(