@agent-native/toolkit 0.198.4 → 0.198.6

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 (96) hide show
  1. package/dist/app/agentkit/react/components.d.ts +6 -3
  2. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/components.js +98 -68
  4. package/dist/app/agentkit/react/components.js.map +1 -1
  5. package/dist/app/agentkit/react/styles.css +36 -24
  6. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  7. package/dist/app/chat/AgentKitAssistantChat.js +32 -15
  8. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  9. package/dist/app/chat/AgentPanel.d.ts +1 -1
  10. package/dist/app/chat/AgentPanel.d.ts.map +1 -1
  11. package/dist/app/chat/AgentPanel.js +11 -38
  12. package/dist/app/chat/AgentPanel.js.map +1 -1
  13. package/dist/app/chat/AgentSidebar.d.ts.map +1 -1
  14. package/dist/app/chat/AgentSidebar.js +3 -11
  15. package/dist/app/chat/AgentSidebar.js.map +1 -1
  16. package/dist/app/chat/chat/tool-call-display.d.ts +2 -1
  17. package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
  18. package/dist/app/chat/chat/tool-call-display.js +83 -55
  19. package/dist/app/chat/chat/tool-call-display.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 +1 -0
  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 +1 -0
  25. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  26. package/dist/app/i18n/catalogs/en-US.d.ts +1 -0
  27. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  28. package/dist/app/i18n/catalogs/en-US.js +1 -0
  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 +1 -0
  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 +1 -0
  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 +1 -0
  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 +1 -0
  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 +1 -0
  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 +1 -0
  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 +1 -0
  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 +1 -0
  53. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  54. package/dist/composer/PromptComposer.d.ts +3 -0
  55. package/dist/composer/PromptComposer.d.ts.map +1 -1
  56. package/dist/composer/PromptComposer.js +2 -2
  57. package/dist/composer/PromptComposer.js.map +1 -1
  58. package/dist/composer/TiptapComposer.d.ts +4 -1
  59. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  60. package/dist/composer/TiptapComposer.js +32 -25
  61. package/dist/composer/TiptapComposer.js.map +1 -1
  62. package/dist/ui/sonner.js +1 -1
  63. package/dist/ui/sonner.js.map +1 -1
  64. package/package.json +3 -3
  65. package/src/app/agentkit/react/components.interaction.spec.tsx +93 -0
  66. package/src/app/agentkit/react/components.spec.tsx +1 -2
  67. package/src/app/agentkit/react/components.tsx +185 -132
  68. package/src/app/agentkit/react/composer-context.spec.tsx +7 -0
  69. package/src/app/agentkit/react/lifecycle.spec.tsx +556 -39
  70. package/src/app/agentkit/react/styles.css +36 -24
  71. package/src/app/chat/AgentKitAssistantChat.spec.tsx +60 -1
  72. package/src/app/chat/AgentKitAssistantChat.tsx +45 -14
  73. package/src/app/chat/AgentPanel.overlay.spec.tsx +28 -4
  74. package/src/app/chat/AgentPanel.tsx +51 -168
  75. package/src/app/chat/AgentSidebar.spec.tsx +19 -19
  76. package/src/app/chat/AgentSidebar.tsx +59 -94
  77. package/src/app/chat/chat/tool-call-display.spec.tsx +371 -15
  78. package/src/app/chat/chat/tool-call-display.tsx +154 -80
  79. package/src/app/i18n/catalogs/ar-SA.ts +1 -0
  80. package/src/app/i18n/catalogs/de-DE.ts +1 -0
  81. package/src/app/i18n/catalogs/en-US.ts +1 -0
  82. package/src/app/i18n/catalogs/es-ES.ts +1 -0
  83. package/src/app/i18n/catalogs/fr-FR.ts +1 -0
  84. package/src/app/i18n/catalogs/hi-IN.ts +1 -0
  85. package/src/app/i18n/catalogs/ja-JP.ts +1 -0
  86. package/src/app/i18n/catalogs/ko-KR.ts +1 -0
  87. package/src/app/i18n/catalogs/pt-BR.ts +1 -0
  88. package/src/app/i18n/catalogs/zh-CN.ts +1 -0
  89. package/src/app/i18n/catalogs/zh-TW.ts +1 -0
  90. package/src/app-shell/sidebar-overflow.spec.tsx +2 -1
  91. package/src/composer/PromptComposer.context.spec.tsx +28 -15
  92. package/src/composer/PromptComposer.tsx +7 -0
  93. package/src/composer/TiptapComposer.spec.ts +40 -6
  94. package/src/composer/TiptapComposer.tsx +89 -89
  95. package/src/ui/sonner.spec.ts +15 -0
  96. package/src/ui/sonner.tsx +1 -1
