@agent-native/toolkit 0.22.0 → 0.22.1-nightly-20260926172134

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 (39) hide show
  1. package/dist/composer/ComposerPlusMenu.d.ts +2 -1
  2. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -1
  3. package/dist/composer/ComposerPlusMenu.js +24 -18
  4. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  5. package/dist/composer/PromptComposer.context.spec.js +40 -25
  6. package/dist/composer/PromptComposer.context.spec.js.map +1 -1
  7. package/dist/composer/PromptComposer.d.ts +15 -7
  8. package/dist/composer/PromptComposer.d.ts.map +1 -1
  9. package/dist/composer/PromptComposer.js +59 -30
  10. package/dist/composer/PromptComposer.js.map +1 -1
  11. package/dist/composer/PromptComposer.spec.js +29 -18
  12. package/dist/composer/PromptComposer.spec.js.map +1 -1
  13. package/dist/composer/TiptapComposer.d.ts +5 -2
  14. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  15. package/dist/composer/TiptapComposer.js +35 -5
  16. package/dist/composer/TiptapComposer.js.map +1 -1
  17. package/dist/composer/TiptapComposer.spec.js +96 -0
  18. package/dist/composer/TiptapComposer.spec.js.map +1 -1
  19. package/dist/composer/index.d.ts +1 -1
  20. package/dist/composer/index.d.ts.map +1 -1
  21. package/dist/composer/index.js +1 -1
  22. package/dist/composer/index.js.map +1 -1
  23. package/dist/composer/runtime-adapters.d.ts +3 -2
  24. package/dist/composer/runtime-adapters.d.ts.map +1 -1
  25. package/dist/composer/runtime-adapters.js +4 -1
  26. package/dist/composer/runtime-adapters.js.map +1 -1
  27. package/dist/icons/ResourceIconPicker.d.ts.map +1 -1
  28. package/dist/icons/ResourceIconPicker.js +6 -1
  29. package/dist/icons/ResourceIconPicker.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/composer/ComposerPlusMenu.tsx +25 -14
  32. package/src/composer/PromptComposer.context.spec.tsx +60 -34
  33. package/src/composer/PromptComposer.spec.ts +39 -20
  34. package/src/composer/PromptComposer.tsx +106 -46
  35. package/src/composer/TiptapComposer.spec.ts +130 -0
  36. package/src/composer/TiptapComposer.tsx +61 -26
  37. package/src/composer/index.ts +1 -0
  38. package/src/composer/runtime-adapters.tsx +13 -4
  39. package/src/icons/ResourceIconPicker.tsx +13 -2
@@ -1172,6 +1172,31 @@ describe("createTiptapComposerExtensions", () => {
1172
1172
  expect(added[0]?.name).toMatch(/^\d+-[a-z0-9]+-image\.png$/);
1173
1173
  });
1174
1174
 
