@agent-native/toolkit 0.206.0 → 0.206.1-nightly-20261009213000

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 (82) hide show
  1. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  2. package/dist/app/agentkit/react/components.js +3 -3
  3. package/dist/app/agentkit/react/components.js.map +1 -1
  4. package/dist/app/agentkit/react/context.d.ts +1 -0
  5. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  6. package/dist/app/agentkit/react/context.js +1 -0
  7. package/dist/app/agentkit/react/context.js.map +1 -1
  8. package/dist/app/agentkit/react/styles.css +43 -0
  9. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  10. package/dist/app/chat/AgentKitAssistantChat.js +219 -75
  11. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  12. package/dist/app/chat/agentkit-chat/guided-questions.d.ts +18 -0
  13. package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
  14. package/dist/app/chat/agentkit-chat/guided-questions.js +12 -4
  15. package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
  16. package/dist/app/chat/chat/run-recovery.d.ts +6 -1
  17. package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
  18. package/dist/app/chat/chat/run-recovery.js +6 -3
  19. package/dist/app/chat/chat/run-recovery.js.map +1 -1
  20. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  21. package/dist/app/i18n/catalogs/ar-SA.js +8 -2
  22. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  23. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  24. package/dist/app/i18n/catalogs/de-DE.js +8 -2
  25. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  26. package/dist/app/i18n/catalogs/en-US.d.ts +8 -2
  27. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  28. package/dist/app/i18n/catalogs/en-US.js +8 -2
  29. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  30. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  31. package/dist/app/i18n/catalogs/es-ES.js +8 -2
  32. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  33. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  34. package/dist/app/i18n/catalogs/fr-FR.js +8 -2
  35. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  36. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  37. package/dist/app/i18n/catalogs/hi-IN.js +8 -2
  38. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  39. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  40. package/dist/app/i18n/catalogs/ja-JP.js +8 -2
  41. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  42. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  43. package/dist/app/i18n/catalogs/ko-KR.js +8 -2
  44. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  45. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  46. package/dist/app/i18n/catalogs/pt-BR.js +8 -2
  47. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  48. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  49. package/dist/app/i18n/catalogs/zh-CN.js +8 -2
  50. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  51. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  52. package/dist/app/i18n/catalogs/zh-TW.js +8 -2
  53. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  54. package/dist/app/sharing/ShareButton.d.ts +9 -0
  55. package/dist/app/sharing/ShareButton.d.ts.map +1 -1
  56. package/dist/app/sharing/ShareButton.js +23 -11
  57. package/dist/app/sharing/ShareButton.js.map +1 -1
  58. package/package.json +4 -4
  59. package/src/app/agentkit/react/components.tsx +6 -3
  60. package/src/app/agentkit/react/context.tsx +2 -0
  61. package/src/app/agentkit/react/styles.css +43 -0
  62. package/src/app/chat/AgentKitAssistantChat.spec.tsx +425 -15
  63. package/src/app/chat/AgentKitAssistantChat.tsx +306 -95
  64. package/src/app/chat/agentkit-chat/guided-questions.flow.spec.tsx +89 -5
  65. package/src/app/chat/agentkit-chat/guided-questions.tsx +17 -6
  66. package/src/app/chat/chat/run-recovery.spec.tsx +48 -0
  67. package/src/app/chat/chat/run-recovery.tsx +23 -1
  68. package/src/app/i18n/catalogs/ar-SA.ts +14 -2
  69. package/src/app/i18n/catalogs/de-DE.ts +12 -2
  70. package/src/app/i18n/catalogs/en-US.ts +13 -2
  71. package/src/app/i18n/catalogs/es-ES.ts +12 -2
  72. package/src/app/i18n/catalogs/fr-FR.ts +12 -2
  73. package/src/app/i18n/catalogs/hi-IN.ts +12 -2
  74. package/src/app/i18n/catalogs/ja-JP.ts +12 -2
  75. package/src/app/i18n/catalogs/ko-KR.ts +12 -2
  76. package/src/app/i18n/catalogs/pt-BR.ts +12 -2
  77. package/src/app/i18n/catalogs/zh-CN.ts +8 -2
  78. package/src/app/i18n/catalogs/zh-TW.ts +8 -2
  79. package/src/app/settings/model/ProviderDialog.spec.tsx +1 -0
  80. package/src/app/sharing/ShareButton.spec.tsx +148 -0
  81. package/src/app/sharing/ShareButton.tsx +44 -15
  82. package/src/app/sharing/useShareButtonController.spec.tsx +25 -1