@@ -791,19 +791,16 @@ describe("controlled composer context", () => {
791
791
  expect(option).toBeDefined();
792
792
  await act(async () => option.click());
793
793
  expect(onSelect).toHaveBeenCalledOnce();
794
+ const contextRow = container.querySelector('[data-context-key="brief"]');
795
+ expect(contextRow?.querySelectorAll("button")).toHaveLength(1);
794
796
  await act(async () => {
795
- container
796
- .querySelector<HTMLButtonElement>(
797
- 'button[aria-label="Retry Brief context"]',
798
- )!
799
- .click();
800
797
  container
801
798
  .querySelector<HTMLButtonElement>(
802
799
  'button[aria-label="Remove Brief context"]',
803
800
  )!
804
801
  .click();
805
802
  });
806
- expect(onRetry).toHaveBeenCalledWith("brief");
803
+ expect(onRetry).not.toHaveBeenCalled();
807
804
  expect(onRemove).toHaveBeenCalledWith("brief");
808
805
  expect(onDisabledClick).not.toHaveBeenCalled();
809
806
  const send = container.querySelector<HTMLButtonElement>(
@@ -1456,7 +1453,7 @@ describe("controlled composer context", () => {
1456
1453
  expect(remove).not.toHaveBeenCalled();
1457
1454
  },
1458
1455
  );
1459
- it("renders context inside the frame, forwards inspection/retry/removal, and blocks click and keyboard submission until ready", async () => {
1456
+ it("renders a noninteractive context chip with only removal and blocks submission until ready", async () => {
1460
1457
  const onSubmit = vi.fn();
1461
1458
  const onRemoveContextItem = vi.fn();
1462
1459
  const onInspectContextItem = vi.fn();
@@ -1522,25 +1519,41 @@ describe("controlled composer context", () => {
1522
1519
  await pressEnter();
1523
1520
  expect(onSubmit).not.toHaveBeenCalled();
1524
1521
  const contextRow = container.querySelector('[data-context-key="brief"]')!;
1522
+ expect(contextRow.className).toContain("py-0.5");
1523
+ expect(contextRow.querySelector("span")?.textContent).toBe("Brief");
1524
+ expect(contextRow.querySelectorAll("button")).toHaveLength(1);
1525
+ expect(
1526
+ contextRow.querySelector('[aria-label="Context failed"]'),
1527
+ ).not.toBeNull();
1528
+ const retryContext = container.querySelector<HTMLButtonElement>(
1529
+ 'button[aria-label="Retry Brief context"]',
1530
+ );
1531
+ expect(retryContext).not.toBeNull();
1532
+ expect(contextRow.contains(retryContext)).toBe(false);
1533
+ await act(async () => retryContext!.click());
1534
+ expect(onRetryContextItem).toHaveBeenCalledWith("brief");
1525
1535
  await act(async () => {
1526
- contextRow.querySelector<HTMLButtonElement>("button")!.click();
1527
- contextRow
1528
- .querySelector<HTMLButtonElement>(
1529
- 'button[aria-label="Retry Brief context"]',
1530
- )!
1531
- .click();
1532
1536
  contextRow
1533
1537
  .querySelector<HTMLButtonElement>(
1534
1538
  'button[aria-label="Remove Brief context"]',
1535
1539
  )!
1536
1540
  .click();
1537
1541
  });
1538
- expect(onInspectContextItem).toHaveBeenCalledWith("brief");
1539
- expect(onRetryContextItem).toHaveBeenCalledWith("brief");
1542
+ expect(onInspectContextItem).not.toHaveBeenCalled();
1543
+ expect(onRetryContextItem).toHaveBeenCalledOnce();
1540
1544
  expect(onRemoveContextItem).toHaveBeenCalledWith("brief");
1541
1545
  expect(
1542
1546
  container.querySelector('[data-context-key="brief"]'),
1543
1547
  ).not.toBeNull();
1548
+ item.removable = false;
1549
+ await render();
1550
+ const nonremovableContextRow = container.querySelector(
1551
+ '[data-context-key="brief"]',
1552
+ )!;
1553
+ expect(nonremovableContextRow.querySelectorAll("button")).toHaveLength(0);
1554
+ expect(
1555
+ container.querySelector('button[aria-label="Retry Brief context"]'),
1556
+ ).not.toBeNull();
1544
1557
  item.status = "ready";
1545
1558
  await render();
1546
1559
  await clickSend();
@@ -106,6 +106,7 @@ export interface PromptComposerProps {
106
106
  ) => void | Promise<void>;
107
107
  /** Return false to stop a submit before it reaches the host runtime. */
108
108
  onBeforeSubmit?: () => boolean | Promise<boolean>;
109
+ onSubmissionPendingChange?: (pending: boolean) => void;
109
110
  /** Scope where a failed submission should be recovered after the host forks. */
110
111
  getSubmitFailureDraftScope?: () => string | null;
111
112
  /** Handle file paste/drop errors in the host chat surface. */
@@ -118,6 +119,8 @@ export interface PromptComposerProps {
118
119
  disabled?: boolean;
119
120
  /** Block all submission paths while allowing draft, file, and context staging. */
120
121
  submissionDisabled?: boolean;
122
+ /** Disable only the send control while the submission is being accepted. */
123
+ sendButtonDisabled?: boolean;
121
124
  /** Prevent submission while preserving editor focus and draft entry. */
122
125
  submitting?: boolean;
123
126
  /** Present the primary action as queueing instead of immediate send. */
@@ -608,6 +611,7 @@ function PromptComposerInner({
608
611
  ariaLabel,
609
612
  disabled,
610
613
  submissionDisabled,
614
+ sendButtonDisabled,
611
615
  submitting,
612
616
  willQueue = false,
613
617
  onDisabledClick,
@@ -680,6 +684,7 @@ function PromptComposerInner({
680
684
  imageModelMenu,
681
685
  composerRef,
682
686
  onBeforeSubmit,
687
+ onSubmissionPendingChange,
683
688
  getSubmitFailureDraftScope,
684
689
  onAttachmentError,
685
690
  interceptBuildRequestsForBuilder,
@@ -890,6 +895,7 @@ function PromptComposerInner({
890
895
  disabled={disabled}
891
896
  contextControlsDisabled={engineSubmissionBlocked}
892
897
  submissionDisabled={submissionDisabled || engineSubmissionBlocked}
898
+ sendButtonDisabled={sendButtonDisabled}
893
899
  submitting={submitting}
894
900
  willQueue={willQueue}
895
901
  maxDocumentAttachmentBytes={maxDocumentAttachmentBytes}
@@ -899,6 +905,7 @@ function PromptComposerInner({
899
905
  initialTextKey={initialTextKey}
900
906
  onSubmit={handleSubmit}
901
907
  onBeforeSubmit={onBeforeSubmit}
908
+ onSubmissionPendingChange={onSubmissionPendingChange}
902
909
  getSubmitFailureDraftScope={getSubmitFailureDraftScope}
903
910
  onAttachmentError={onAttachmentError}
904
911
  interceptBuildRequestsForBuilder={interceptBuildRequestsForBuilder}
@@ -1581,10 +1581,16 @@ describe("createTiptapComposerExtensions", () => {
1581
1581
  availableModels: [
1582
1582
  {
1583
1583
  engine: "builder",
1584
- label: "Builder.io Gateway",
1584
+ label: "OpenAI · Builder.io",
1585
1585
  models,
1586
1586
  configured: true,
1587
1587
  },
1588
+ {
1589
+ engine: "claude",
1590
+ label: "Claude · Builder.io",
1591
+ models: ["claude-haiku-4-5"],
1592
+ configured: true,
1593
+ },
1588
1594
  ],
1589
1595
  selectedModel: "gpt-6-luna",
1590
1596
  selectedEngine: "builder",
@@ -1619,6 +1625,9 @@ describe("createTiptapComposerExtensions", () => {
1619
1625
  expect(picker?.textContent).toContain("GPT-6 Luna");
1620
1626
  expect(picker?.textContent).toContain("Claude Opus 5.5");
1621
1627
  expect(picker?.textContent).toContain("Gemini 3.8 Flash");
1628
+ expect(picker?.textContent).toContain("OpenAI");
1629
+ expect(picker?.textContent).toContain("Claude");
1630
+ expect(picker?.textContent).not.toContain("Builder.io");
1622
1631
  });
1623
1632
 
1624
1633
  it("chooses the newest tier version regardless of catalog order", () => {
@@ -1983,8 +1992,11 @@ describe("TiptapComposer slash commands", () => {
1983
1992
  '[data-agent-composer-slot="send-button"]',
1984
1993
  );
1985
1994
  expect(sendButton?.disabled).toBe(true);
1986
- expect(sendButton?.getAttribute("aria-label")).toMatch(/loading/i);
1995
+ expect(sendButton?.getAttribute("aria-label")).not.toMatch(/loading/i);
1987
1996
  expect(sendButton?.getAttribute("aria-label")).not.toMatch(/checking/i);
1997
+ expect(sendButton?.getAttribute("aria-busy")).toBeNull();
1998
+ expect(sendButton?.querySelector(".animate-spin")).toBeNull();
1999
+ expect(sendButton?.querySelector(".tabler-icon-arrow-up")).not.toBeNull();
1988
2000
  });
1989
2001
 
1990
2002
  it("keeps the editor focused after a successful submission", async () => {
@@ -2166,7 +2178,13 @@ describe("TiptapComposer slash commands", () => {
2166
2178
  resolveSubmit = resolve;
2167
2179
  });
2168
2180
  const onBeforeSubmit = vi.fn(() => preflight);
2169
- const onSubmit = vi.fn(() => submission);
2181
+ const onSubmissionPendingChange = vi.fn();
2182
+ const onSubmit = vi.fn<NonNullable<TiptapComposerProps["onSubmit"]>>(
2183
+ async (_text, _references, _attachments, options) => {
2184
+ await submission;
2185
+ options?.onLocalSubmit?.();
2186
+ },
2187
+ );
2170
2188
  const focusRef = React.createRef<TiptapComposerHandle>();
2171
2189
 
2172
2190
  function Harness() {
@@ -2180,6 +2198,7 @@ describe("TiptapComposer slash commands", () => {
2180
2198
  React.createElement(TiptapComposer, {
2181
2199
  focusRef,
2182
2200
  onBeforeSubmit,
2201
+ onSubmissionPendingChange,
2183
2202
  onSubmit,
2184
2203
  clearOnSubmitImmediately: true,
2185
2204
  includeDefaultSlashSkills: false,
@@ -2209,6 +2228,7 @@ describe("TiptapComposer slash commands", () => {
2209
2228
 
2210
2229
  expect(editor.textContent).toBe("");
2211
2230
  expect(onSubmit).not.toHaveBeenCalled();
2231
+ expect(onSubmissionPendingChange).toHaveBeenLastCalledWith(true);
2212
2232
  act(() => focusRef.current?.setText("follow-up prompt"));
2213
2233
 
2214
2234
  await act(async () => {
@@ -2225,6 +2245,8 @@ describe("TiptapComposer slash commands", () => {
2225
2245
  );
2226
2246
  expect(editor.textContent).toBe("follow-up prompt");
2227
2247
  await act(async () => resolveSubmit());
2248
+ expect(editor.textContent).toBe("follow-up prompt");
2249
+ expect(onSubmissionPendingChange).toHaveBeenLastCalledWith(false);
2228
2250
  });
2229
2251
 
2230
2252
  it("restores a prompt when async preflight declines it", async () => {
@@ -2337,10 +2359,11 @@ describe("TiptapComposer slash commands", () => {
2337
2359
 
2338
2360
  it("clears immediately while submitting and restores the draft on failure", async () => {
2339
2361
  let rejectSubmit!: (error: Error) => void;
2340
- const onSubmit = vi.fn(
2341
- () =>
2362
+ const onSubmit = vi.fn<NonNullable<TiptapComposerProps["onSubmit"]>>(
2363
+ (_text, _references, _attachments, options) =>
2342
2364
  new Promise<void>((_resolve, reject) => {
2343
2365
  rejectSubmit = reject;
2366
+ options?.onLocalSubmit?.();
2344
2367
  }),
2345
2368
  );
2346
2369
  const focusRef = React.createRef<TiptapComposerHandle>();
@@ -2384,6 +2407,7 @@ describe("TiptapComposer slash commands", () => {
2384
2407
 
2385
2408
  expect(onSubmit).toHaveBeenCalledOnce();
2386
2409
  expect(editor.textContent).toBe("");
2410
+ act(() => focusRef.current?.setText("follow-up prompt"));
2387
2411
 
2388
2412
  await act(async () => {
2389
2413
  rejectSubmit(new Error("network unavailable"));
@@ -2391,7 +2415,17 @@ describe("TiptapComposer slash commands", () => {
2391
2415
  await Promise.resolve();
2392
2416
  });
2393
2417
 
2394
- expect(editor.textContent).toBe("send this once");
2418
+ expect(
2419
+ [...editor.querySelectorAll("p")].map(
2420
+ (paragraph) => paragraph.textContent,
2421
+ ),
2422
+ ).toEqual(["send this once", "follow-up prompt"]);
2423
+ expect(localStorage.getItem(getComposerDraftKey())).toContain(
2424
+ "send this once",
2425
+ );
2426
+ expect(localStorage.getItem(getComposerDraftKey())).toContain(
2427
+ "follow-up prompt",
2428
+ );
2395
2429
  });
2396
2430
 
2397
2431
  it("restores the persisted draft when an immediate submit fails after unmount", async () => {
@@ -14,9 +14,9 @@ import {
14
14
  IconPencil,
15
15
  IconPlugConnected,
16
16
  IconHelpCircle,
17
- IconRefresh,
18
17
  IconAlertCircle,
19
18
  IconLoader2,
19
+ IconRefresh,
20
20
  } from "@tabler/icons-react";
21
21
  import Placeholder from "@tiptap/extension-placeholder";
22
22
  import { TextSelection } from "@tiptap/pm/state";
@@ -888,6 +888,8 @@ export interface TiptapComposerProps {
888
888
  contextControlsDisabled?: boolean;
889
889
  /** Prevent submission without making the editable surface lose focus. */
890
890
  submissionDisabled?: boolean;
891
+ /** Disable only the send control while the submission is being accepted. */
892
+ sendButtonDisabled?: boolean;
891
893
  /** Prevent submission while a host request is in flight. */
892
894
  submitting?: boolean;
893
895
  /** Override the generic document attachment cap for a multipart host. */
@@ -916,6 +918,7 @@ export interface TiptapComposerProps {
916
918
  ) => void | Promise<void>;
917
919
  /** Return false to stop a submit before it enters the chat runtime. */
918
920
  onBeforeSubmit?: () => boolean | Promise<boolean>;
921
+ onSubmissionPendingChange?: (pending: boolean) => void;
919
922
  /** Scope where a failed submission should be recovered after the host forks. */
920
923
  getSubmitFailureDraftScope?: () => string | null;
921
924
  /**
@@ -2381,7 +2384,7 @@ function ModelSelector({
2381
2384
  {showProviderLabels && (
2382
2385
  <div className="group flex items-center px-2 py-1">
2383
2386
  <span className="shrink-0 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
2384
- {group.label}
2387
+ {group.label.replace(/ · Builder\.io$/i, "")}
2385
2388
  </span>
2386
2389
  {!group.configured && statusLabel && (
2387
2390
  <button
@@ -2547,6 +2550,7 @@ export function TiptapComposer({
2547
2550
  disabled = false,
2548
2551
  contextControlsDisabled = false,
2549
2552
  submissionDisabled = false,
2553
+ sendButtonDisabled = false,
2550
2554
  submitting = false,
2551
2555
  maxDocumentAttachmentBytes = MAX_DOCUMENT_ATTACHMENT_BYTES,
2552
2556
  documentAttachmentLimitLabel = "PDFs",
@@ -2558,6 +2562,7 @@ export function TiptapComposer({
2558
2562
  initialTextKey,
2559
2563
  onSubmit,
2560
2564
  onBeforeSubmit,
2565
+ onSubmissionPendingChange,
2561
2566
  getSubmitFailureDraftScope,
2562
2567
  clearOnSubmit = true,
2563
2568
  clearOnSubmitImmediately = false,
@@ -2602,7 +2607,6 @@ export function TiptapComposer({
2602
2607
  draftScope,
2603
2608
  contextItems: providedContextItems,
2604
2609
  onRemoveContextItem,
2605
- onInspectContextItem,
2606
2610
  onRetryContextItem,
2607
2611
  contextMenuItems,
2608
2612
  plusMenuMode = "full",
@@ -4243,7 +4247,6 @@ export function TiptapComposer({
4243
4247
  slotReferencesRef.current.length > 0);
4244
4248
  const canPreserveFollowUp =
4245
4249
  preserveFollowUp &&
4246
- clearedBeforePreflight &&
4247
4250
  !isForkRecovery &&
4248
4251
  hasFollowUp &&
4249
4252
  (currentDraft === null ||
@@ -4330,6 +4333,7 @@ export function TiptapComposer({
4330
4333
 
4331
4334
  if (onBeforeSubmit) {
4332
4335
  submitInFlightRef.current = true;
4336
+ onSubmissionPendingChange?.(true);
4333
4337
  try {
4334
4338
  const shouldSubmit = await onBeforeSubmit();
4335
4339
  if (!shouldSubmit) {
@@ -4351,6 +4355,7 @@ export function TiptapComposer({
4351
4355
  return false;
4352
4356
  } finally {
4353
4357
  submitInFlightRef.current = false;
4358
+ onSubmissionPendingChange?.(false);
4354
4359
  }
4355
4360
  }
4356
4361
  if (
@@ -4498,6 +4503,7 @@ export function TiptapComposer({
4498
4503
  submitInFlightRef.current = true;
4499
4504
  let locallySubmitted = false;
4500
4505
  let settled = false;
4506
+ onSubmissionPendingChange?.(true);
4501
4507
  const clearSubmittedComposer = () => {
4502
4508
  if (clearOnSubmit) {
4503
4509
  clearComposerDraft(submittingDraftKey, submittingDraftSnapshot);
@@ -4575,7 +4581,7 @@ export function TiptapComposer({
4575
4581
  );
4576
4582
  } catch (error) {
4577
4583
  if (locallySubmitted) {
4578
- restoreSubmittedDraft();
4584
+ restoreSubmittedDraft(true);
4579
4585
  return true;
4580
4586
  }
4581
4587
  restoreSubmittedDraft(true);
@@ -4593,6 +4599,7 @@ export function TiptapComposer({
4593
4599
  } finally {
4594
4600
  settled = true;
4595
4601
  submitInFlightRef.current = false;
4602
+ onSubmissionPendingChange?.(false);
4596
4603
  }
4597
4604
 
4598
4605
  if (!isCurrentDraftScope()) {
@@ -4685,6 +4692,7 @@ export function TiptapComposer({
4685
4692
  clearOnSubmitImmediately,
4686
4693
  getSubmitFailureDraftScope,
4687
4694
  onBeforeSubmit,
4695
+ onSubmissionPendingChange,
4688
4696
  extractComposerPayload,
4689
4697
  syncComposerState,
4690
4698
  updateSlotReferences,
@@ -5142,7 +5150,7 @@ export function TiptapComposer({
5142
5150
  {slotReferences.map((ref) => (
5143
5151
  <span
5144
5152
  key={ref.slotKey}
5145
- className="inline-flex max-w-full items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1 text-[11px] font-medium text-foreground shadow-sm"
5153
+ className="inline-flex max-w-full items-center gap-1.5 rounded-md border border-border bg-background px-2 py-0.5 text-[11px] font-medium text-foreground shadow-sm"
5146
5154
  >
5147
5155
  <MentionItemMedia
5148
5156
  media={ref.media}
@@ -5176,79 +5184,80 @@ export function TiptapComposer({
5176
5184
  </span>
5177
5185
  ))}
5178
5186
  {contextItems.map((item) => (
5179
- <span
5180
- key={item.key}
5181
- data-context-key={item.key}
5182
- data-context-status={item.status ?? "ready"}
5183
- title={item.statusMessage}
5184
- data-state={
5185
- selectedContextItemKey === item.key ? "selected" : undefined
5186
- }
5187
- className={`inline-flex max-w-full items-center gap-1.5 rounded-md border px-2 py-1 text-[11px] font-medium text-foreground ${
5188
- selectedContextItemKey === item.key
5189
- ? "border-ring bg-accent ring-2 ring-ring/40"
5190
- : "border-border bg-muted/50"
5191
- }`}
5192
- >
5193
- {item.status === "pending" ? (
5194
- <IconLoader2
5195
- aria-label={t("agentChat.composer.contextPending", {
5196
- defaultValue: "Context pending",
5197
- })}
5198
- className="size-3 shrink-0 animate-spin motion-reduce:animate-none text-muted-foreground"
5199
- />
5200
- ) : item.status === "error" ? (
5201
- <IconAlertCircle
5202
- aria-label={t("agentChat.composer.contextError", {
5203
- defaultValue: "Context failed",
5204
- })}
5205
- className="size-3 shrink-0 text-destructive"
5206
- />
5207
- ) : (
5208
- <IconClipboardList className="h-3 w-3 shrink-0 text-muted-foreground" />
5209
- )}
5210
- {onInspectContextItem ? (
5211
- <button
5212
- type="button"
5213
- onClick={() => onInspectContextItem(item.key)}
5214
- className="min-w-0 truncate hover:underline"
5215
- >
5216
- {item.title}
5217
- </button>
5218
- ) : (
5187
+ <React.Fragment key={item.key}>
5188
+ <span
5189
+ data-context-key={item.key}
5190
+ data-context-status={item.status ?? "ready"}
5191
+ title={item.statusMessage}
5192
+ data-state={
5193
+ selectedContextItemKey === item.key ? "selected" : undefined
5194
+ }
5195
+ className={`inline-flex max-w-full items-center gap-1.5 rounded-md border px-2 py-0.5 text-[11px] font-medium text-foreground ${
5196
+ selectedContextItemKey === item.key
5197
+ ? "border-ring bg-accent ring-2 ring-ring/40"
5198
+ : "border-border bg-muted/50"
5199
+ }`}
5200
+ >
5201
+ {item.status === "pending" ? (
5202
+ <IconLoader2
5203
+ aria-label={t("agentChat.composer.contextPending", {
5204
+ defaultValue: "Context pending",
5205
+ })}
5206
+ className="size-3 shrink-0 animate-spin motion-reduce:animate-none text-muted-foreground"
5207
+ />
5208
+ ) : item.status === "error" ? (
5209
+ <IconAlertCircle
5210
+ aria-label={t("agentChat.composer.contextError", {
5211
+ defaultValue: "Context failed",
5212
+ })}
5213
+ className="size-3 shrink-0 text-destructive"
5214
+ />
5215
+ ) : (
5216
+ <IconClipboardList className="h-3 w-3 shrink-0 text-muted-foreground" />
5217
+ )}
5219
5218
  <span className="min-w-0 truncate">{item.title}</span>
5220
- )}
5219
+ {onRemoveContextItem && item.removable !== false ? (
5220
+ <button
5221
+ type="button"
5222
+ onClick={() => {
5223
+ selectedContextItemKeyRef.current = null;
5224
+ setSelectedContextItemKey(null);
5225
+ onRemoveContextItem?.(item.key);
5226
+ }}
5227
+ aria-label={t("agentChat.composer.removeContext", {
5228
+ defaultValue: "Remove {{name}} context",
5229
+ name: item.title,
5230
+ })}
5231
+ className="ms-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
5232
+ >
5233
+ <IconX className="h-3 w-3" />
5234
+ </button>
5235
+ ) : null}
5236
+ </span>
5221
5237
  {item.status === "error" && onRetryContextItem ? (
5222
- <button
5223
- type="button"
5224
- onClick={() => onRetryContextItem(item.key)}
5225
- aria-label={t("agentChat.composer.retryContext", {
5226
- defaultValue: "Retry {{name}} context",
5227
- name: item.title,
5228
- })}
5229
- className="flex size-4 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
5230
- >
5231
- <IconRefresh className="size-3" />
5232
- </button>
5233
- ) : null}
5234
- {onRemoveContextItem && item.removable !== false ? (
5235
- <button
5236
- type="button"
5237
- onClick={() => {
5238
- selectedContextItemKeyRef.current = null;
5239
- setSelectedContextItemKey(null);
5240
- onRemoveContextItem?.(item.key);
5241
- }}
5242
- aria-label={t("agentChat.composer.removeContext", {
5243
- defaultValue: "Remove {{name}} context",
5244
- name: item.title,
5245
- })}
5246
- className="ms-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
5247
- >
5248
- <IconX className="h-3 w-3" />
5249
- </button>
5238
+ <Tooltip>
5239
+ <TooltipTrigger asChild>
5240
+ <button
5241
+ type="button"
5242
+ onClick={() => onRetryContextItem(item.key)}
5243
+ aria-label={t("agentChat.composer.retryContext", {
5244
+ defaultValue: "Retry {{name}} context",
5245
+ name: item.title,
5246
+ })}
5247
+ className="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
5248
+ >
5249
+ <IconRefresh aria-hidden="true" className="size-3.5" />
5250
+ </button>
5251
+ </TooltipTrigger>
5252
+ <TooltipContent>
5253
+ {t("agentChat.composer.retryContext", {
5254
+ defaultValue: "Retry {{name}} context",
5255
+ name: item.title,
5256
+ })}
5257
+ </TooltipContent>
5258
+ </Tooltip>
5250
5259
  ) : null}
5251
- </span>
5260
+ </React.Fragment>
5252
5261
  ))}
5253
5262
  </div>
5254
5263
  )}
@@ -5391,24 +5400,15 @@ export function TiptapComposer({
5391
5400
  <button
5392
5401
  type="button"
5393
5402
  onClick={() => void submitComposer("immediate")}
5394
- disabled={!canSend}
5395
- aria-label={
5396
- submitting ? t("common.loading") : sendButtonTooltip
5397
- }
5398
- aria-busy={submitting || undefined}
5403
+ disabled={!canSend || sendButtonDisabled}
5404
+ aria-label={sendButtonTooltip}
5399
5405
  data-agent-composer-slot="send-button"
5400
5406
  className="agent-composer-send-button shrink-0 flex h-7 w-7 items-center justify-center rounded-full bg-primary text-primary-foreground hover:opacity-90 transition-[opacity,transform] duration-150 active:scale-[0.97] disabled:opacity-30 disabled:cursor-not-allowed"
5401
5407
  >
5402
- {submitting ? (
5403
- <IconLoader2 className="size-3.5 animate-spin motion-reduce:animate-none" />
5404
- ) : (
5405
- <IconArrowUp className="h-3.5 w-3.5" />
5406
- )}
5408
+ <IconArrowUp className="h-3.5 w-3.5" />
5407
5409
  </button>
5408
5410
  </TooltipTrigger>
5409
- <TooltipContent>
5410
- {submitting ? t("common.loading") : sendButtonTooltip}
5411
- </TooltipContent>
5411
+ <TooltipContent>{sendButtonTooltip}</TooltipContent>
5412
5412
  </Tooltip>
5413
5413
  )}
5414
5414
  </>
@@ -0,0 +1,15 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ const source = readFileSync(new URL("./sonner.tsx", import.meta.url), "utf8");
6
+
7
+ describe("Sonner toast layout", () => {
8
+ it("allows toast content to shrink beside an action", () => {
9
+ expect(source).toContain("group-[.toast]:!min-w-0");
10
+ expect(source).not.toContain(
11
+ "group-[.toast]:!min-w-[min(16rem,calc(100vw_-_14rem))]",
12
+ );
13
+ expect(source).toContain("group-[.toast]:!shrink-0");
14
+ });
15
+ });
package/src/ui/sonner.tsx CHANGED
@@ -8,7 +8,7 @@ type ToasterProps = React.ComponentProps<typeof Sonner>;
8
8
  const toastWidthClasses =
9
9
  "group-[.toaster]:!w-[var(--width)] group-[.toaster]:!min-w-[min(20rem,calc(100vw_-_2rem))] group-[.toaster]:!max-w-[var(--width)] group-[.toaster]:!gap-3 group-[.toaster]:!break-normal";
10
10
  const toastContentClasses =
11
- "group-[.toast]:!min-w-[min(16rem,calc(100vw_-_14rem))] group-[.toast]:!flex-1 group-[.toast]:!basis-auto group-[.toast]:break-words";
11
+ "group-[.toast]:!min-w-0 group-[.toast]:!flex-1 group-[.toast]:!basis-auto group-[.toast]:break-words";
12
12
  const toastButtonClasses =
13
13
  "group-[.toast]:!shrink-0 group-[.toast]:!whitespace-nowrap";
14
14