@agent-native/toolkit 0.208.0 → 0.209.0-nightly-20261014070126

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 (46) hide show
  1. package/dist/app/chat/MultiTabAssistantChat.d.ts.map +1 -1
  2. package/dist/app/chat/MultiTabAssistantChat.js +12 -1
  3. package/dist/app/chat/MultiTabAssistantChat.js.map +1 -1
  4. package/dist/app/chat/agentkit-chat/guided-questions.d.ts +12 -0
  5. package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
  6. package/dist/app/chat/agentkit-chat/guided-questions.js +32 -4
  7. package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
  8. package/dist/app/chat/agentkit-chat/index.d.ts +1 -1
  9. package/dist/app/chat/agentkit-chat/index.d.ts.map +1 -1
  10. package/dist/app/chat/agentkit-chat/index.js +1 -1
  11. package/dist/app/chat/agentkit-chat/index.js.map +1 -1
  12. package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
  13. package/dist/app/chat/chat/run-recovery.js +15 -8
  14. package/dist/app/chat/chat/run-recovery.js.map +1 -1
  15. package/dist/app/sharing/ShareButton.d.ts.map +1 -1
  16. package/dist/app/sharing/ShareButton.js +66 -73
  17. package/dist/app/sharing/ShareButton.js.map +1 -1
  18. package/dist/app/sharing/ShareDialog.d.ts.map +1 -1
  19. package/dist/app/sharing/ShareDialog.js +4 -12
  20. package/dist/app/sharing/ShareDialog.js.map +1 -1
  21. package/dist/sharing/ShareControls.d.ts +19 -1
  22. package/dist/sharing/ShareControls.d.ts.map +1 -1
  23. package/dist/sharing/ShareControls.js +14 -7
  24. package/dist/sharing/ShareControls.js.map +1 -1
  25. package/dist/sharing/SharePrimitives.js +1 -1
  26. package/dist/sharing/SharePrimitives.js.map +1 -1
  27. package/dist/sharing/index.d.ts +1 -1
  28. package/dist/sharing/index.d.ts.map +1 -1
  29. package/dist/sharing/index.js +1 -1
  30. package/dist/sharing/index.js.map +1 -1
  31. package/package.json +4 -4
  32. package/src/app/chat/MultiTabAssistantChat.spec.tsx +101 -2
  33. package/src/app/chat/MultiTabAssistantChat.tsx +14 -1
  34. package/src/app/chat/agentkit-chat/guided-questions.flow.spec.tsx +168 -0
  35. package/src/app/chat/agentkit-chat/guided-questions.tsx +54 -9
  36. package/src/app/chat/agentkit-chat/index.ts +2 -0
  37. package/src/app/chat/chat/run-recovery.spec.tsx +64 -0
  38. package/src/app/chat/chat/run-recovery.tsx +14 -8
  39. package/src/app/sharing/ShareButton.spec.tsx +125 -32
  40. package/src/app/sharing/ShareButton.tsx +119 -149
  41. package/src/app/sharing/ShareDialog.spec.tsx +31 -0
  42. package/src/app/sharing/ShareDialog.tsx +46 -71
  43. package/src/sharing/ShareControls.spec.tsx +86 -1
  44. package/src/sharing/ShareControls.tsx +83 -27
  45. package/src/sharing/SharePrimitives.tsx +1 -1
  46. package/src/sharing/index.ts +4 -0
@@ -1097,6 +1097,35 @@ export type PendingQuestionRefetchResult =
1097
1097
  | { status: "none" }
1098
1098
  | { status: "error"; error: unknown };
1099
1099
 
