@agent-native/toolkit 0.203.0-nightly-20261007093643 → 0.203.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 (34) hide show
  1. package/dist/agentkit.d.ts +2 -1
  2. package/dist/agentkit.d.ts.map +1 -1
  3. package/dist/agentkit.js +2 -1
  4. package/dist/agentkit.js.map +1 -1
  5. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  6. package/dist/app/agentkit/react/components.js +5 -5
  7. package/dist/app/agentkit/react/components.js.map +1 -1
  8. package/dist/app/agentkit/react/context.d.ts +1 -0
  9. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  10. package/dist/app/agentkit/react/context.js +1 -0
  11. package/dist/app/agentkit/react/context.js.map +1 -1
  12. package/dist/app/agentkit/react/styles.css +6 -1
  13. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  14. package/dist/app/chat/AgentKitAssistantChat.js +2 -1
  15. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  16. package/dist/composer/PromptComposer.d.ts +1 -0
  17. package/dist/composer/PromptComposer.d.ts.map +1 -1
  18. package/dist/composer/PromptComposer.js +13 -1
  19. package/dist/composer/PromptComposer.js.map +1 -1
  20. package/dist/composer/TiptapComposer.d.ts +2 -2
  21. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  22. package/dist/composer/TiptapComposer.js +9 -6
  23. package/dist/composer/TiptapComposer.js.map +1 -1
  24. package/package.json +4 -4
  25. package/src/agentkit.ts +2 -0
  26. package/src/app/agentkit/react/components.interaction.spec.tsx +68 -0
  27. package/src/app/agentkit/react/components.tsx +11 -4
  28. package/src/app/agentkit/react/context.tsx +2 -0
  29. package/src/app/agentkit/react/styles.css +6 -1
  30. package/src/app/chat/AgentKitAssistantChat.tsx +4 -1
  31. package/src/composer/PromptComposer.spec.ts +80 -0
  32. package/src/composer/PromptComposer.tsx +15 -0
  33. package/src/composer/TiptapComposer.spec.ts +23 -0
  34. package/src/composer/TiptapComposer.tsx +18 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/toolkit",
3
- "version": "0.203.0-nightly-20261007093643",
3
+ "version": "0.203.0",
4
4
  "description": "Reusable app-building UI and helpers for Agent-Native apps.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -673,7 +673,7 @@
673
673
  "y-protocols": "^1.0.7",
674
674
  "yjs": "^13.6.33",
675
675
  "zod": "^4.3.6",
676
- "@agent-native/agentkit": "^0.203.0-nightly-20261007093643"
676
+ "@agent-native/agentkit": "^0.203.0"
677
677
  },
678
678
  "devDependencies": {
679
679
  "@excalidraw/excalidraw": "0.18.1",
@@ -697,10 +697,10 @@
697
697
  "typescript-7": "npm:typescript@^7.0.2",
698
698
  "vite": "8.1.0",
699
699
  "vitest": "^4.1.5",
700
- "@agent-native/core": "^0.203.0-nightly-20261007093643"
700
+ "@agent-native/core": "^0.203.0"
701
701
  },
