@agent-native/toolkit 0.22.2-nightly-20260927082735 → 0.22.2

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.
@@ -56,6 +56,54 @@ describe("controlled composer context", () => {
56
56
  container.querySelector('button[aria-label="Add context"]'),
57
57
  ).toBeNull();
58
58
  });
59
+ it("requests storage setup only after choosing Upload File", async () => {
60
+ const onAttachmentRequest = vi.fn();
61
+ await mount({
62
+ attachmentsEnabled: false,
63
+ plusMenuMode: "full",
64
+ onAttachmentRequest,
65
+ });
66
+ expect(onAttachmentRequest).not.toHaveBeenCalled();
67
+
68
+ await act(async () => {
69
+ container
70
+ .querySelector<HTMLButtonElement>(
71
+ 'button[data-agent-composer-slot="plus-button"]',
72
+ )!
73
+ .click();
74
+ });
75
+ const uploadFile = Array.from(
76
+ document.querySelectorAll<HTMLButtonElement>("button"),
77
+ ).find((button) => button.textContent?.includes("Upload File"));
78
+ expect(uploadFile).toBeDefined();
79
+ await act(async () => uploadFile!.click());
80
+
81
+ expect(onAttachmentRequest).toHaveBeenCalledOnce();
82
+ });
83
+
84
+ it("requests storage setup from the upload-only button", async () => {
85
+ const onAttachmentRequest = vi.fn();
86
+ await mount({
87
+ attachmentsEnabled: false,
88
+ onAttachmentRequest,
89
+ plusMenuMode: "upload-only",
90
+ });
91
+
92
+ expect(onAttachmentRequest).not.toHaveBeenCalled();
93
+ const trigger = container.querySelector<HTMLButtonElement>(
94
+ 'button[aria-label="Add context"]',
95
+ )!;
96
+ await act(async () =>
97
+ trigger.dispatchEvent(
98
+ new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
99
+ ),
100
+ );
101
+ const uploadFile = document.querySelector<HTMLElement>('[role="menuitem"]');
102
+ expect(uploadFile).toBeDefined();
103
+ expect(uploadFile?.textContent).toContain("Upload File");
104
+ await act(async () => uploadFile!.click());
105
+ expect(onAttachmentRequest).toHaveBeenCalledOnce();
106
+ });
59
107
  async function mount(props: Partial<PromptComposerProps> = {}) {
60
108
  const composerRef = React.createRef<TiptapComposerHandle>();
61
109
  const onSubmit = vi.fn();
@@ -54,6 +54,7 @@ import {
54
54
  isLocalRuntimeEngine,
55
55
  TiptapComposer,
56
56
  type ComposerAgentOption,
57
+ type ComposerImageModelMenu,
57
58
  type ComposerSubmitIntent,
58
59
  type TiptapComposerHandle,
59
60
  type TiptapComposerSubmitOptions,
@@ -67,6 +68,11 @@ import type {
67
68
 
68
69
  const MAX_INLINE_TEXT_FILE_CHARS = 60_000;
69
70
 
71
+ /**
72
+ * Files the user attached via the "+" button in PromptComposer. The host owns
73
+ * what to do with them — typically POST to a per-app upload endpoint and pass
74
+ * the resulting URLs/paths into the prompt that gets sent to the agent.
75
+ */
70
76
  export type PromptComposerFile = File;
71
77
 
72
78
  export interface PromptComposerSubmitOptions {
@@ -84,83 +90,165 @@ export interface PromptComposerProps {
84
90
  onInspectContextItem?: (key: string) => void;
85
91
  onRetryContextItem?: (key: string) => void;
86
92
  contextMenuItems?: readonly ComposerContextMenuItem[];
93
+ /** Called when the user submits the composer. */
87
94
  onSubmit: (
88
95
  text: string,
89
96
  files: PromptComposerFile[],
90
97
  references: Reference[],
91
98
  options: PromptComposerSubmitOptions,
92
99
  ) => void | Promise<void>;
100
+ /** Return false to stop a submit before it reaches the host runtime. */
101
+ onBeforeSubmit?: () => boolean | Promise<boolean>;
102
+ /** Handle file paste/drop errors in the host chat surface. */
103
+ onAttachmentError?: (message: string) => void;
104
+ /** Delegate app-scaffolding prompts to the enclosing Builder chat. */
105
+ interceptBuildRequestsForBuilder?: boolean;
93
106
  placeholder?: string;
107
+ /** Accessible name forwarded to the rich text editor. */
94
108
  ariaLabel?: string;
95
109
  disabled?: boolean;
110
+ /** Block all submission paths while allowing draft, file, and context staging. */
96
111
  submissionDisabled?: boolean;
112
+ /** Prevent submission while preserving editor focus and draft entry. */
97
113
  submitting?: boolean;
114
+ /** Present the primary action as queueing instead of immediate send. */
98
115
  willQueue?: boolean;
116
+ /** Called when a host-gated composer is clicked while it is disabled. */
99
117
  onDisabledClick?: () => void;
118
+ /** Override the generic document attachment cap for a multipart host. */
100
119
  maxDocumentAttachmentBytes?: number;
120
+ /** Label used in the visible document attachment limit error. */
101
121
  documentAttachmentLimitLabel?: string;
102
122
  autoFocus?: boolean;
103
123
  className?: string;
104
124
  style?: CSSProperties;
105
125
  rootClassName?: string;
106
126
  rootStyle?: CSSProperties;
127
+ /** Forwarded to TiptapComposer for draft persistence. */
107
128
  draftScope?: string;
129
+ /** Keep the submitted prompt in the editor. Default: false. */
108
130
  preserveDraftOnSubmit?: boolean;
131
+ /** Show the model selector (default: true). */
109
132
  showModelSelector?: boolean;
133
+ /** Controlled open state for hosts that resize around the model picker. */
110
134
  modelSelectorOpen?: boolean;
135
+ /** Show the legacy provider-level Auto model option (default: true). */
111
136
  showAutoModelOption?: boolean;
137
+ /** Show the voice dictation button. Defaults to DEFAULT_VOICE_DICTATION_ENABLED. */
112
138
  voiceEnabled?: boolean;
139
+ /** Show file upload controls and pass submitted files to onSubmit (default: true). */
113
140
  attachmentsEnabled?: boolean;
141
+ /** Opens host-owned storage setup when the user chooses an upload action. */
114
142
  onAttachmentRequest?: () => void;
143
+ /** Hide the Add context tooltip while the host storage popover is open. */
144
+ contextButtonTooltipDisabled?: boolean;
145
+ /** Host-owned file acceptance and staging; the shared composer still owns picker and chips. */
115
146
  attachmentAdapter?: AttachmentAdapter;
147
+ /** Let hosts extract ordinary uploaded text without also inlining it. */
116
148
  inlineTextAttachments?: boolean;
149
+ /**
150
+ * Controls the shared "+" affordance. Defaults to upload-only for standalone
151
+ * prompt forms; chat surfaces can opt into the full sidebar menu.
152
+ */
117
153
  plusMenuMode?: "full" | "upload-only" | "terminal" | "hidden";
154
+ /** Controls the terminal-specific plus menu when `plusMenuMode` is terminal. */
118
155
  terminalModeControl?: ComposerTerminalModeControl;
156
+ /**
157
+ * Include extension creation in the full "+" menu. Defaults to false.
158
+ */
119
159
  extensionTools?: boolean;
160
+ /** Programmatically seed the composer with plain text. */
120
161
  initialText?: string;
162
+ /** Stable key used to re-apply `initialText` when the host picks a preset. */
121
163
  initialTextKey?: string | number;
164
+ /** Optional host-owned control rendered directly after the "+" button. */
122
165
  modeControl?: ReactNode;
166
+ /** Current agent execution mode shown in the shared composer toolbar. */
123
167
  execMode?: "build" | "plan";
168
+ /** Called when the user switches between acting and read-only planning. */
124
169
  onExecModeChange?: (mode: "build" | "plan") => void;
170
+ /** Disable Plan mode while leaving Act mode available. */
171
+ planModeDisabled?: boolean;
172
+ /** Explanation shown next to the disabled Plan option. */
173
+ planModeDisabledReason?: string;
174
+ /** Explicit host-owned toolbar slot rendered directly after the "+" button. */
125
175
  toolbarSlot?: ReactNode;
176
+ /** Custom attachment button to render instead of the default "+" affordance. */
126
177
  attachButton?: ReactNode;
178
+ /** Custom action button to render instead of the default send button. */
127
179
  actionButton?: ReactNode;
180
+ /** Extra button rendered alongside the default send button. */
128
181
  extraActionButton?: ReactNode;
182
+ /** Optional stop control shown while the host runtime is active. */
183
+ stopButton?: ReactNode;
184
+ /** Shared sizing/layout variant for host surfaces. Default keeps sidebar behavior. */
129
185
  layoutVariant?: AgentComposerLayoutVariant;
186
+ /** Additional slash commands surfaced in the shared / menu. */
130
187
  slashCommands?: SlashCommand[];
188
+ /** Additional slash skills surfaced in the shared / menu. */
131
189
  slashSkills?: SkillResult[];
190
+ /** Include built-in sidebar slash commands when onSlashCommand is provided. */
132
191
  includeDefaultSlashCommands?: boolean;
192
+ /** Include app-discovered skills from the default agent endpoint. Default true. */
133
193
  includeDefaultSlashSkills?: boolean;
194
+ /** Called when a slash command from the shared / menu is executed. */
134
195
  onSlashCommand?: (command: string) => void;
196
+ /** External model list for hosts that already resolve models outside the app. */
135
197
  availableModels?: EngineModelGroup[];
198
+ /** Whether the external model list is still being resolved. */
136
199
  modelListLoading?: boolean;
137
200
  selectedModel?: string;
138
201
  selectedEngine?: string;
139
202
  selectedEffort?: ReasoningEffort;
140
203
  onModelChange?: (model: string, engine: string) => void;
141
204
  onEffortChange?: (effort: ReasoningEffort) => void;
205
+ /** Local or hosted agent runtimes shown above the model list. */
142
206
  availableAgents?: ComposerAgentOption[];
207
+ /** Selected agent runtime identifier. */
143
208
  selectedAgent?: string;
209
+ /** Show only the selected agent in the model control. */
144
210
  agentOnly?: boolean;
211
+ /** Callback when the user picks an agent runtime. */
145
212
  onAgentChange?: (agent: string) => void;
213
+ /** Called when the shared model picker opens or closes. */
146
214
  onModelSelectorOpenChange?: (open: boolean) => void;
215
+ /** Enable server-backed model/provider status checks. Defaults on, except for a selected local runtime. */
147
216
  modelStatusChecksEnabled?: boolean;
217
+ /** Called whenever the plain editor text changes. */
148
218
  onTextChange?: (text: string) => void;
219
+ /** Called whenever attached files change, before the composer is submitted. */
149
220
  onAttachmentsChange?: (files: PromptComposerFile[]) => void;
221
+ /** Called whenever the composer resolves a model, engine, or effort choice. */
150
222
  onModelSelectionChange?: (
151
223
  selection: Pick<PromptComposerSubmitOptions, "model" | "engine" | "effort">,
152
224
  ) => void;
225
+ /**
226
+ * Override the Builder.io connect action in the model picker. When provided,
227
+ * clicking "Connect Builder.io" calls this instead of opening a browser popup.
228
+ * Used by the Electron desktop app to route through the native IPC handler.
229
+ */
153
230
  onConnectProvider?: () => void;
231
+ /** Called when a local runtime needs its native sign-in/setup flow. */
154
232
  onConnectLocalRuntime?: (engine: string) => void;
233
+ imageModelMenu?: ComposerImageModelMenu;
234
+ /** Imperative handle for focusing the composer. */
155
235
  composerRef?: Ref<TiptapComposerHandle>;
156
236
  }
157
237
 
238
+ // Minimal pass-through adapter. PromptComposer always submits through
239
+ // onSubmitOverride, so the runtime never actually calls this — but
240
+ // `useLocalRuntime` needs *something* shaped like a ChatModelAdapter.
158
241
  const NOOP_ADAPTER: ChatModelAdapter = {
159
242
  async *run() {
160
243
  yield* [];
161
244
  },
162
245
  };
163
246
 
247
+ /**
248
+ * Local binary document adapter so reference PDFs, decks, and docs can be
249
+ * attached without dragging the whole assistant chat module into bundles that
250
+ * just want a prompt popover.
251
+ */
164
252
  class BinaryDocumentAttachmentAdapter implements AttachmentAdapter {
165
253
  public accept = PROMPT_DOCUMENT_ATTACHMENT_ACCEPT;
166
254
 
@@ -223,12 +311,17 @@ function formatInlineTextFile(name: string, text: string): string {
223
311
  .join("\n");
224
312
  }
225
313
 
314
+ /** Chat stays closed until the provider check confirms it can run. */
226
315
  export function shouldGateComposerForEngine(
227
316
  state: ComposerAgentEngineState,
228
317
  ): boolean {
229
318
  return state !== "configured";
230
319
  }
231
320
 
321
+ /**
322
+ * Show setup treatment only for a confirmed-missing engine with a setup
323
+ * component. Unresolved status uses the retry treatment instead.
324
+ */
232
325
  export function shouldGateComposerForMissingEngine(input: {
233
326
  state: string;
234
327
  hasSetupComponent: boolean;
@@ -286,6 +379,11 @@ export async function buildPromptComposerSubmission(options: {
286
379
  // Keep the upload path fallback below.
287
380
  }
288
381
  }
382
+ // Note: images are NOT inlined into the prompt text even when small.
383
+ // Inlining a base64 data-URL into a text string consumes an enormous
384
+ // number of tokens (≈ 700 K per MB) and most hosts handle images via
385
+ // proper attachment channels. The `files` array below carries the image
386
+ // for the host to process through a dedicated attachment pipeline.
289
387
  files.push(file);
290
388
  }
291
389
  }
@@ -507,6 +605,7 @@ function PromptComposerInner({
507
605
  voiceEnabled = DEFAULT_VOICE_DICTATION_ENABLED,
508
606
  attachmentsEnabled = true,
509
607
  onAttachmentRequest,
608
+ contextButtonTooltipDisabled = false,
510
609
  inlineTextAttachments = true,
511
610
  plusMenuMode,
512
611
  terminalModeControl,
@@ -516,10 +615,13 @@ function PromptComposerInner({
516
615
  modeControl,
517
616
  execMode,
518
617
  onExecModeChange,
618
+ planModeDisabled,
619
+ planModeDisabledReason,
519
620
  toolbarSlot,
520
621
  attachButton,
521
622
  actionButton,
522
623
  extraActionButton,
624
+ stopButton,
523
625
  layoutVariant,
524
626
  slashCommands,
525
627
  slashSkills,
@@ -544,7 +646,11 @@ function PromptComposerInner({
544
646
  onModelSelectionChange,
545
647
  onConnectProvider,
546
648
  onConnectLocalRuntime,
649
+ imageModelMenu,
547
650
  composerRef,
651
+ onBeforeSubmit,
652
+ onAttachmentError,
653
+ interceptBuildRequestsForBuilder,
548
654
  }: PromptComposerProps) {
549
655
  const adapters = useComposerRuntimeAdapters();
550
656
  const t = adapters.translate!;
@@ -665,6 +771,12 @@ function PromptComposerInner({
665
771
  attachments?: ReadonlyArray<unknown>,
666
772
  submitOptions?: TiptapComposerSubmitOptions,
667
773
  ) => {
774
+ // PromptComposer hosts (NewWorkspaceAppFlow, create-extension, create-deck,
775
+ // …) submit a single string prompt — they don't run the assistant-ui
776
+ // attachment send pipeline. TiptapComposer auto-converts large pastes
777
+ // into a "Pasted text" chip, which would otherwise disappear into an
778
+ // unprocessed File. Inline the chip body back into the prompt text so
779
+ // newlines and full content survive the round-trip.
668
780
  const { text: finalText, files } = await buildPromptComposerSubmission({
669
781
  text,
670
782
  attachments,
@@ -759,6 +871,7 @@ function PromptComposerInner({
759
871
  onRetryContextItem={onRetryContextItem}
760
872
  attachmentsEnabled={attachmentsEnabled}
761
873
  onAttachmentRequest={onAttachmentRequest}
874
+ contextButtonTooltipDisabled={contextButtonTooltipDisabled}
762
875
  ariaLabel={ariaLabel}
763
876
  focusRef={handleRef}
764
877
  disabled={disabled || gateComposer}
@@ -779,6 +892,9 @@ function PromptComposerInner({
779
892
  initialText={initialText}
780
893
  initialTextKey={initialTextKey}
781
894
  onSubmit={handleSubmit}
895
+ onBeforeSubmit={onBeforeSubmit}
896
+ onAttachmentError={onAttachmentError}
897
+ interceptBuildRequestsForBuilder={interceptBuildRequestsForBuilder}
782
898
  clearOnSubmit={!preserveDraftOnSubmit}
783
899
  plusMenuMode={
784
900
  gateComposer
@@ -798,9 +914,12 @@ function PromptComposerInner({
798
914
  modeControl={modeControl}
799
915
  execMode={execMode}
800
916
  onExecModeChange={onExecModeChange}
917
+ planModeDisabled={planModeDisabled}
918
+ planModeDisabledReason={planModeDisabledReason}
801
919
  toolbarSlot={toolbarSlot}
802
920
  actionButton={actionButton}
803
921
  extraActionButton={extraActionButton}
922
+ stopButton={stopButton}
804
923
  layoutVariant={layoutVariant}
805
924
  slashCommands={slashCommands}
806
925
  slashSkills={slashSkills}
@@ -827,12 +946,24 @@ function PromptComposerInner({
827
946
  providerConnectStatusEnabled={resolvedModelStatusChecksEnabled}
828
947
  onConnectProvider={onConnectProvider}
829
948
  onConnectLocalRuntime={onConnectLocalRuntime}
949
+ imageModelMenu={imageModelMenu}
830
950
  />
831
951
  </AgentComposerFrame>
832
952
  </>
833
953
  );
834
954
  }
835
955
 
956
+ /**
957
+ * Standalone composer that mirrors the agent sidebar's input experience —
958
+ * voice dictation, file upload, model selector, submit-on-Enter — for use in
959
+ * popovers and inline prompt forms (create tool, create deck, create dashboard,
960
+ * the Dispatch new-app flow, etc.).
961
+ *
962
+ * The host owns submission: when the user presses Enter or clicks submit,
963
+ * `onSubmit(text, files, references, options)` is called. PromptComposer runs
964
+ * its own minimal assistant-ui runtime so it can be dropped into any subtree
965
+ * without needing the outer chat to be mounted.
966
+ */
836
967
  function PromptComposerRuntime(props: PromptComposerProps) {
837
968
  const StaleIndexBoundary =
838
969
  useComposerRuntimeAdapters().agentChat!.StaleIndexBoundary!;