1175
+ it("consumes but does not attach file drops while uploads are disabled", () => {
1176
+ const file = new File(["fake"], "image.png", { type: "image/png" });
1177
+ const added = vi.fn();
1178
+ let prevented = false;
1179
+ let stopped = false;
1180
+ const handled = handleComposerFileDrop({
1181
+ event: {
1182
+ dataTransfer: { files: [file] },
1183
+ preventDefault: () => {
1184
+ prevented = true;
1185
+ },
1186
+ stopPropagation: () => {
1187
+ stopped = true;
1188
+ },
1189
+ } as unknown as DragEvent,
1190
+ addAttachment: added,
1191
+ attachmentsEnabled: false,
1192
+ });
1193
+
1194
+ expect(handled).toBe(true);
1195
+ expect(prevented).toBe(true);
1196
+ expect(stopped).toBe(true);
1197
+ expect(added).not.toHaveBeenCalled();
1198
+ });
1199
+
1175
1200
  it("caps the model picker height without forcing empty vertical space", () => {
1176
1201
  expect(MODEL_SELECTOR_POPOVER_STYLE).toMatchObject({
1177
1202
  fontSize: 13,
@@ -1481,6 +1506,111 @@ describe("createTiptapComposerExtensions", () => {
1481
1506
  });
1482
1507
  });
1483
1508
 
1509
+ describe("TiptapComposer paste handling", () => {
1510
+ it("keeps text from a mixed text and image paste when uploads are disabled", async () => {
1511
+ const pastedText = "Keep this text";
1512
+
1513
+ function Harness() {
1514
+ const runtime = useLocalRuntime(emptyChatModelAdapter);
1515
+ return React.createElement(
1516
+ AssistantRuntimeProvider,
1517
+ { runtime },
1518
+ React.createElement(
1519
+ TooltipProvider,
1520
+ null,
1521
+ React.createElement(TiptapComposer, {
1522
+ attachmentsEnabled: false,
1523
+ includeDefaultSlashSkills: false,
1524
+ plusMenuMode: "hidden",
1525
+ providerConnectStatusEnabled: false,
1526
+ voiceEnabled: false,
1527
+ }),
1528
+ ),
1529
+ );
1530
+ }
1531
+
1532
+ await act(async () => {
1533
+ root.render(React.createElement(Harness));
1534
+ await new Promise((resolve) => setTimeout(resolve, 0));
1535
+ });
1536
+
1537
+ const editor = container.querySelector<HTMLElement>(
1538
+ ".agent-composer-prosemirror",
1539
+ );
1540
+ expect(editor).not.toBeNull();
1541
+ const clipboardData = {
1542
+ files: [new File(["image"], "image.png", { type: "image/png" })],
1543
+ getData: (type: string) => (type === "text/plain" ? pastedText : ""),
1544
+ };
1545
+ const pasteEvent = new Event("paste", {
1546
+ bubbles: true,
1547
+ cancelable: true,
1548
+ });
1549
+ Object.defineProperty(pasteEvent, "clipboardData", {
1550
+ value: clipboardData,
1551
+ });
1552
+
1553
+ await act(async () => {
1554
+ editor?.dispatchEvent(pasteEvent);
1555
+ await new Promise((resolve) => setTimeout(resolve, 0));
1556
+ });
1557
+
1558
+ expect(editor?.textContent).toBe(pastedText);
1559
+ expect(pasteEvent.defaultPrevented).toBe(true);
1560
+ });
1561
+
1562
+ it("keeps large plain-text pastes in the composer when uploads are disabled", async () => {
1563
+ const pastedText = "A".repeat(3200);
1564
+
1565
+ function Harness() {
1566
+ const runtime = useLocalRuntime(emptyChatModelAdapter);
1567
+ return React.createElement(
1568
+ AssistantRuntimeProvider,
1569
+ { runtime },
1570
+ React.createElement(
1571
+ TooltipProvider,
1572
+ null,
1573
+ React.createElement(TiptapComposer, {
1574
+ attachmentsEnabled: false,
1575
+ includeDefaultSlashSkills: false,
1576
+ plusMenuMode: "hidden",
1577
+ providerConnectStatusEnabled: false,
1578
+ voiceEnabled: false,
1579
+ }),
1580
+ ),
1581
+ );
1582
+ }
1583
+
1584
+ await act(async () => {
1585
+ root.render(React.createElement(Harness));
1586
+ await new Promise((resolve) => setTimeout(resolve, 0));
1587
+ });
1588
+
1589
+ const editor = container.querySelector<HTMLElement>(
1590
+ ".agent-composer-prosemirror",
1591
+ );
1592
+ expect(editor).not.toBeNull();
1593
+ const clipboardData = {
1594
+ files: [],
1595
+ getData: (type: string) => (type === "text/plain" ? pastedText : ""),
1596
+ };
1597
+ const pasteEvent = new Event("paste", {
1598
+ bubbles: true,
1599
+ cancelable: true,
1600
+ });
1601
+ Object.defineProperty(pasteEvent, "clipboardData", {
1602
+ value: clipboardData,
1603
+ });
1604
+
1605
+ await act(async () => {
1606
+ editor?.dispatchEvent(pasteEvent);
1607
+ await new Promise((resolve) => setTimeout(resolve, 0));
1608
+ });
1609
+
1610
+ expect(editor?.textContent).toBe(pastedText);
1611
+ });
1612
+ });
1613
+
1484
1614
  describe("TiptapComposer slash commands", () => {
1485
1615
  it("locks submission without blurring the editable surface", () => {
1486
1616
  const focusRef = React.createRef<TiptapComposerHandle>();
@@ -513,6 +513,7 @@ function clearComposerDraft(
513
513
  export function handleComposerFileDrop(options: {
514
514
  event: Pick<DragEvent, "dataTransfer" | "preventDefault" | "stopPropagation">;
515
515
  addAttachment: (file: File) => Promise<unknown>;
516
+ attachmentsEnabled?: boolean;
516
517
  onError?: (error: unknown) => void;
517
518
  }): boolean {
518
519
  const droppedFiles = Array.from(options.event.dataTransfer?.files ?? []);
@@ -520,6 +521,7 @@ export function handleComposerFileDrop(options: {
520
521
 
521
522
  options.event.preventDefault();
522
523
  options.event.stopPropagation();
524
+ if (options.attachmentsEnabled === false) return true;
523
525
  const attachments = droppedFiles.map(uniquifyComposerImageFile);
524
526
  void Promise.all(
525
527
  attachments.map((file) => options.addAttachment(file)),
@@ -783,6 +785,8 @@ export interface TiptapComposerProps {
783
785
  submitting?: boolean;
784
786
  /** Override the generic document attachment cap for a multipart host. */
785
787
  maxDocumentAttachmentBytes?: number;
788
+ /** Disable file attachments while keeping text chat available. */
789
+ attachmentsEnabled?: boolean;
786
790
  /** Label used in the visible document attachment limit error. */
787
791
  documentAttachmentLimitLabel?: string;
788
792
  focusRef?: React.Ref<TiptapComposerHandle>;
@@ -913,7 +917,6 @@ export interface TiptapComposerProps {
913
917
  onInspectContextItem?: (key: string) => void;
914
918
  onRetryContextItem?: (key: string) => void;
915
919
  contextMenuItems?: readonly ComposerContextMenuItem[];
916
- attachmentsEnabled?: boolean;
917
920
  /**
918
921
  * Controls the "+" menu next to the composer. `"full"` (default) shows the
919
922
  * normal Upload / Skill / Job / Automation / MCP picker, plus Extension when
@@ -1148,11 +1151,15 @@ const LOCAL_RUNTIME_ENGINES = new Set([
1148
1151
  "opencode-cli",
1149
1152
  ]);
1150
1153
 
1154
+ export function isLocalRuntimeEngine(engine?: string): boolean {
1155
+ return engine !== undefined && LOCAL_RUNTIME_ENGINES.has(engine);
1156
+ }
1157
+
1151
1158
  export function hasConfiguredCloudProvider(
1152
1159
  groups: ReadonlyArray<{ engine: string; configured: boolean }>,
1153
1160
  ): boolean {
1154
1161
  return groups.some(
1155
- (group) => group.configured && !LOCAL_RUNTIME_ENGINES.has(group.engine),
1162
+ (group) => group.configured && !isLocalRuntimeEngine(group.engine),
1156
1163
  );
1157
1164
  }
1158
1165
 
@@ -2460,6 +2467,7 @@ export function TiptapComposer({
2460
2467
  submitting = false,
2461
2468
  maxDocumentAttachmentBytes = MAX_DOCUMENT_ATTACHMENT_BYTES,
2462
2469
  documentAttachmentLimitLabel = "PDFs",
2470
+ attachmentsEnabled = true,
2463
2471
  focusRef,
2464
2472
  initialText,
2465
2473
  initialTextKey,
@@ -2510,7 +2518,6 @@ export function TiptapComposer({
2510
2518
  onInspectContextItem,
2511
2519
  onRetryContextItem,
2512
2520
  contextMenuItems,
2513
- attachmentsEnabled = true,
2514
2521
  plusMenuMode = "full",
2515
2522
  terminalModeControl,
2516
2523
  extensionTools = false,
@@ -2776,7 +2783,25 @@ export function TiptapComposer({
2776
2783
  class:
2777
2784
  "agent-composer-prosemirror flex-1 resize-none bg-transparent text-sm text-foreground outline-none leading-[1.625rem] min-h-[3.25rem] max-h-[10rem] overflow-y-auto",
2778
2785
  },
2779
- handlePaste: (_view, event) => {
2786
+ handlePaste: (view, event) => {
2787
+ if (disabled) {
2788
+ if (event.clipboardData?.files.length) {
2789
+ event.preventDefault();
2790
+ return true;
2791
+ }
2792
+ return false;
2793
+ }
2794
+ if (!attachmentsEnabled) {
2795
+ if (event.clipboardData?.files.length) {
2796
+ event.preventDefault();
2797
+ const pastedText = readClipboardPaste(event.clipboardData).text;
2798
+ if (pastedText) {
2799
+ view.pasteText(pastedText, new Event("paste") as ClipboardEvent);
2800
+ }
2801
+ return true;
2802
+ }
2803
+ return false;
2804
+ }
2780
2805
  const paste = readClipboardPaste(event.clipboardData);
2781
2806
  const pastedText = paste.text;
2782
2807
  const files = Array.from(event.clipboardData?.files ?? []).filter(
@@ -2842,12 +2867,20 @@ export function TiptapComposer({
2842
2867
  return false;
2843
2868
  },
2844
2869
  handleDrop: (_view, event) => {
2870
+ if (disabled || !attachmentsEnabled) {
2871
+ if (event.dataTransfer?.files.length) {
2872
+ event.preventDefault();
2873
+ return true;
2874
+ }
2875
+ return false;
2876
+ }
2845
2877
  // Drag-and-drop files (decks, images, PDFs, etc.) into the composer.
2846
2878
  // Mark handled drops as consumed so the chat-wide drop target does not
2847
2879
  // add the same file a second time.
2848
2880
  return handleComposerFileDrop({
2849
2881
  event: event as DragEvent,
2850
2882
  addAttachment: addAttachmentForCurrentScope,
2883
+ attachmentsEnabled,
2851
2884
  onError: (error) => {
2852
2885
  const msg = formatAttachmentError(
2853
2886
  error,
@@ -4259,28 +4292,30 @@ export function TiptapComposer({
4259
4292
  data-agent-composer-slot="toolbar"
4260
4293
  className="agent-composer-toolbar flex items-center gap-1 px-2 py-1.5"
4261
4294
  >
4262
- {attachButton ??
4263
- (contextMenuItems !== undefined || plusMenuMode === "upload-only" ? (
4264
- <ComposerContextMenu
4265
- items={contextMenuItems ?? []}
4266
- addAttachment={
4267
- attachmentsEnabled ? addAttachmentForCurrentScope : undefined
4268
- }
4269
- attachmentAccept={composerRuntime.getState().attachmentAccept}
4270
- onAttachmentError={onAttachmentError}
4271
- disabled={disabled}
4272
- />
4273
- ) : plusMenuMode === "hidden" ? null : (
4274
- <ComposerPlusMenu
4275
- addAttachment={addAttachmentForCurrentScope}
4276
- attachmentAccept={composerRuntime.getState().attachmentAccept}
4277
- onSelectMode={handleSelectMode}
4278
- mode={plusMenuMode}
4279
- terminalModeControl={terminalModeControl}
4280
- extensionTools={extensionTools}
4281
- onAttachmentError={onAttachmentError}
4282
- />
4283
- ))}
4295
+ {!disabled && attachmentsEnabled && attachButton ? (
4296
+ attachButton
4297
+ ) : contextMenuItems !== undefined || plusMenuMode === "upload-only" ? (
4298
+ <ComposerContextMenu
4299
+ items={contextMenuItems ?? []}
4300
+ addAttachment={
4301
+ attachmentsEnabled ? addAttachmentForCurrentScope : undefined
4302
+ }
4303
+ attachmentAccept={composerRuntime.getState().attachmentAccept}
4304
+ onAttachmentError={onAttachmentError}
4305
+ disabled={disabled}
4306
+ />
4307
+ ) : disabled || plusMenuMode === "hidden" ? null : (
4308
+ <ComposerPlusMenu
4309
+ addAttachment={addAttachmentForCurrentScope}
4310
+ attachmentsEnabled={attachmentsEnabled}
4311
+ attachmentAccept={composerRuntime.getState().attachmentAccept}
4312
+ onSelectMode={handleSelectMode}
4313
+ mode={plusMenuMode}
4314
+ terminalModeControl={terminalModeControl}
4315
+ extensionTools={extensionTools}
4316
+ onAttachmentError={onAttachmentError}
4317
+ />
4318
+ )}
4284
4319
  {toolbarSlot ?? modeControl}
4285
4320
  <div data-agent-composer-slot="toolbar-spacer" className="flex-1" />
4286
4321
  {shouldRenderModelSelector(availableModels, onModelChange) && (
@@ -45,6 +45,7 @@ export {
45
45
  displayableComposerModeMessage,
46
46
  getComposerSubmitIntentForEnterKey,
47
47
  handleComposerFileDrop,
48
+ isLocalRuntimeEngine,
48
49
  getOversizedDocumentAttachmentError,
49
50
  insertComposerHardBreakAndScrollIntoView,
50
51
  type ComposerSubmitIntent,
@@ -25,6 +25,12 @@ export type ReasoningEffort =
25
25
  | "xhigh"
26
26
  | "max";
27
27
 
28
+ export type ComposerAgentEngineState =
29
+ | "unknown"
30
+ | "unavailable"
31
+ | "missing"
32
+ | "configured";
33
+
28
34
  export interface EngineModelGroup {
29
35
  engine: string;
30
36
  label: string;
@@ -144,12 +150,12 @@ export interface ComposerRuntimeAdapters {
144
150
  useChatModels?: (options: { enabled: boolean }) => ComposerModelState;
145
151
  useAgentEngineConfigured?: (enabled: boolean) => {
146
152
  missing: boolean;
147
- state: string;
153
+ state: ComposerAgentEngineState;
148
154
  };
149
155
  fetchAgentEngineConfiguredState?: (
150
156
  enabled: boolean,
151
157
  options: { timeoutMs: number },
152
- ) => Promise<"missing" | "configured" | (string & {})>;
158
+ ) => Promise<ComposerAgentEngineState>;
153
159
  BuilderSetupCard?: ComponentType<any>;
154
160
  BuilderSetupContent?: ComponentType<any>;
155
161
  reasoning?: {
@@ -236,8 +242,11 @@ const fallbackModels = {
236
242
  onModelChange: () => {},
237
243
  onEffortChange: () => {},
238
244
  }),
239
- useAgentEngineConfigured: () => ({ missing: false, state: "configured" }),
240
- fetchAgentEngineConfiguredState: async () => "configured",
245
+ useAgentEngineConfigured: () => ({
246
+ missing: false,
247
+ state: "configured" as const,
248
+ }),
249
+ fetchAgentEngineConfiguredState: async () => "configured" as const,
241
250
  };
242
251
  const FragmentBoundary: ComponentType<{ children?: ReactNode }> = ({
243
252
  children,
@@ -534,6 +534,10 @@ export function ResourceIconPicker({
534
534
  other.authority === entry.authority,
535
535
  ) === index,
536
536
  );
537
+ const showUploadTab = !!onUpload || uploadChoices.length > 0;
538
+ React.useEffect(() => {
539
+ if (!showUploadTab && tab === "upload") setTab("icons");
540
+ }, [showUploadTab, tab]);
537
541
  return (
538
542
  <Popover
539
543
  open={open}
@@ -590,10 +594,17 @@ export function ResourceIconPicker({
590
594
  }}
591
595
  >
592
596
  <div className="p-2">
593
- <TabsList className="grid w-full grid-cols-3">
597
+ <TabsList
598
+ className={cn(
599
+ "grid w-full",
600
+ showUploadTab ? "grid-cols-3" : "grid-cols-2",
601
+ )}
602
+ >
594
603
  <TabsTrigger value="icons">{labels.iconsTab}</TabsTrigger>
595
604
  <TabsTrigger value="emoji">{labels.emojiTab}</TabsTrigger>
596
- <TabsTrigger value="upload">{labels.uploadTab}</TabsTrigger>
605
+ {showUploadTab && (
606
+ <TabsTrigger value="upload">{labels.uploadTab}</TabsTrigger>
607
+ )}
597
608
  </TabsList>
598
609
  </div>
599
610
  {tab !== "upload" && (