702
702
  "peerDependencies": {
703
- "@agent-native/core": "0.203.0-nightly-20261007093643",
703
+ "@agent-native/core": ">=0.196.0",
704
704
  "@excalidraw/excalidraw": ">=0.18",
705
705
  "@excalidraw/mermaid-to-excalidraw": ">=2",
706
706
  "@tanstack/react-query": "^5.99.2",
package/src/agentkit.ts CHANGED
@@ -3,6 +3,7 @@ export {
3
3
  AgentSuggestionBar,
4
4
  agentSuggestionPrompt,
5
5
  } from "./composer/AgentSuggestionBar.js";
6
+ export { formatAttachmentError } from "./composer/attachment-accept.js";
6
7
  export { MessageQueueDrawer } from "./composer/MessageQueueDrawer.js";
7
8
  export {
8
9
  ComposerContextMenu,
@@ -21,6 +22,7 @@ export {
21
22
  type ComposerContextPickerFooterAction,
22
23
  } from "./composer/ComposerContextMenu.js";
23
24
  export {
25
+ createChatAttachmentAdapter,
24
26
  PromptComposer,
25
27
  type PromptComposerFile,
26
28
  type PromptComposerProps,
@@ -870,6 +870,74 @@ describe("AgentKitChat interactions", () => {
870
870
  }
871
871
  });
872
872
 
873
+ it("attaches dropped SVGs and names rejected drops without the accept list", async () => {
874
+ const transport: AgentTransport = {
875
+ capabilities: { uploads: true },
876
+ async startRun() {
877
+ return { runId: "run-svg" };
878
+ },
879
+ async *subscribeToRun() {},
880
+ async cancelRun() {},
881
+ };
882
+ const client = new AgentKitClient({ transport });
883
+ const onAttachmentError = vi.fn();
884
+ const container = document.createElement("div");
885
+ document.body.appendChild(container);
886
+ const root = createRoot(container);
887
+ const actEnvironment = globalThis as typeof globalThis & {
888
+ IS_REACT_ACT_ENVIRONMENT?: boolean;
889
+ };
890
+ const previousActEnvironment = actEnvironment.IS_REACT_ACT_ENVIRONMENT;
891
+ actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
892
+ const dropOnChat = async (file: File) => {
893
+ const drop = new Event("drop", { bubbles: true, cancelable: true });
894
+ Object.defineProperty(drop, "dataTransfer", {
895
+ value: { types: ["Files"], files: [file], dropEffect: "none" },
896
+ });
897
+ await act(async () => {
898
+ container.querySelector(".agentkit-chat")?.dispatchEvent(drop);
899
+ await new Promise((resolve) => setTimeout(resolve, 0));
900
+ });
901
+ };
902
+
903
+ try {
904
+ await act(async () => {
905
+ root.render(
906
+ <AgentKitProvider controller={client} threadId="thread-svg">
907
+ <AgentKitChat composerProps={{ onAttachmentError }} />
908
+ </AgentKitProvider>,
909
+ );
910
+ await Promise.resolve();
911
+ });
912
+
913
+ await dropOnChat(
914
+ new File(['<svg xmlns="http://www.w3.org/2000/svg"/>'], "logo.svg", {
915
+ type: "image/svg+xml",
916
+ }),
917
+ );
918
+ expect(container.textContent).toContain("logo.svg");
919
+ expect(onAttachmentError).not.toHaveBeenCalled();
920
+
921
+ await dropOnChat(
922
+ new File(["zip"], "archive.zip", { type: "application/zip" }),
923
+ );
924
+ expect(onAttachmentError).toHaveBeenCalledWith(
925
+ "archive.zip: Could not add the dropped file. Try a different format.",
926
+ );
927
+ } finally {
928
+ await act(async () => {
929
+ root.unmount();
930
+ await Promise.resolve();
931
+ });
932
+ container.remove();
933
+ if (previousActEnvironment === undefined) {
934
+ delete actEnvironment.IS_REACT_ACT_ENVIRONMENT;
935
+ } else {
936
+ actEnvironment.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment;
937
+ }
938
+ }
939
+ });
940
+
873
941
  it("disables transcript file drops when the host disables uploads", async () => {
874
942
  const transport: AgentTransport = {
875
943
  capabilities: { uploads: true },
@@ -68,6 +68,8 @@ import {
68
68
  Surface,
69
69
  TextField,
70
70
  agentSuggestionPrompt,
71
+ createChatAttachmentAdapter,
72
+ formatAttachmentError,
71
73
  type PromptComposerFile,
72
74
  type PromptComposerProps,
73
75
  type TiptapComposerHandle,
@@ -4065,6 +4067,10 @@ export function AgentKitComposer({
4065
4067
  }, []);
4066
4068
  const [uncontrolledMode, setUncontrolledMode] = useState(defaultMode);
4067
4069
  const [attachmentError, setAttachmentError] = useState<string | null>(null);
4070
+ const composerAttachmentAdapter = useMemo(
4071
+ () => attachmentAdapter ?? createChatAttachmentAdapter(),
4072
+ [attachmentAdapter],
4073
+ );
4068
4074
  const executionMode = mode ?? uncontrolledMode;
4069
4075
  const active = hasActiveRuns(thread);
4070
4076
  const composerInitialText = editingMessage
@@ -4095,12 +4101,13 @@ export function AgentKitComposer({
4095
4101
  await composer.addAttachment(file);
4096
4102
  setAttachmentError(null);
4097
4103
  } catch (error) {
4098
- const message = error instanceof Error ? error.message : labels.error;
4099
- reportAttachmentError(message);
4104
+ reportAttachmentError(
4105
+ `${file.name}: ${formatAttachmentError(error, labels.dropFileFailed)}`,
4106
+ );
4100
4107
  throw error;
4101
4108
  }
4102
4109
  },
4103
- [labels.error, reportAttachmentError],
4110
+ [labels.dropFileFailed, labels.error, reportAttachmentError],
4104
4111
  );
4105
4112
  useEffect(() => {
4106
4113
  const receiveAttachments = (event: Event) => {
@@ -4494,7 +4501,7 @@ export function AgentKitComposer({
4494
4501
  onInspectContextItem={onInspectContextItem}
4495
4502
  onRetryContextItem={onRetryContextItem}
4496
4503
  contextMenuItems={contextMenuItems}
4497
- attachmentAdapter={attachmentAdapter}
4504
+ attachmentAdapter={composerAttachmentAdapter}
4498
4505
  inlineTextAttachments={inlineTextAttachments}
4499
4506
  rootClassName="agentkit-composer"
4500
4507
  className={
@@ -274,6 +274,7 @@ export interface AgentKitLabels {
274
274
  imagePreview: string;
275
275
  closePreview: string;
276
276
  dropFilesToAttach: string;
277
+ dropFileFailed: string;
277
278
  scrollToBottom: string;
278
279
  formatTimestamp?: (createdAt: string) => string;
279
280
  error: string;
@@ -384,6 +385,7 @@ export const defaultAgentKitLabels: AgentKitLabels = {
384
385
  imagePreview: "Image preview",
385
386
  closePreview: "Close preview",
386
387
  dropFilesToAttach: "Drop files to attach",
388
+ dropFileFailed: "Could not add the dropped file. Try a different format.",
387
389
  scrollToBottom: "Scroll to bottom",
388
390
  error: "Something went wrong",
389
391
  renderError: "This content couldn’t be displayed.",
@@ -1715,11 +1715,16 @@ button.agentkit-object {
1715
1715
 
1716
1716
  .agentkit-composer-error {
1717
1717
  display: flex;
1718
- align-items: center;
1718
+ align-items: flex-start;
1719
1719
  gap: 0.375rem;
1720
1720
  padding: 0.375rem 0.625rem 0;
1721
1721
  }
1722
1722
 
1723
+ .agentkit-composer-error > span {
1724
+ min-inline-size: 0;
1725
+ overflow-wrap: anywhere;
1726
+ }
1727
+
1723
1728
  .agentkit-file {
1724
1729
  display: inline-flex;
1725
1730
  align-items: center;
@@ -887,6 +887,7 @@ export const AgentKitAssistantChat = forwardRef<
887
887
  imagePreview: t("agentChat.composer.imagePreview"),
888
888
  closePreview: t("agentChat.composer.closePreview"),
889
889
  dropFilesToAttach: t("agentChat.composer.dropToAttach"),
890
+ dropFileFailed: t("agentChat.composer.droppedFileError"),
890
891
  scrollToBottom: t("agentChat.composer.scrollToBottom"),
891
892
  formatTimestamp: (createdAt: string) => {
892
893
  const date = new Date(createdAt);
@@ -4239,7 +4240,9 @@ function AgentKitComposerSurface({
4239
4240
  className="mx-3 mb-1.5 flex shrink-0 items-start gap-2 rounded-md border border-border bg-muted/70 px-3 py-2 text-xs text-foreground shadow-sm"
4240
4241
  >
4241
4242
  <IconAlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
4242
- <span className="flex-1 leading-snug">{composerError}</span>
4243
+ <span className="min-w-0 flex-1 break-words leading-snug">
4244
+ {composerError}
4245
+ </span>
4243
4246
  <button
4244
4247
  type="button"
4245
4248
  aria-label={t("agentChat.common.dismissError")}
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
6
 
7
7
  import {
8
8
  buildPromptComposerSubmission,
9
+ createChatAttachmentAdapter,
9
10
  PromptComposer,
10
11
  resolveComposerModelStatusChecksEnabled,
11
12
  shouldGateComposerForEngine,
@@ -234,3 +235,82 @@ describe("PromptComposer scoped runtime", () => {
234
235
  expect(attachedFiles).toHaveLength(0);
235
236
  });
236
237
  });
238
+
239
+ describe("PromptComposer attachment policy", () => {
240
+ it("keeps SVGs out of standalone prompts while chat stages them as documents", async () => {
241
+ await act(async () => {
242
+ root.render(
243
+ React.createElement(PromptComposer, {
244
+ attachmentsEnabled: true,
245
+ includeDefaultSlashSkills: false,
246
+ onSubmit: () => {},
247
+ plusMenuMode: "upload-only",
248
+ showModelSelector: false,
249
+ voiceEnabled: false,
250
+ }),
251
+ );
252
+ await new Promise((resolve) => setTimeout(resolve, 0));
253
+ });
254
+ const standaloneAccept = container
255
+ .querySelector<HTMLInputElement>('input[type="file"]')
256
+ ?.getAttribute("accept")
257
+ ?.split(",");
258
+ expect(standaloneAccept).toContain(".pdf");
259
+ expect(standaloneAccept).not.toContain(".svg");
260
+
261
+ const chatAdapter = createChatAttachmentAdapter();
262
+ expect(chatAdapter.accept.split(",")).toEqual(
263
+ expect.arrayContaining(["image/svg+xml", ".svg", ".png", ".pdf"]),
264
+ );
265
+ await expect(
266
+ chatAdapter.add({
267
+ file: new File(["<svg/>"], "logo.svg", { type: "image/svg+xml" }),
268
+ }),
269
+ ).resolves.toMatchObject({
270
+ type: "document",
271
+ name: "logo.svg",
272
+ contentType: "image/svg+xml",
273
+ });
274
+ await expect(
275
+ chatAdapter.add({
276
+ file: new File(["png"], "logo.png", { type: "image/png" }),
277
+ }),
278
+ ).resolves.toMatchObject({ type: "image", name: "logo.png" });
279
+ });
280
+
281
+ it("shows why a file was rejected when the host passes no error handler", async () => {
282
+ await act(async () => {
283
+ root.render(
284
+ React.createElement(PromptComposer, {
285
+ attachmentsEnabled: true,
286
+ includeDefaultSlashSkills: false,
287
+ onSubmit: () => {},
288
+ plusMenuMode: "upload-only",
289
+ showModelSelector: false,
290
+ voiceEnabled: false,
291
+ }),
292
+ );
293
+ await new Promise((resolve) => setTimeout(resolve, 0));
294
+ });
295
+ const editor = container.querySelector<HTMLElement>(
296
+ ".agent-composer-prosemirror",
297
+ );
298
+ expect(editor).not.toBeNull();
299
+ const pasteEvent = new Event("paste", { bubbles: true, cancelable: true });
300
+ Object.defineProperty(pasteEvent, "clipboardData", {
301
+ value: {
302
+ files: [new File(["<svg/>"], "logo.svg", { type: "image/svg+xml" })],
303
+ getData: () => "",
304
+ },
305
+ });
306
+
307
+ await act(async () => {
308
+ editor?.dispatchEvent(pasteEvent);
309
+ await new Promise((resolve) => setTimeout(resolve, 20));
310
+ });
311
+
312
+ expect(container.querySelector('[role="alert"]')?.textContent).toBe(
313
+ "Could not attach the pasted image. Try a different format.",
314
+ );
315
+ });
316
+ });
@@ -33,6 +33,7 @@ import { cn } from "../utils.js";
33
33
  import { AgentComposerFrame } from "./AgentComposerFrame.js";
34
34
  import { IMAGE_ATTACHMENT_ACCEPT } from "./attachment-accept.js";
35
35
  import {
36
+ CHAT_DOCUMENT_ATTACHMENT_ACCEPT,
36
37
  PROMPT_DOCUMENT_ATTACHMENT_ACCEPT,
37
38
  TextAttachmentAdapter,
38
39
  } from "./attachment-accept.js";
@@ -314,6 +315,20 @@ class RasterImageAttachmentAdapter extends SimpleImageAttachmentAdapter {
314
315
  }
315
316
  }
316
317
 
318
+ // SVGs never match the raster adapter, so agent chat stages them as reference
319
+ // documents rather than images a model would try to read as pixels.
320
+ class ChatDocumentAttachmentAdapter extends BinaryDocumentAttachmentAdapter {
321
+ public accept = CHAT_DOCUMENT_ATTACHMENT_ACCEPT;
322
+ }
323
+
324
+ export function createChatAttachmentAdapter(): AttachmentAdapter {
325
+ return new CompositeAttachmentAdapter([
326
+ new RasterImageAttachmentAdapter(),
327
+ new ChatDocumentAttachmentAdapter(),
328
+ new TextAttachmentAdapter(),
329
+ ]);
330
+ }
331
+
317
332
  function isInlineableTextFile(file: File): boolean {
318
333
  if (file.type.startsWith("text/")) return true;
319
334
  if (
@@ -2554,6 +2554,29 @@ describe("createTiptapComposerExtensions", () => {
2554
2554
  });
2555
2555
  });
2556
2556
 
2557
+ it("names a rejected image drop by the file the user dropped", async () => {
2558
+ const file = new File(["<svg/>"], "logo.svg", { type: "image/svg+xml" });
2559
+ const added: File[] = [];
2560
+ const onError = vi.fn();
2561
+ handleComposerFileDrop({
2562
+ event: {
2563
+ dataTransfer: { files: [file] },
2564
+ preventDefault: () => {},
2565
+ stopPropagation: () => {},
2566
+ } as unknown as DragEvent,
2567
+ addAttachment: async (attachment) => {
2568
+ added.push(attachment);
2569
+ throw new Error("File type image/svg+xml is not accepted.");
2570
+ },
2571
+ onError,
2572
+ });
2573
+
2574
+ await vi.waitFor(() => {
2575
+ expect(onError).toHaveBeenCalledWith(expect.any(Error), "logo.svg");
2576
+ });
2577
+ expect(added[0]?.name).toMatch(/^\d+-[a-z0-9]+-logo\.svg$/);
2578
+ });
2579
+
2557
2580
  it("caps the model picker height without forcing empty vertical space", () => {
2558
2581
  expect(MODEL_SELECTOR_POPOVER_STYLE).toMatchObject({
2559
2582
  fontSize: 13,
@@ -770,16 +770,15 @@ export function handleComposerFileDrop(options: {
770
770
  options.event.preventDefault();
771
771
  options.event.stopPropagation();
772
772
  if (options.attachmentsEnabled === false) return true;
773
- const attachments = droppedFiles.map(uniquifyComposerImageFile);
774
773
  let errorReported = false;
775
774
  void Promise.all(
776
- attachments.map(async (file) => {
775
+ droppedFiles.map(async (droppedFile) => {
777
776
  try {
778
- await options.addAttachment(file);
777
+ await options.addAttachment(uniquifyComposerImageFile(droppedFile));
779
778
  } catch (error) {
780
779
  if (errorReported) return;
781
780
  errorReported = true;
782
- options.onError?.(error, file.name);
781
+ options.onError?.(error, droppedFile.name);
783
782
  }
784
783
  }),
785
784
  );
@@ -1223,8 +1222,8 @@ export interface TiptapComposerProps {
1223
1222
  interceptBuildRequestsForBuilder?: boolean;
1224
1223
  /**
1225
1224
  * Called when a drag-drop or paste attachment fails (e.g. unsupported format,
1226
- * size cap). Use this to surface a visible error in the parent chat surface
1227
- * rather than silently swallowing the problem.
1225
+ * size cap) so the host can show it in its own surface. Without it, the
1226
+ * composer shows the message inline.
1228
1227
  */
1229
1228
  onAttachmentError?: (message: string) => void;
1230
1229
  }
@@ -2873,8 +2872,10 @@ export function TiptapComposer({
2873
2872
  // Refs for values accessed in handleKeyDown (ProseMirror doesn't re-bind)
2874
2873
  const popoverStateRef = useRef<PopoverState>(null);
2875
2874
  const composingRef = useRef(false);
2875
+ const [attachmentError, setAttachmentError] = useState<string | null>(null);
2876
2876
  const onAttachmentErrorRef = useRef(onAttachmentError);
2877
- onAttachmentErrorRef.current = onAttachmentError;
2877
+ // Many standalone prompts pass no handler; a rejected file must still say so.
2878
+ onAttachmentErrorRef.current = onAttachmentError ?? setAttachmentError;
2878
2879
  const onSubmitRef = useRef(onSubmit);
2879
2880
  onSubmitRef.current = onSubmit;
2880
2881
  const execModeRef = useRef(execMode);
@@ -3101,6 +3102,7 @@ export function TiptapComposer({
3101
3102
  }, [cleanStaleAttachments, composerRuntime]);
3102
3103
  const addAttachmentForCurrentScope = useCallback(
3103
3104
  (file: File) => {
3105
+ setAttachmentError(null);
3104
3106
  const scopeGeneration = draftScopeGenerationRef.current;
3105
3107
  const submissionBarrier = attachmentSubmissionBarrierRef.current;
3106
3108
  let resolveOperation!: () => void;
@@ -3297,6 +3299,7 @@ export function TiptapComposer({
3297
3299
  // Drive the send button's enabled state from the actual editor contents;
3298
3300
  // the composer runtime is only synced on submit, so its isEmpty lags.
3299
3301
  setEditorHasText(composerDocumentHasContent(ed.state.doc));
3302
+ setAttachmentError(null);
3300
3303
  onTextChangeRef.current?.(ed.getText({ blockSeparator: "\n" }).trim());
3301
3304
  setReferenceRevision((revision) => revision + 1);
3302
3305
 
@@ -5871,6 +5874,14 @@ export function TiptapComposer({
5871
5874
  {contextSubmissionError}
5872
5875
  </p>
5873
5876
  ) : null}
5877
+ {attachmentError ? (
5878
+ <p
5879
+ role="alert"
5880
+ className="break-words px-2.5 pt-2 text-xs text-destructive"
5881
+ >
5882
+ {attachmentError}
5883
+ </p>
5884
+ ) : null}
5874
5885
  <div
5875
5886
  data-agent-composer-variant={layoutVariant}
5876
5887
  data-agent-composer-slot="editor-wrap"