@@ -1,6 +1,9 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
- import { AgentKitClient } from "@agent-native/agentkit/client";
3
+ import {
4
+ AgentKitClient,
5
+ AgentKitUploadError,
6
+ } from "@agent-native/agentkit/client";
4
7
  import type {
5
8
  FilePart,
6
9
  AgentMessage,
@@ -1129,7 +1132,7 @@ describe("AgentKitAssistantChat host behavior", () => {
1129
1132
  ).toBeNull();
1130
1133
  });
1131
1134
 
1132
- it("shows and copies the active run ID as soon as it becomes available", async () => {
1135
+ it("copies the active run ID from its action menu", async () => {
1133
1136
  const props = baseProps();
1134
1137
  await mount(props);
1135
1138
 
@@ -1140,22 +1143,72 @@ describe("AgentKitAssistantChat host behavior", () => {
1140
1143
  chatMocks.thread = { ...chatMocks.thread, activeRunIds: ["run-active"] };
1141
1144
  await act(async () => root.render(<AgentKitAssistantChat {...props} />));
1142
1145
 
1143
- const copyRequestIdButton = container.querySelector(
1144
- '[data-agentkit-active-run-id-copy] button[aria-label="agentChat.message.copyRequestId"]',
1146
+ const actions = container.querySelector(
1147
+ "[data-agentkit-active-run-id-copy]",
1148
+ );
1149
+ const actionsTrigger = actions?.querySelector(
1150
+ 'button[aria-label="agentChat.message.actions"]',
1151
+ );
1152
+ expect(actionsTrigger).toBeTruthy();
1153
+ expect(actionsTrigger?.getAttribute("aria-haspopup")).toBe("menu");
1154
+ expect(
1155
+ actions?.querySelector(
1156
+ 'button[aria-label="agentChat.message.copyRequestId"]',
1157
+ ),
1158
+ ).toBeNull();
1159
+
1160
+ await act(async () => {
1161
+ actionsTrigger?.focus();
1162
+ actionsTrigger?.dispatchEvent(
1163
+ new KeyboardEvent("keydown", { bubbles: true, key: "ArrowDown" }),
1164
+ );
1165
+ await Promise.resolve();
1166
+ });
1167
+
1168
+ const menuItems = Array.from(
1169
+ document.body.querySelectorAll('[role="menuitem"]'),
1145
1170
  );
1146
- expect(copyRequestIdButton).toBeTruthy();
1171
+ expect(menuItems.map((item) => item.textContent?.trim())).toEqual([
1172
+ "agentChat.message.copyRequestId",
1173
+ ]);
1147
1174
 
1148
1175
  await act(async () => {
1149
- copyRequestIdButton?.click();
1176
+ menuItems[0]?.click();
1150
1177
  await Promise.resolve();
1151
1178
  });
1152
1179
 
1153
1180
  expect(chatMocks.writeClipboardText).toHaveBeenCalledWith("run-active");
1181
+ const copiedFeedback = container.querySelector(
1182
+ '[data-agentkit-active-run-id-copy] [role="status"]',
1183
+ );
1184
+ expect(copiedFeedback?.textContent).toContain("agentChat.common.copied");
1185
+ expect(copiedFeedback?.classList.contains("sr-only")).toBe(false);
1186
+
1187
+ chatMocks.writeClipboardText.mockResolvedValueOnce(false);
1188
+ await act(async () => {
1189
+ actionsTrigger?.focus();
1190
+ actionsTrigger?.dispatchEvent(
1191
+ new KeyboardEvent("keydown", { bubbles: true, key: "ArrowDown" }),
1192
+ );
1193
+ await Promise.resolve();
1194
+ });
1195
+ const retryMenuItem = document.body.querySelector('[role="menuitem"]');
1196
+ await act(async () => {
1197
+ retryMenuItem?.click();
1198
+ await Promise.resolve();
1199
+ });
1200
+ const failedFeedback = container.querySelector(
1201
+ '[data-agentkit-active-run-id-copy] [role="alert"]',
1202
+ );
1203
+ expect(failedFeedback?.textContent).toContain(
1204
+ "agentChat.recovery.copyFailed",
1205
+ );
1206
+ expect(failedFeedback?.classList.contains("sr-only")).toBe(false);
1154
1207
  expect(
1155
- container.querySelector(
1156
- '[data-agentkit-active-run-id-copy] button[aria-label="agentChat.common.copied"]',
1208
+ failedFeedback?.classList.contains(
1209
+ "agentkit-active-run-actions-feedback-error",
1157
1210
  ),
1158
- ).toBeTruthy();
1211
+ ).toBe(true);
1159
1212
  });
1160
1213
 
1161
1214
  it("does not duplicate Thinking after the run becomes active", async () => {
@@ -2905,7 +2958,10 @@ describe("AgentKitAssistantChat host behavior", () => {
2905
2958
  expect(context.prepareSubmission).toHaveBeenCalledOnce(),
2906
2959
  );
2907
2960
  });
2908
- rejection = expect(submission).rejects.toThrow("agentChat.error.failed");
2961
+ rejection = expect(submission).rejects.toMatchObject({
2962
+ message: "agentChat.composer.submissionScopeChanged",
2963
+ code: "submission_scope_changed",
2964
+ });
2909
2965
 
2910
2966
  await act(async () => {
2911
2967
  root.render(
@@ -3392,13 +3448,31 @@ describe("AgentKitAssistantChat host behavior", () => {
3392
3448
  "agentChat.composer.sessionExpired",
3393
3449
  "upload_session_expired",
3394
3450
  ],
3451
+ [
3452
+ 403,
3453
+ { error: "Forbidden" },
3454
+ "agentChat.composer.sessionExpired",
3455
+ "upload_session_expired",
3456
+ ],
3457
+ [
3458
+ 429,
3459
+ { error: "Too many uploads" },
3460
+ "agentChat.composer.uploadUnavailable",
3461
+ "upload_unavailable",
3462
+ ],
3395
3463
  [
3396
3464
  503,
3397
3465
  {
3398
3466
  error: "Storage provider is unavailable. Check File uploads settings.",
3399
3467
  },
3468
+ "agentChat.composer.uploadUnavailable",
3469
+ "upload_unavailable",
3470
+ ],
3471
+ [
3472
+ 400,
3473
+ { error: "No file uploaded" },
3400
3474
  "agentChat.composer.uploadFailed",
3401
- "upload_http_503",
3475
+ "upload_http_400",
3402
3476
  ],
3403
3477
  ] as const)(
3404
3478
  "preserves actionable upload failures for HTTP %s",
@@ -3424,17 +3498,284 @@ describe("AgentKitAssistantChat host behavior", () => {
3424
3498
  message,
3425
3499
  code,
3426
3500
  status,
3427
- retryable: status >= 500,
3501
+ retryable: status === 429 || status >= 500,
3428
3502
  });
3429
- if (status === 503) {
3430
- expect(error.message).not.toContain(payload.error);
3431
- }
3503
+ expect(error.message).not.toContain(payload.error);
3432
3504
  } finally {
3433
3505
  fetch.mockRestore();
3434
3506
  }
3435
3507
  },
3436
3508
  );
3437
3509
 
3510
+ it("localizes an upload that never reached the server", async () => {
3511
+ const fetch = vi
3512
+ .spyOn(globalThis, "fetch")
3513
+ .mockRejectedValue(new TypeError("Failed to fetch"));
3514
+ await mount(baseProps());
3515
+
3516
+ try {
3517
+ await expect(
3518
+ chatMocks.rootProps.clientOptions.upload(
3519
+ { uploadId: "upload-1", method: "POST", url: "/uploads" },
3520
+ {
3521
+ name: "reference.png",
3522
+ mediaType: "image/png",
3523
+ size: 4,
3524
+ body: new Blob(["data"], { type: "image/png" }),
3525
+ },
3526
+ ),
3527
+ ).rejects.toMatchObject({
3528
+ message: "agentChat.composer.uploadOffline",
3529
+ code: "upload_network_error",
3530
+ retryable: true,
3531
+ });
3532
+ } finally {
3533
+ fetch.mockRestore();
3534
+ }
3535
+ });
3536
+
3537
+ it("names a file over the upload limit before uploading anything", async () => {
3538
+ await mount(baseProps());
3539
+ const notes = new File(["notes"], "notes.txt", { type: "text/plain" });
3540
+ const huge = new File(["video"], "huge.mov", { type: "video/quicktime" });
3541
+ Object.defineProperty(huge, "size", { value: 26 * 1024 * 1024 });
3542
+
3543
+ await act(async () => {
3544
+ await expect(
3545
+ chatMocks.composerProps.onSubmit("Review these", [notes, huge], [], {
3546
+ intent: "immediate",
3547
+ }),
3548
+ ).rejects.toMatchObject({
3549
+ message: "huge.mov: agentChat.composer.fileTooLarge",
3550
+ code: "upload_failed",
3551
+ });
3552
+ });
3553
+
3554
+ expect(chatMocks.control.uploadFiles).not.toHaveBeenCalled();
3555
+ expect(chatMocks.control.sendMessage).not.toHaveBeenCalled();
3556
+ });
3557
+
3558
+ it("reuses the files that uploaded when a sibling failed", async () => {
3559
+ await mount(baseProps());
3560
+ const notes = new File(["notes"], "notes.txt", { type: "text/plain" });
3561
+ const sheet = new File(["sheet"], "sheet.csv", { type: "text/csv" });
3562
+ const notesPart = {
3563
+ type: "file" as const,
3564
+ name: "notes.txt",
3565
+ mediaType: "text/plain",
3566
+ url: "https://files.example.test/notes.txt",
3567
+ };
3568
+ const sheetPart = {
3569
+ type: "file" as const,
3570
+ name: "sheet.csv",
3571
+ mediaType: "text/csv",
3572
+ url: "https://files.example.test/sheet.csv",
3573
+ };
3574
+ chatMocks.control.uploadFiles
3575
+ .mockRejectedValueOnce(
3576
+ new AgentKitUploadError(
3577
+ [
3578
+ {
3579
+ index: 1,
3580
+ name: "sheet.csv",
3581
+ error: new Error("agentChat.composer.uploadUnavailable"),
3582
+ },
3583
+ ],
3584
+ [{ index: 0, part: notesPart }],
3585
+ ),
3586
+ )
3587
+ .mockResolvedValueOnce([sheetPart]);
3588
+
3589
+ await act(async () => {
3590
+ await expect(
3591
+ chatMocks.composerProps.onSubmit("Compare", [notes, sheet], [], {
3592
+ intent: "immediate",
3593
+ }),
3594
+ ).rejects.toThrow("sheet.csv: agentChat.composer.uploadUnavailable");
3595
+ });
3596
+ await act(async () => {
3597
+ await chatMocks.composerProps.onSubmit("Compare", [notes, sheet], [], {
3598
+ intent: "immediate",
3599
+ });
3600
+ });
3601
+
3602
+ expect(chatMocks.control.uploadFiles).toHaveBeenCalledTimes(2);
3603
+ expect(chatMocks.control.uploadFiles.mock.calls[1]?.[0]).toMatchObject([
3604
+ { name: "sheet.csv" },
3605
+ ]);
3606
+ expect(
3607
+ chatMocks.control.sendMessage.mock.calls[0]?.[0].attachments,
3608
+ ).toEqual([notesPart, sheetPart]);
3609
+ });
3610
+
3611
+ it("keeps an upload when provider readiness flips while it runs", async () => {
3612
+ const props = baseProps();
3613
+ await mount(props);
3614
+ const file = new File(["image"], "keep.png", { type: "image/png" });
3615
+ const uploading = Promise.withResolvers<FilePart[]>();
3616
+ chatMocks.control.uploadFiles.mockReturnValueOnce(uploading.promise);
3617
+ let submission!: Promise<void>;
3618
+ await act(async () => {
3619
+ submission = chatMocks.composerProps.onSubmit("Use this", [file], [], {
3620
+ intent: "immediate",
3621
+ });
3622
+ await vi.waitFor(() =>
3623
+ expect(chatMocks.control.uploadFiles).toHaveBeenCalledOnce(),
3624
+ );
3625
+ });
3626
+
3627
+ await act(async () =>
3628
+ root.render(
3629
+ <AgentKitAssistantChat {...props} composerSubmissionDisabled />,
3630
+ ),
3631
+ );
3632
+ await act(async () => {
3633
+ uploading.resolve([
3634
+ {
3635
+ type: "file",
3636
+ name: "keep.png",
3637
+ mediaType: "image/png",
3638
+ url: "https://files.example.test/keep.png",
3639
+ },
3640
+ ]);
3641
+ await submission;
3642
+ });
3643
+
3644
+ expect(chatMocks.control.sendMessage).toHaveBeenCalledOnce();
3645
+ expect(
3646
+ chatMocks.control.sendMessage.mock.calls[0]?.[0].attachments,
3647
+ ).toEqual([
3648
+ {
3649
+ type: "file",
3650
+ name: "keep.png",
3651
+ mediaType: "image/png",
3652
+ url: "https://files.example.test/keep.png",
3653
+ },
3654
+ ]);
3655
+ });
3656
+
3657
+ it("stores no inline image bytes in thread or queue snapshots", async () => {
3658
+ const dataUrl = "data:image/png;base64,aGVsbG8=";
3659
+ chatMocks.thread.messages = [
3660
+ {
3661
+ id: "user-image",
3662
+ role: "user",
3663
+ createdAt: "2026-10-09T00:00:00.000Z",
3664
+ parts: [
3665
+ { type: "text", text: "Use this image" },
3666
+ {
3667
+ type: "file",
3668
+ name: "inline.png",
3669
+ mediaType: "image/png",
3670
+ url: dataUrl,
3671
+ },
3672
+ {
3673
+ type: "file",
3674
+ name: "durable.png",
3675
+ mediaType: "image/png",
3676
+ url: "https://files.example.test/durable.png",
3677
+ },
3678
+ ],
3679
+ },
3680
+ ];
3681
+ chatMocks.thread.events = [
3682
+ {
3683
+ type: "message.created",
3684
+ message: chatMocks.thread.messages[0],
3685
+ },
3686
+ ];
3687
+ chatMocks.thread.queuedMessages = [
3688
+ {
3689
+ id: "queued-image",
3690
+ threadId: "thread-1",
3691
+ text: "And this one",
3692
+ createdAt: "2026-10-09T00:00:01.000Z",
3693
+ attachments: [
3694
+ {
3695
+ type: "file",
3696
+ name: "queued.png",
3697
+ mediaType: "image/png",
3698
+ url: dataUrl,
3699
+ },
3700
+ ],
3701
+ requestAttachments: [
3702
+ { type: "image", name: "pending.png", data: dataUrl },
3703
+ {
3704
+ type: "image",
3705
+ name: "resized.png",
3706
+ data: dataUrl,
3707
+ url: "https://files.example.test/resized.png",
3708
+ },
3709
+ ],
3710
+ },
3711
+ ];
3712
+ const ref = createRef<AssistantChatHandle>();
3713
+ await mount(
3714
+ baseProps({ createTransport: () => chatMocks.transport as never }),
3715
+ ref,
3716
+ );
3717
+
3718
+ const snapshot = ref.current?.exportThreadSnapshot();
3719
+ const threadData = snapshot?.threadData ?? "";
3720
+ const saved = JSON.parse(threadData);
3721
+
3722
+ expect(threadData).not.toContain("base64,");
3723
+ expect(threadData).not.toContain("data:image");
3724
+ expect(saved.agentKit.messages[0].parts).toEqual([
3725
+ { type: "text", text: "Use this image" },
3726
+ {
3727
+ type: "file",
3728
+ name: "inline.png",
3729
+ mediaType: "image/png",
3730
+ omitted: "inline-bytes",
3731
+ },
3732
+ {
3733
+ type: "file",
3734
+ name: "durable.png",
3735
+ mediaType: "image/png",
3736
+ url: "https://files.example.test/durable.png",
3737
+ },
3738
+ ]);
3739
+ expect(saved.queuedMessages[0].requestAttachments).toEqual([
3740
+ {
3741
+ type: "image",
3742
+ name: "resized.png",
3743
+ url: "https://files.example.test/resized.png",
3744
+ },
3745
+ ]);
3746
+ expect(
3747
+ chatMocks.thread.messages[0].parts.some(
3748
+ (part: { url?: string }) => part.url === dataUrl,
3749
+ ),
3750
+ ).toBe(true);
3751
+ });
3752
+
3753
+ it("uploads inline images before storing a deferred submission", async () => {
3754
+ chatMocks.history = { isRestoring: true };
3755
+ chatMocks.control.uploadFiles.mockResolvedValueOnce([
3756
+ {
3757
+ type: "file",
3758
+ name: "image",
3759
+ mediaType: "image/png",
3760
+ url: "https://files.example.test/image.png",
3761
+ },
3762
+ ]);
3763
+ const ref = createRef<AssistantChatHandle>();
3764
+ await mount(baseProps(), ref);
3765
+
3766
+ await act(async () => {
3767
+ await ref.current?.sendMessage("Use this image", [
3768
+ "data:image/png;base64,aGVsbG8=",
3769
+ ]);
3770
+ });
3771
+
3772
+ const stored = JSON.stringify([...chatMocks.appState.values()]);
3773
+ expect(chatMocks.control.uploadFiles).toHaveBeenCalledOnce();
3774
+ expect(stored).toContain("https://files.example.test/image.png");
3775
+ expect(stored).not.toContain("base64,");
3776
+ expect(stored).not.toContain("data:image");
3777
+ });
3778
+
3438
3779
  it("does not clear persisted selection when hydration fails", async () => {
3439
3780
  chatMocks.appState.set("pending-selection-context", {
3440
3781
  text: "selection to preserve",
@@ -4839,6 +5180,75 @@ describe("AgentKitAssistantChat host behavior", () => {
4839
5180
  });
4840
5181
  });
4841
5182
 
5183
+ it("retries a rejected attachment's request with its text and context only", async () => {
5184
+ const requestText = appendAgentChatContextToMessage(
5185
+ "Make a LinkedIn ad 1200x627",
5186
+ "Selected brand kit",
5187
+ );
5188
+ chatMocks.failureError = {
5189
+ code: "invalid_attachment",
5190
+ message: "The provider rejected this attachment.",
5191
+ retryable: false,
5192
+ };
5193
+ chatMocks.thread.messages = [
5194
+ {
5195
+ id: "user-attachment",
5196
+ role: "user",
5197
+ createdAt: new Date().toISOString(),
5198
+ parts: [
5199
+ { type: "text", text: requestText },
5200
+ {
5201
+ type: "file",
5202
+ name: "huge.tiff",
5203
+ mediaType: "image/tiff",
5204
+ url: "https://files.example.test/huge.tiff",
5205
+ },
5206
+ ],
5207
+ metadata: { model: "model-original", requestMode: "act" },
5208
+ },
5209
+ ];
5210
+ await mount(baseProps());
5211
+
5212
+ expect(chatMocks.failureProps.info).toMatchObject({
5213
+ errorCode: "invalid_attachment",
5214
+ });
5215
+ await act(async () => {
5216
+ chatMocks.failureProps.onRetryWithoutAttachments();
5217
+ await new Promise((resolve) => setTimeout(resolve, 0));
5218
+ });
5219
+
5220
+ expect(chatMocks.control.sendMessage).toHaveBeenCalledOnce();
5221
+ const request = chatMocks.control.sendMessage.mock.calls[0]?.[0];
5222
+ expect(request.text).toBe(requestText);
5223
+ expect(request.attachments).toEqual([]);
5224
+ expect(request.requestAttachments).toBeUndefined();
5225
+ expect(request.metadata).toMatchObject({
5226
+ model: "model-original",
5227
+ custom: {
5228
+ agentNativeRecoveryAction: "retry",
5229
+ agentNativeRecoveryOfRunId: "run-1",
5230
+ },
5231
+ });
5232
+ });
5233
+
5234
+ it("offers no attachment-free retry when the failed request had no attachments", async () => {
5235
+ chatMocks.failureError = {
5236
+ code: "invalid_attachment",
5237
+ message: "The provider rejected this attachment.",
5238
+ retryable: false,
5239
+ };
5240
+ chatMocks.thread.messages = [
5241
+ {
5242
+ id: "user-text",
5243
+ role: "user",
5244
+ parts: [{ type: "text", text: "Just text" }],
5245
+ },
5246
+ ];
5247
+ await mount(baseProps());
5248
+
5249
+ expect(chatMocks.failureProps.onRetryWithoutAttachments).toBeUndefined();
5250
+ });
5251
+
4842
5252
  it("reuses file IDs when retrying a saved request", async () => {
4843
5253
  chatMocks.thread.messages = [
4844
5254
  {