1100
+ /**
1101
+ * Window event dispatched once per human submit of a guided question card,
1102
+ * only when the flow's `onSubmitMessage` resolves `{ delivered: true }`, so apps can
1103
+ * observe answers (for example, which option was picked) without owning the
1104
+ * flow.
1105
+ */
1106
+ export const GUIDED_QUESTIONS_SUBMITTED_EVENT =
1107
+ "agentNative.guidedQuestionsSubmitted";
1108
+
1109
+ export interface GuidedQuestionsSubmittedDetail {
1110
+ stateKey: string;
1111
+ questions: GuidedQuestion[];
1112
+ answers: GuidedQuestionAnswers;
1113
+ }
1114
+
1115
+ function announceGuidedQuestionsSubmitted(
1116
+ detail: GuidedQuestionsSubmittedDetail,
1117
+ ): void {
1118
+ // Listeners are observers; a throwing one must never block the submit.
1119
+ try {
1120
+ if (typeof window === "undefined") return;
1121
+ window.dispatchEvent(
1122
+ new CustomEvent(GUIDED_QUESTIONS_SUBMITTED_EVENT, { detail }),
1123
+ );
1124
+ } catch {
1125
+ // coercion-ok: an observer failure must not affect delivering the answers.
1126
+ }
1127
+ }
1128
+
1100
1129
  function payloadBelongsToThread(
1101
1130
  payload: GuidedQuestionPayload,
1102
1131
  threadId: string | undefined,
@@ -1241,9 +1270,13 @@ export function useGuidedQuestionFlow({
1241
1270
  }, [queryClient, resolvedQueryKey, scopedKey, stateKey]);
1242
1271
 
1243
1272
  const sendAndClearOnDelivery = useCallback(
1244
- (send: () => void | Promise<{ delivered: boolean }>) => {
1273
+ (
1274
+ send: () => void | Promise<{ delivered: boolean }>,
1275
+ onDelivered?: () => void,
1276
+ ) => {
1245
1277
  if (submissionInFlightRef.current) return;
1246
1278
  const result = send();
1279
+ // A synchronous send reports no delivery, so it is never announced.
1247
1280
  if (!result) {
1248
1281
  clear();
1249
1282
  return;
@@ -1252,7 +1285,9 @@ export function useGuidedQuestionFlow({
1252
1285
  setIsSubmitting(true);
1253
1286
  void result
1254
1287
  .then(({ delivered }) => {
1255
- if (delivered) clear();
1288
+ if (!delivered) return;
1289
+ onDelivered?.();
1290
+ clear();
1256
1291
  })
1257
1292
  .catch((error) => {
1258
1293
  console.error("Guided question submission failed", error);
@@ -1276,6 +1311,12 @@ export function useGuidedQuestionFlow({
1276
1311
  return;
1277
1312
  }
1278
1313
  if (isSubmissionBlocked) return;
1314
+ const announce = () =>
1315
+ announceGuidedQuestionsSubmitted({
1316
+ stateKey,
1317
+ questions: visiblePayload?.questions ?? [],
1318
+ answers,
1319
+ });
1279
1320
  const formattedAnswers = formatGuidedAnswersForAgent(
1280
1321
  answers,
1281
1322
  visiblePayload?.questions,
@@ -1292,15 +1333,18 @@ export function useGuidedQuestionFlow({
1292
1333
  .filter(Boolean)
1293
1334
  .join("\n\n");
1294
1335
  if (onSubmitMessage) {
1295
- sendAndClearOnDelivery(() =>
1296
- onSubmitMessage({
1297
- answers,
1298
- formattedAnswers,
1299
- message: resolvedSubmitMessage,
1300
- context,
1301
- }),
1336
+ sendAndClearOnDelivery(
1337
+ () =>
1338
+ onSubmitMessage({
1339
+ answers,
1340
+ formattedAnswers,
1341
+ message: resolvedSubmitMessage,
1342
+ context,
1343
+ }),
1344
+ announce,
1302
1345
  );
1303
1346
  } else {
1347
+ // Fire-and-forget: there is no delivery signal to announce on.
1304
1348
  sendToAgentChat({
1305
1349
  message: resolvedSubmitMessage,
1306
1350
  context,
@@ -1315,6 +1359,7 @@ export function useGuidedQuestionFlow({
1315
1359
  onSubmitMessage,
1316
1360
  sendAndClearOnDelivery,
1317
1361
  isSubmissionBlocked,
1362
+ stateKey,
1318
1363
  visiblePayload,
1319
1364
  submitMessage,
1320
1365
  ],
@@ -6,6 +6,7 @@ export {
6
6
  } from "./questions.js";
7
7
  export {
8
8
  askUserQuestion,
9
+ GUIDED_QUESTIONS_SUBMITTED_EVENT,
9
10
  formatGuidedAnswerValue,
10
11
  formatGuidedAnswersForAgent,
11
12
  getOtherGuidedAnswerText,
@@ -23,6 +24,7 @@ export {
23
24
  type GuidedQuestionOption,
24
25
  type GuidedQuestionPayload,
25
26
  type GuidedQuestionType,
27
+ type GuidedQuestionsSubmittedDetail,
26
28
  type UseGuidedQuestionFlowOptions,
27
29
  } from "./guided-questions.js";
28
30
  export { AgentKitActionWidget } from "./action-widget.js";
@@ -1076,6 +1076,70 @@ describe("run recovery surfaces", () => {
1076
1076
  );
1077
1077
  });
1078
1078
 
1079
+ it("keeps the setup card working when setup tracking throws", async () => {
1080
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
1081
+ setupTelemetryMock.mockImplementation(() => {
1082
+ throw new Error("tracker exploded");
1083
+ });
1084
+ const onConnected = vi.fn();
1085
+ const router = createMemoryRouter(
1086
+ [
1087
+ {
1088
+ path: "/home",
1089
+ element: (
1090
+ <AgentNativeI18nProvider
1091
+ initialLocale="en-US"
1092
+ initialPreference="en-US"
1093
+ persistPreference={false}
1094
+ >
1095
+ <BuilderSetupCard onConnected={onConnected} />
1096
+ </AgentNativeI18nProvider>
1097
+ ),
1098
+ },
1099
+ { path: "/settings/keys", element: <SetupTrackingFlowProbe /> },
1100
+ ],
1101
+ { initialEntries: ["/home"] },
1102
+ );
1103
+
1104
+ await act(async () => {
1105
+ root.render(<RouterProvider router={router} />);
1106
+ });
1107
+ expect(container.querySelector(".agent-builder-setup-card")).not.toBeNull();
1108
+
1109
+ await act(async () => {
1110
+ container
1111
+ .querySelector<HTMLButtonElement>(
1112
+ ".agent-builder-setup-card__builder-button",
1113
+ )
1114
+ ?.click();
1115
+ });
1116
+ await vi.waitFor(() =>
1117
+ expect(builderConnectMock.onConnect).toBeTypeOf("function"),
1118
+ );
1119
+ await act(async () => builderConnectMock.onConnect?.(true));
1120
+ await act(async () => {
1121
+ await expect(
1122
+ builderFlowMock.onConnected?.({ orgName: null }),
1123
+ ).resolves.toBeUndefined();
1124
+ });
1125
+ expect(onConnected).toHaveBeenCalledTimes(1);
1126
+
1127
+ await act(async () => {
1128
+ container
1129
+ .querySelector<HTMLAnchorElement>('a[href="/settings/keys"]')
1130
+ ?.click();
1131
+ await Promise.resolve();
1132
+ });
1133
+ expect(router.state.location.pathname).toBe("/settings/keys");
1134
+ expect(setupTelemetryMock.mock.calls.map(([name]) => name)).toEqual([
1135
+ "integration_setup_exposed",
1136
+ "integration_method_clicked",
1137
+ "integration_method_outcome",
1138
+ "integration_method_clicked",
1139
+ ]);
1140
+ expect(warn).toHaveBeenCalled();
1141
+ });
1142
+
1079
1143
  it("tracks Builder success only after a selected setup attempt is confirmed", async () => {
1080
1144
  const card = (
1081
1145
  <AgentNativeI18nProvider
@@ -82,14 +82,20 @@ function trackSetupFunnelEvent(
82
82
  action: SetupTelemetryAction,
83
83
  outcome: SetupTelemetryOutcome,
84
84
  ): void {
85
- trackOnboardingEvent(eventName, {
86
- flow: "chat_setup",
87
- app_name: setupTelemetryAppName(),
88
- step_id: "connect_ai",
89
- method_id: methodId,
90
- action,
91
- outcome,
92
- });
85
+ // Runs in the card's mount effect and ahead of the setup links' navigation,
86
+ // so a throw here would unmount the card or swallow the click.
87
+ try {
88
+ trackOnboardingEvent(eventName, {
89
+ flow: "chat_setup",
90
+ app_name: setupTelemetryAppName(),
91
+ step_id: "connect_ai",
92
+ method_id: methodId,
93
+ action,
94
+ outcome,
95
+ });
96
+ } catch (err) {
97
+ console.warn(`[connect-ai-setup] ${eventName} tracking failed:`, err);
98
+ }
93
99
  }
94
100
 
95
101
  type BuilderConnectTelemetryAttempt = {
@@ -92,7 +92,7 @@ vi.mock("@agent-native/toolkit/ui/popover", () => {
92
92
  string,
93
93
  unknown
94
94
  >;
95
- return String(props.className ?? "").includes("w-[min(460px,92vw)]");
95
+ return String(props.className ?? "").includes("w-[360px]");
96
96
  });
97
97
 
98
98
  return {
@@ -191,6 +191,15 @@ function setInputValue(
191
191
  });
192
192
  }
193
193
 
194
+ // Radix tabs activate on mousedown, like a real pointer press.
195
+ function pressTab(tab: HTMLElement) {
196
+ act(() => {
197
+ tab.dispatchEvent(
198
+ new MouseEvent("mousedown", { bubbles: true, button: 0 }),
199
+ );
200
+ });
201
+ }
202
+
194
203
  describe("ShareButton", () => {
195
204
  let container: HTMLDivElement;
196
205
  let root: Root;
@@ -265,8 +274,8 @@ describe("ShareButton", () => {
265
274
  ) as HTMLInputElement;
266
275
  setInputValue(input, "first@example.com");
267
276
 
268
- const add = Array.from(container.querySelectorAll("button")).find(
269
- (button) => button.textContent === "Add",
277
+ const add = container.querySelector<HTMLButtonElement>(
278
+ 'button[aria-label="Add"]',
270
279
  );
271
280
  if (!add) throw new Error("Add button not found");
272
281
 
@@ -323,8 +332,8 @@ describe("ShareButton", () => {
323
332
  'input[placeholder="Add people by email"]',
324
333
  ) as HTMLInputElement;
325
334
  setInputValue(input, "commenter@example.com");
326
- const add = Array.from(container.querySelectorAll("button")).find(
327
- (button) => button.textContent === "Add",
335
+ const add = container.querySelector<HTMLButtonElement>(
336
+ 'button[aria-label="Add"]',
328
337
  );
329
338
  if (!add) throw new Error("Add button not found");
330
339
 
@@ -391,8 +400,8 @@ describe("ShareButton", () => {
391
400
  ) as HTMLTextAreaElement;
392
401
  setInputValue(message, "Here is the latest version.");
393
402
 
394
- const add = Array.from(container.querySelectorAll("button")).find(
395
- (button) => button.textContent === "Add",
403
+ const add = container.querySelector<HTMLButtonElement>(
404
+ 'button[aria-label="Add"]',
396
405
  );
397
406
  if (!add) throw new Error("Add button not found");
398
407
  act(() => add.click());
@@ -406,6 +415,55 @@ describe("ShareButton", () => {
406
415
  );
407
416
  });
408
417
 
418
+ it("keeps the invite row compact with the role inside the field and an icon-only Add", async () => {
419
+ await act(async () => {
420
+ root.render(
421
+ <QueryClientProvider client={queryClient}>
422
+ <ShareButton resourceType="document" resourceId="doc-1" />
423
+ </QueryClientProvider>,
424
+ );
425
+ });
426
+
427
+ const input = container.querySelector<HTMLInputElement>(
428
+ 'input[placeholder="Add people by email"]',
429
+ );
430
+ const field = input?.parentElement;
431
+ const role = container.querySelector('button[aria-label="Role"]');
432
+ const add = container.querySelector<HTMLButtonElement>(
433
+ 'button[aria-label="Add"]',
434
+ );
435
+ expect(field?.contains(role ?? null)).toBe(true);
436
+ expect(field?.contains(add)).toBe(false);
437
+ expect(add?.textContent).toBe("");
438
+ expect(add?.querySelector("svg")).not.toBeNull();
439
+ expect(add?.getAttribute("title")).toBe("Add");
440
+ expect(add?.disabled).toBe(true);
441
+ });
442
+
443
+ it("renders the compact 360px popover and lets a host raise its layer", async () => {
444
+ await act(async () => {
445
+ root.render(
446
+ <QueryClientProvider client={queryClient}>
447
+ <ShareButton
448
+ resourceType="design"
449
+ resourceId="design-1"
450
+ popoverClassName="z-[100010]"
451
+ />
452
+ </QueryClientProvider>,
453
+ );
454
+ });
455
+
456
+ const popover = Array.from(
457
+ container.querySelectorAll<HTMLElement>(
458
+ "[data-agent-native-share-overlay]",
459
+ ),
460
+ ).find((node) => node.className.includes("w-[360px]"));
461
+ expect(popover).toBeDefined();
462
+ expect(popover?.className).toContain("z-[100010]");
463
+ expect(popover?.className).not.toContain("z-[2000]");
464
+ expect(popover?.className).not.toContain("460px");
465
+ });
466
+
409
467
  it("keeps a draft email when the share popover is closed and reopened", async () => {
410
468
  popoverTestState.simulateMounting = true;
411
469
 
@@ -954,31 +1012,68 @@ describe("ShareButton", () => {
954
1012
  );
955
1013
  });
956
1014
 
957
- expect(container.textContent).toContain("Share link");
958
- expect(container.textContent).toContain("Export");
959
- expect(container.textContent).toContain("Send to...");
960
- expect(container.textContent).toContain("Context");
1015
+ const tabs = Array.from(
1016
+ container.querySelectorAll<HTMLButtonElement>('[role="tab"]'),
1017
+ );
1018
+ expect(tabs.map((tab) => tab.textContent)).toEqual([
1019
+ "Share link",
1020
+ "Export",
1021
+ "Send to...",
1022
+ "Context",
1023
+ ]);
1024
+ expect(tabs[0].getAttribute("aria-selected")).toBe("true");
1025
+ expect(
1026
+ container.querySelector('[role="tablist"]')?.getAttribute("aria-label"),
1027
+ ).toBe("Share options");
961
1028
  expect(container.textContent).not.toContain("Context body");
962
1029
  expect(container.textContent).not.toContain("Export body");
963
- for (const tab of container.querySelectorAll<HTMLButtonElement>(
964
- '[role="tab"]',
965
- )) {
966
- expect(
967
- document.getElementById(tab.getAttribute("aria-controls") ?? ""),
968
- ).not.toBeNull();
969
- }
970
-
971
- const exportTab = Array.from(container.querySelectorAll("button")).find(
972
- (button) => button.textContent === "Export",
973
- );
974
- if (!exportTab) throw new Error("Export tab not found");
1030
+ expect(
1031
+ container
1032
+ .querySelector('[role="tabpanel"]')
1033
+ ?.getAttribute("aria-labelledby"),
1034
+ ).toBe(tabs[0].id);
975
1035
 
976
- act(() => {
977
- exportTab.click();
978
- });
1036
+ pressTab(tabs[1]);
979
1037
 
1038
+ expect(tabs[1].getAttribute("aria-selected")).toBe("true");
980
1039
  expect(container.textContent).toContain("Export body");
981
1040
  expect(container.textContent).not.toContain("Send body");
1041
+ expect(container.textContent).not.toContain("Add people by email");
1042
+ });
1043
+
1044
+ it("draws every share tab, including app-owned ones, as an underline tab", async () => {
1045
+ await act(async () => {
1046
+ root.render(
1047
+ <QueryClientProvider client={queryClient}>
1048
+ <ShareButton
1049
+ resourceType="design"
1050
+ resourceId="design-1"
1051
+ shareTabs={{
1052
+ tabs: [
1053
+ {
1054
+ value: "export",
1055
+ label: "Export",
1056
+ content: <div>Export body</div>,
1057
+ },
1058
+ ],
1059
+ }}
1060
+ />
1061
+ </QueryClientProvider>,
1062
+ );
1063
+ });
1064
+
1065
+ const list = container.querySelector<HTMLElement>('[role="tablist"]');
1066
+ expect(list?.className).toContain("bg-transparent");
1067
+ expect(list?.className).not.toContain("bg-muted");
1068
+ const tabs = Array.from(
1069
+ container.querySelectorAll<HTMLElement>('[role="tab"]'),
1070
+ );
1071
+ expect(tabs).toHaveLength(2);
1072
+ for (const tab of tabs) {
1073
+ expect(tab.className).toContain("after:h-0.5");
1074
+ expect(tab.className).toContain("data-[state=active]:bg-transparent");
1075
+ expect(tab.className).not.toContain("shadow-sm");
1076
+ }
982
1077
  });
983
1078
 
984
1079
  it("renders the context tab when it is the only custom share tab", async () => {
@@ -1007,14 +1102,12 @@ describe("ShareButton", () => {
1007
1102
  expect(container.textContent).not.toContain("Context body");
1008
1103
  expect(container.querySelector('[role="tablist"]')).not.toBeNull();
1009
1104
 
1010
- const contextTab = Array.from(container.querySelectorAll("button")).find(
1011
- (button) => button.textContent === "Context",
1012
- );
1105
+ const contextTab = Array.from(
1106
+ container.querySelectorAll<HTMLElement>('[role="tab"]'),
1107
+ ).find((tab) => tab.textContent === "Context");
1013
1108
  if (!contextTab) throw new Error("Context tab not found");
1014
1109
 
1015
- act(() => {
1016
- contextTab.click();
1017
- });
1110
+ pressTab(contextTab);
1018
1111
 
1019
1112
  expect(container.textContent).toContain("Context body");
1020
1113
  const contextPanelId = contextTab.getAttribute("aria-controls");