@agent-native/toolkit 0.21.3 → 0.22.0-nightly-20260926111339

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 (166) hide show
  1. package/agent-native.eject.json +11 -20
  2. package/dist/agentkit.d.ts +5 -1
  3. package/dist/agentkit.d.ts.map +1 -1
  4. package/dist/agentkit.js +2 -0
  5. package/dist/agentkit.js.map +1 -1
  6. package/dist/app-shell/PromptHome.d.ts +26 -0
  7. package/dist/app-shell/PromptHome.d.ts.map +1 -0
  8. package/dist/app-shell/PromptHome.js +19 -0
  9. package/dist/app-shell/PromptHome.js.map +1 -0
  10. package/dist/app-shell/PromptHome.spec.d.ts +2 -0
  11. package/dist/app-shell/PromptHome.spec.d.ts.map +1 -0
  12. package/dist/app-shell/PromptHome.spec.js +206 -0
  13. package/dist/app-shell/PromptHome.spec.js.map +1 -0
  14. package/dist/app-shell/TemplateLibraryGrid.d.ts +48 -0
  15. package/dist/app-shell/TemplateLibraryGrid.d.ts.map +1 -0
  16. package/dist/app-shell/TemplateLibraryGrid.js +20 -0
  17. package/dist/app-shell/TemplateLibraryGrid.js.map +1 -0
  18. package/dist/app-shell/TemplatePreviewDialog.d.ts +41 -0
  19. package/dist/app-shell/TemplatePreviewDialog.d.ts.map +1 -0
  20. package/dist/app-shell/TemplatePreviewDialog.js +50 -0
  21. package/dist/app-shell/TemplatePreviewDialog.js.map +1 -0
  22. package/dist/app-shell/TemplatePreviewDialog.spec.d.ts +2 -0
  23. package/dist/app-shell/TemplatePreviewDialog.spec.d.ts.map +1 -0
  24. package/dist/app-shell/TemplatePreviewDialog.spec.js +258 -0
  25. package/dist/app-shell/TemplatePreviewDialog.spec.js.map +1 -0
  26. package/dist/app-shell/index.d.ts +3 -0
  27. package/dist/app-shell/index.d.ts.map +1 -1
  28. package/dist/app-shell/index.js +3 -0
  29. package/dist/app-shell/index.js.map +1 -1
  30. package/dist/app-shell/prompt-home.css +159 -0
  31. package/dist/composer/ComposerContextMenu.d.ts +48 -0
  32. package/dist/composer/ComposerContextMenu.d.ts.map +1 -0
  33. package/dist/composer/ComposerContextMenu.js +295 -0
  34. package/dist/composer/ComposerContextMenu.js.map +1 -0
  35. package/dist/composer/ComposerContextMenu.spec.d.ts +2 -0
  36. package/dist/composer/ComposerContextMenu.spec.d.ts.map +1 -0
  37. package/dist/composer/ComposerContextMenu.spec.js +270 -0
  38. package/dist/composer/ComposerContextMenu.spec.js.map +1 -0
  39. package/dist/composer/ComposerContextMenuSearch.d.ts +11 -0
  40. package/dist/composer/ComposerContextMenuSearch.d.ts.map +1 -0
  41. package/dist/composer/ComposerContextMenuSearch.js +42 -0
  42. package/dist/composer/ComposerContextMenuSearch.js.map +1 -0
  43. package/dist/composer/ComposerContextPicker.d.ts +7 -0
  44. package/dist/composer/ComposerContextPicker.d.ts.map +1 -0
  45. package/dist/composer/ComposerContextPicker.js +71 -0
  46. package/dist/composer/ComposerContextPicker.js.map +1 -0
  47. package/dist/composer/ComposerContextPicker.spec.d.ts +2 -0
  48. package/dist/composer/ComposerContextPicker.spec.d.ts.map +1 -0
  49. package/dist/composer/ComposerContextPicker.spec.js +454 -0
  50. package/dist/composer/ComposerContextPicker.spec.js.map +1 -0
  51. package/dist/composer/ComposerContextPickerDialog.d.ts +8 -0
  52. package/dist/composer/ComposerContextPickerDialog.d.ts.map +1 -0
  53. package/dist/composer/ComposerContextPickerDialog.js +101 -0
  54. package/dist/composer/ComposerContextPickerDialog.js.map +1 -0
  55. package/dist/composer/ComposerContextPickerDialog.spec.d.ts +2 -0
  56. package/dist/composer/ComposerContextPickerDialog.spec.d.ts.map +1 -0
  57. package/dist/composer/ComposerContextPickerDialog.spec.js +329 -0
  58. package/dist/composer/ComposerContextPickerDialog.spec.js.map +1 -0
  59. package/dist/composer/ComposerContextSearchInput.d.ts +7 -0
  60. package/dist/composer/ComposerContextSearchInput.d.ts.map +1 -0
  61. package/dist/composer/ComposerContextSearchInput.js +18 -0
  62. package/dist/composer/ComposerContextSearchInput.js.map +1 -0
  63. package/dist/composer/PromptComposer.context.spec.d.ts +2 -0
  64. package/dist/composer/PromptComposer.context.spec.d.ts.map +1 -0
  65. package/dist/composer/PromptComposer.context.spec.js +383 -0
  66. package/dist/composer/PromptComposer.context.spec.js.map +1 -0
  67. package/dist/composer/PromptComposer.d.ts +19 -5
  68. package/dist/composer/PromptComposer.d.ts.map +1 -1
  69. package/dist/composer/PromptComposer.js +30 -25
  70. package/dist/composer/PromptComposer.js.map +1 -1
  71. package/dist/composer/PromptComposer.spec.js +24 -1
  72. package/dist/composer/PromptComposer.spec.js.map +1 -1
  73. package/dist/composer/TiptapComposer.d.ts +13 -2
  74. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  75. package/dist/composer/TiptapComposer.js +89 -32
  76. package/dist/composer/TiptapComposer.js.map +1 -1
  77. package/dist/composer/context-items.d.ts +12 -0
  78. package/dist/composer/context-items.d.ts.map +1 -0
  79. package/dist/composer/context-items.js +46 -0
  80. package/dist/composer/context-items.js.map +1 -0
  81. package/dist/composer/context-items.spec.d.ts +2 -0
  82. package/dist/composer/context-items.spec.d.ts.map +1 -0
  83. package/dist/composer/context-items.spec.js +43 -0
  84. package/dist/composer/context-items.spec.js.map +1 -0
  85. package/dist/composer/index.d.ts +4 -1
  86. package/dist/composer/index.d.ts.map +1 -1
  87. package/dist/composer/index.js +3 -1
  88. package/dist/composer/index.js.map +1 -1
  89. package/dist/composer/runtime-adapters.d.ts +6 -1
  90. package/dist/composer/runtime-adapters.d.ts.map +1 -1
  91. package/dist/composer/runtime-adapters.js.map +1 -1
  92. package/dist/composer/useComposerContextPicker.d.ts +111 -0
  93. package/dist/composer/useComposerContextPicker.d.ts.map +1 -0
  94. package/dist/composer/useComposerContextPicker.js +299 -0
  95. package/dist/composer/useComposerContextPicker.js.map +1 -0
  96. package/dist/design-system/default-adapter.d.ts.map +1 -1
  97. package/dist/design-system/default-adapter.js +13 -7
  98. package/dist/design-system/default-adapter.js.map +1 -1
  99. package/dist/design-system/default-adapter.spec.js +13 -0
  100. package/dist/design-system/default-adapter.spec.js.map +1 -1
  101. package/dist/design-system/types.d.ts +4 -1
  102. package/dist/design-system/types.d.ts.map +1 -1
  103. package/dist/index.d.ts +0 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +0 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/styles.css +1 -0
  108. package/dist/ui/command.d.ts +8 -2
  109. package/dist/ui/command.d.ts.map +1 -1
  110. package/dist/ui/command.js +9 -2
  111. package/dist/ui/command.js.map +1 -1
  112. package/dist/ui/command.spec.js +20 -1
  113. package/dist/ui/command.spec.js.map +1 -1
  114. package/package.json +2 -15
  115. package/src/agentkit.ts +26 -0
  116. package/src/app-shell/PromptHome.spec.tsx +404 -0
  117. package/src/app-shell/PromptHome.tsx +111 -0
  118. package/src/app-shell/TemplateLibraryGrid.tsx +212 -0
  119. package/src/app-shell/TemplatePreviewDialog.spec.tsx +473 -0
  120. package/src/app-shell/TemplatePreviewDialog.tsx +224 -0
  121. package/src/app-shell/index.ts +3 -0
  122. package/src/app-shell/prompt-home.css +159 -0
  123. package/src/composer/ComposerContextMenu.spec.tsx +343 -0
  124. package/src/composer/ComposerContextMenu.tsx +610 -0
  125. package/src/composer/ComposerContextMenuSearch.tsx +69 -0
  126. package/src/composer/ComposerContextPicker.spec.tsx +520 -0
  127. package/src/composer/ComposerContextPicker.tsx +280 -0
  128. package/src/composer/ComposerContextPickerDialog.spec.tsx +405 -0
  129. package/src/composer/ComposerContextPickerDialog.tsx +274 -0
  130. package/src/composer/ComposerContextSearchInput.tsx +48 -0
  131. package/src/composer/PromptComposer.context.spec.tsx +577 -0
  132. package/src/composer/PromptComposer.spec.ts +24 -1
  133. package/src/composer/PromptComposer.tsx +60 -26
  134. package/src/composer/TiptapComposer.tsx +198 -48
  135. package/src/composer/context-items.spec.ts +68 -0
  136. package/src/composer/context-items.ts +72 -0
  137. package/src/composer/index.ts +27 -0
  138. package/src/composer/runtime-adapters.tsx +6 -1
  139. package/src/composer/useComposerContextPicker.ts +441 -0
  140. package/src/design-system/default-adapter.spec.tsx +23 -0
  141. package/src/design-system/default-adapter.tsx +52 -23
  142. package/src/design-system/types.ts +4 -1
  143. package/src/index.ts +0 -1
  144. package/src/styles.css +1 -0
  145. package/src/ui/command.spec.tsx +33 -1
  146. package/src/ui/command.tsx +52 -9
  147. package/dist/marketing/MarketingHome.d.ts +0 -36
  148. package/dist/marketing/MarketingHome.d.ts.map +0 -1
  149. package/dist/marketing/MarketingHome.js +0 -28
  150. package/dist/marketing/MarketingHome.js.map +0 -1
  151. package/dist/marketing/MarketingHome.spec.d.ts +0 -2
  152. package/dist/marketing/MarketingHome.spec.d.ts.map +0 -1
  153. package/dist/marketing/MarketingHome.spec.js +0 -44
  154. package/dist/marketing/MarketingHome.spec.js.map +0 -1
  155. package/dist/marketing/Starfield.d.ts +0 -8
  156. package/dist/marketing/Starfield.d.ts.map +0 -1
  157. package/dist/marketing/Starfield.js +0 -349
  158. package/dist/marketing/Starfield.js.map +0 -1
  159. package/dist/marketing/index.d.ts +0 -3
  160. package/dist/marketing/index.d.ts.map +0 -1
  161. package/dist/marketing/index.js +0 -3
  162. package/dist/marketing/index.js.map +0 -1
  163. package/src/marketing/MarketingHome.spec.tsx +0 -75
  164. package/src/marketing/MarketingHome.tsx +0 -183
  165. package/src/marketing/Starfield.tsx +0 -403
  166. package/src/marketing/index.ts +0 -6
@@ -0,0 +1,610 @@
1
+ import { IconFile, IconPlus, IconTextRecognition } from "@tabler/icons-react";
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ type ReactNode,
8
+ } from "react";
9
+
10
+ import { Button } from "../ui/button.js";
11
+ import {
12
+ DropdownMenu,
13
+ DropdownMenuContent,
14
+ DropdownMenuGroup,
15
+ DropdownMenuItem,
16
+ DropdownMenuSub,
17
+ DropdownMenuSubContent,
18
+ DropdownMenuSubTrigger,
19
+ DropdownMenuTrigger,
20
+ } from "../ui/dropdown-menu.js";
21
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
22
+ import { formatAttachmentError } from "./attachment-accept.js";
23
+ import { ComposerContextMenuSearch } from "./ComposerContextMenuSearch.js";
24
+ import {
25
+ ComposerContextPicker,
26
+ type ComposerContextPickerConfig,
27
+ } from "./ComposerContextPicker.js";
28
+ import { ComposerContextPickerDialog } from "./ComposerContextPickerDialog.js";
29
+ import { useComposerRuntimeAdapters } from "./runtime-adapters.js";
30
+
31
+ export {
32
+ ComposerContextSearchInput,
33
+ type ComposerContextSearchInputProps,
34
+ } from "./ComposerContextSearchInput.js";
35
+ export type {
36
+ ComposerContextPickerConfig,
37
+ ComposerContextPickerItem,
38
+ ComposerContextPickerRequest,
39
+ ComposerContextPickerResult,
40
+ ComposerContextPickerSelection,
41
+ ComposerContextPickerFooterAction,
42
+ } from "./ComposerContextPicker.js";
43
+
44
+ interface ComposerContextMenuEntry {
45
+ id: string;
46
+ label: string;
47
+ keywords?: readonly string[];
48
+ icon?: ReactNode;
49
+ disabled?: boolean;
50
+ }
51
+ export type ComposerContextMenuAction = ComposerContextMenuEntry & {
52
+ onDismiss?: () => void;
53
+ children?: never;
54
+ } & (
55
+ | { picker: ComposerContextPickerConfig; render?: never; onSelect?: never }
56
+ | {
57
+ onSelect: () => void | Promise<void>;
58
+ render?: (controls: ComposerContextPageControls) => ReactNode;
59
+ picker?: never;
60
+ }
61
+ );
62
+ export interface ComposerContextPageControls {
63
+ onBack(): void;
64
+ onClose(options?: { restoreFocus?: boolean }): void;
65
+ onResume(): void;
66
+ }
67
+ export interface ComposerContextMenuCategory extends ComposerContextMenuEntry {
68
+ children: readonly ComposerContextMenuItem[];
69
+ searchPlaceholder?: string;
70
+ onSelect?: never;
71
+ picker?: never;
72
+ render?: never;
73
+ }
74
+ export type ComposerContextMenuItem =
75
+ | ComposerContextMenuAction
76
+ | ComposerContextMenuCategory;
77
+ export interface ComposerContextMenuProps {
78
+ items: readonly ComposerContextMenuItem[];
79
+ addAttachment?: (file: File) => Promise<unknown>;
80
+ attachmentAccept?: string;
81
+ onAttachmentError?: (message: string) => void;
82
+ disabled?: boolean;
83
+ }
84
+ interface ComposerContextPage {
85
+ id: string;
86
+ origin: string[];
87
+ onDismiss?: () => void;
88
+ }
89
+ interface ComposerContextDialogSession {
90
+ id: string;
91
+ scopeKey?: string;
92
+ }
93
+
94
+ function findAction(
95
+ items: readonly ComposerContextMenuItem[],
96
+ id: string,
97
+ ): ComposerContextMenuAction | undefined {
98
+ for (const item of items) {
99
+ if (item.children) {
100
+ const match = findAction(item.children, id);
101
+ if (match) return match;
102
+ } else if (item.id === id) return item;
103
+ }
104
+ }
105
+
106
+ export function getComposerContextMenuEntries(
107
+ items: readonly ComposerContextMenuItem[],
108
+ path: readonly string[],
109
+ query: string,
110
+ ): ComposerContextMenuItem[] {
111
+ let scope = items;
112
+ for (const id of path) {
113
+ const category = scope.find((item) => item.id === id);
114
+ if (!category?.children || category.disabled) return [];
115
+ scope = category.children;
116
+ }
117
+ const terms = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
118
+ if (!terms.length) return [...scope];
119
+ const matches: ComposerContextMenuItem[] = [];
120
+ const visit = (
121
+ entries: readonly ComposerContextMenuItem[],
122
+ ancestors: string[],
123
+ disabled = false,
124
+ ) => {
125
+ for (const entry of entries) {
126
+ const searchable = [...ancestors, entry.label, ...(entry.keywords ?? [])];
127
+ if (entry.children)
128
+ visit(entry.children, searchable, disabled || entry.disabled === true);
129
+ else if (
130
+ terms.every((term) =>
131
+ searchable.join(" ").toLocaleLowerCase().includes(term),
132
+ )
133
+ )
134
+ matches.push(disabled ? { ...entry, disabled: true } : entry);
135
+ }
136
+ };
137
+ visit(scope, []);
138
+ return matches;
139
+ }
140
+
141
+ function ContextSubmenu({
142
+ label,
143
+ icon,
144
+ disabled,
145
+ open,
146
+ onOpenChange,
147
+ children,
148
+ }: {
149
+ label: string;
150
+ icon?: ReactNode;
151
+ disabled?: boolean;
152
+ open: boolean;
153
+ onOpenChange: (open: boolean) => void;
154
+ children: ReactNode;
155
+ }) {
156
+ const trigger = useRef<HTMLDivElement>(null);
157
+ return (
158
+ <DropdownMenuSub open={open} onOpenChange={onOpenChange}>
159
+ <DropdownMenuSubTrigger ref={trigger} disabled={disabled}>
160
+ {icon}
161
+ {label}
162
+ </DropdownMenuSubTrigger>
163
+ <DropdownMenuSubContent
164
+ className="w-64"
165
+ data-agent-native-composer-popover="true"
166
+ onFocusOutside={(event) => {
167
+ const target = event.target;
168
+ // Radix focuses ancestor menus/triggers while a resumed chain mounts.
169
+ if (
170
+ target instanceof HTMLElement &&
171
+ target.closest('[data-agent-native-composer-popover="true"]') &&
172
+ target.matches(
173
+ '[role="menu"], [aria-haspopup="menu"][aria-expanded="true"]',
174
+ )
175
+ ) {
176
+ event.preventDefault();
177
+ }
178
+ }}
179
+ onEscapeKeyDown={(event) => {
180
+ event.preventDefault();
181
+ event.stopPropagation();
182
+ onOpenChange(false);
183
+ trigger.current?.focus();
184
+ }}
185
+ >
186
+ {children}
187
+ </DropdownMenuSubContent>
188
+ </DropdownMenuSub>
189
+ );
190
+ }
191
+
192
+ function ContextMenuPanel({
193
+ placeholder,
194
+ children,
195
+ }: {
196
+ placeholder: string;
197
+ children: (query: string) => ReactNode;
198
+ }) {
199
+ const [query, setQuery] = useState("");
200
+ return (
201
+ <>
202
+ <ComposerContextMenuSearch
203
+ placeholder={placeholder}
204
+ value={query}
205
+ onValueChange={setQuery}
206
+ />
207
+ {children(query)}
208
+ </>
209
+ );
210
+ }
211
+
212
+ function matchesEntry(entry: ComposerContextMenuItem, query: string): boolean {
213
+ const terms = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
214
+ const text = [entry.label, ...(entry.keywords ?? [])]
215
+ .join(" ")
216
+ .toLocaleLowerCase();
217
+ return (
218
+ terms.every((term) => text.includes(term)) ||
219
+ Boolean(entry.children?.some((child) => matchesEntry(child, query)))
220
+ );
221
+ }
222
+
223
+ function LegacyContextPage({ children }: { children: ReactNode }) {
224
+ const element = useRef<HTMLDivElement>(null);
225
+ useEffect(() => {
226
+ const frame = requestAnimationFrame(() => {
227
+ const input = element.current?.querySelector<HTMLElement>(
228
+ "[data-autofocus], [cmdk-input], input:not([type=hidden]):not(:disabled), textarea:not(:disabled)",
229
+ );
230
+ input?.focus();
231
+ });
232
+ return () => cancelAnimationFrame(frame);
233
+ }, []);
234
+ return <div ref={element}>{children}</div>;
235
+ }
236
+
237
+ export function ComposerContextMenu({
238
+ items,
239
+ addAttachment,
240
+ attachmentAccept,
241
+ onAttachmentError,
242
+ disabled,
243
+ }: ComposerContextMenuProps) {
244
+ const t = useComposerRuntimeAdapters().translate!;
245
+ const [open, setOpen] = useState(false);
246
+ const [contextOpen, setContextOpen] = useState(false);
247
+ const [path, setPath] = useState<string[]>([]);
248
+ const pathRef = useRef(path);
249
+ const [page, setPage] = useState<ComposerContextPage | null>(null);
250
+ const pageRef = useRef(page);
251
+ const itemsRef = useRef(items);
252
+ itemsRef.current = items;
253
+ const [error, setError] = useState<string | null>(null);
254
+ const inputRef = useRef<HTMLInputElement>(null);
255
+ const triggerRef = useRef<HTMLButtonElement>(null);
256
+ const pendingDialog = useRef<ComposerContextDialogSession | null>(null);
257
+ const [dialog, setDialog] = useState<ComposerContextDialogSession | null>(
258
+ null,
259
+ );
260
+ const dialogRef = useRef(dialog);
261
+ dialogRef.current = dialog;
262
+ const dialogAction = dialog ? findAction(items, dialog.id) : undefined;
263
+ const dialogAvailable =
264
+ dialogAction?.picker &&
265
+ typeof dialogAction.picker.presentation === "object" &&
266
+ dialogAction.picker.scopeKey === dialog?.scopeKey;
267
+ useEffect(() => {
268
+ if (dialog && !dialogAvailable) {
269
+ dialogRef.current = null;
270
+ setDialog(null);
271
+ }
272
+ }, [dialog, dialogAvailable]);
273
+ const restoreFocusOnClose = useRef(true);
274
+ const label = t("agentChat.composer.addContext", {
275
+ defaultValue: "Add context",
276
+ });
277
+ const searchContext = t("agentChat.composer.searchContext", {
278
+ defaultValue: "Search context…",
279
+ });
280
+ const uploadLabel = t("agentChat.composer.menu.uploadFile", {
281
+ defaultValue: "Upload File",
282
+ });
283
+ const reportError = useCallback(
284
+ (cause: unknown) => {
285
+ const message = formatAttachmentError(
286
+ cause,
287
+ t("agentChat.composer.contextActionFailed", {
288
+ defaultValue: "Could not add context.",
289
+ }),
290
+ );
291
+ setError(message);
292
+ onAttachmentError?.(message);
293
+ },
294
+ [t, onAttachmentError],
295
+ );
296
+ const dismissPage = useCallback(() => {
297
+ const current = pageRef.current;
298
+ pageRef.current = null;
299
+ setPage(null);
300
+ if (!current) return;
301
+ try {
302
+ (
303
+ findAction(itemsRef.current, current.id)?.onDismiss ?? current.onDismiss
304
+ )?.();
305
+ } catch (cause) {
306
+ reportError(cause);
307
+ }
308
+ }, [reportError]);
309
+ const updatePath = (next: string[]) => {
310
+ pathRef.current = next;
311
+ setPath(next);
312
+ };
313
+ const changeOpen = (next: boolean) => {
314
+ setOpen(next);
315
+ if (!next) {
316
+ dismissPage();
317
+ updatePath([]);
318
+ setContextOpen(false);
319
+ }
320
+ };
321
+ const selectAction = (action: ComposerContextMenuAction) => {
322
+ setError(null);
323
+ try {
324
+ void Promise.resolve(action.onSelect?.()).catch(reportError);
325
+ } catch (cause) {
326
+ reportError(cause);
327
+ }
328
+ };
329
+ const activate = (
330
+ action: ComposerContextMenuAction,
331
+ origin: string[],
332
+ select = true,
333
+ ) => {
334
+ dismissPage();
335
+ const next = { id: action.id, origin, onDismiss: action.onDismiss };
336
+ pageRef.current = next;
337
+ setPage(next);
338
+ updatePath([...origin, action.id]);
339
+ if (select && !action.picker) selectAction(action);
340
+ };
341
+ const currentAction = page ? findAction(items, page.id) : undefined;
342
+ useEffect(() => {
343
+ if (page && !currentAction?.render && !currentAction?.picker) dismissPage();
344
+ }, [page, currentAction, dismissPage]);
345
+
346
+ const renderEntries = (
347
+ entries: readonly ComposerContextMenuItem[],
348
+ origin: string[],
349
+ ): ReactNode => (
350
+ <DropdownMenuGroup>
351
+ {entries.map((entry) => {
352
+ if (entry.picker && typeof entry.picker.presentation === "object") {
353
+ return (
354
+ <DropdownMenuItem
355
+ key={entry.id}
356
+ disabled={entry.disabled}
357
+ onSelect={() => {
358
+ pendingDialog.current = {
359
+ id: entry.id,
360
+ scopeKey: entry.picker?.scopeKey,
361
+ };
362
+ changeOpen(false);
363
+ }}
364
+ >
365
+ {entry.icon}
366
+ {entry.label}
367
+ </DropdownMenuItem>
368
+ );
369
+ }
370
+ const branch = [...origin, entry.id];
371
+ const expanded = branch.every((id, index) => path[index] === id);
372
+ if (entry.children || entry.picker || entry.render) {
373
+ const back = () => {
374
+ dismissPage();
375
+ updatePath(origin);
376
+ };
377
+ const activePage = page;
378
+ return (
379
+ <ContextSubmenu
380
+ key={entry.id}
381
+ label={entry.label}
382
+ icon={entry.icon}
383
+ disabled={entry.disabled}
384
+ open={expanded}
385
+ onOpenChange={(next) => {
386
+ const isOpen = branch.every(
387
+ (id, index) => pathRef.current[index] === id,
388
+ );
389
+ if (next === isOpen) return;
390
+ if (!next) {
391
+ back();
392
+ return;
393
+ }
394
+ setError(null);
395
+ if (entry.children) {
396
+ dismissPage();
397
+ updatePath(branch);
398
+ } else activate(entry, origin);
399
+ }}
400
+ >
401
+ {entry.children ? (
402
+ <ContextMenuPanel
403
+ placeholder={entry.searchPlaceholder ?? searchContext}
404
+ >
405
+ {(query) =>
406
+ renderEntries(
407
+ entry.children.filter((child) =>
408
+ matchesEntry(child, query),
409
+ ),
410
+ branch,
411
+ )
412
+ }
413
+ </ContextMenuPanel>
414
+ ) : entry.picker ? (
415
+ <ComposerContextPicker
416
+ key={JSON.stringify([entry.id, entry.picker.scopeKey])}
417
+ config={entry.picker}
418
+ onClose={() => {
419
+ if (pageRef.current === activePage) changeOpen(false);
420
+ }}
421
+ />
422
+ ) : entry.render && expanded ? (
423
+ <LegacyContextPage>
424
+ {entry.render({
425
+ onBack: back,
426
+ onClose: (options) => {
427
+ if (pageRef.current !== activePage) return;
428
+ restoreFocusOnClose.current =
429
+ options?.restoreFocus !== false;
430
+ changeOpen(false);
431
+ },
432
+ onResume: () => {
433
+ const action = findAction(itemsRef.current, entry.id);
434
+ if (!action?.render) {
435
+ reportError(
436
+ new Error(
437
+ t("agentChat.composer.contextActionFailed", {
438
+ defaultValue: "Could not add context.",
439
+ }),
440
+ ),
441
+ );
442
+ return;
443
+ }
444
+ setOpen(true);
445
+ setContextOpen(true);
446
+ activate(action, origin, false);
447
+ },
448
+ })}
449
+ </LegacyContextPage>
450
+ ) : null}
451
+ </ContextSubmenu>
452
+ );
453
+ }
454
+ return (
455
+ <DropdownMenuItem
456
+ key={entry.id}
457
+ disabled={entry.disabled}
458
+ onSelect={() => {
459
+ changeOpen(false);
460
+ selectAction(entry);
461
+ }}
462
+ >
463
+ {entry.icon}
464
+ {entry.label}
465
+ </DropdownMenuItem>
466
+ );
467
+ })}
468
+ </DropdownMenuGroup>
469
+ );
470
+
471
+ return (
472
+ <>
473
+ {addAttachment && (
474
+ <input
475
+ ref={inputRef}
476
+ type="file"
477
+ multiple
478
+ accept={attachmentAccept}
479
+ hidden
480
+ onChange={(event) => {
481
+ const files = Array.from(event.target.files ?? []);
482
+ event.target.value = "";
483
+ setError(null);
484
+ void Promise.all(
485
+ files.map((file) =>
486
+ Promise.resolve().then(() => addAttachment(file)),
487
+ ),
488
+ ).catch(reportError);
489
+ }}
490
+ />
491
+ )}
492
+ <DropdownMenu open={open} onOpenChange={changeOpen}>
493
+ <Tooltip>
494
+ <TooltipTrigger asChild>
495
+ <DropdownMenuTrigger asChild>
496
+ <Button
497
+ ref={triggerRef}
498
+ type="button"
499
+ variant="ghost"
500
+ size="icon"
501
+ className="size-7 shrink-0"
502
+ disabled={disabled}
503
+ aria-label={label}
504
+ >
505
+ <IconPlus />
506
+ </Button>
507
+ </DropdownMenuTrigger>
508
+ </TooltipTrigger>
509
+ <TooltipContent>{label}</TooltipContent>
510
+ </Tooltip>
511
+ <DropdownMenuContent
512
+ align="start"
513
+ className="w-64"
514
+ data-agent-native-composer-popover="true"
515
+ onCloseAutoFocus={(event) => {
516
+ if (pendingDialog.current) {
517
+ event.preventDefault();
518
+ setDialog(pendingDialog.current);
519
+ pendingDialog.current = null;
520
+ }
521
+ if (!restoreFocusOnClose.current) event.preventDefault();
522
+ restoreFocusOnClose.current = true;
523
+ }}
524
+ >
525
+ <ContextMenuPanel
526
+ placeholder={t("agentChat.composer.menu.search", {
527
+ defaultValue: "Search…",
528
+ })}
529
+ >
530
+ {(query) => (
531
+ <DropdownMenuGroup>
532
+ {addAttachment &&
533
+ uploadLabel
534
+ .toLocaleLowerCase()
535
+ .includes(query.trim().toLocaleLowerCase()) && (
536
+ <DropdownMenuItem
537
+ onSelect={() => {
538
+ changeOpen(false);
539
+ inputRef.current?.click();
540
+ }}
541
+ >
542
+ <IconFile size={16} />
543
+ {uploadLabel}
544
+ </DropdownMenuItem>
545
+ )}
546
+ {items.length > 0 &&
547
+ (label
548
+ .toLocaleLowerCase()
549
+ .includes(query.trim().toLocaleLowerCase()) ||
550
+ items.some((item) => matchesEntry(item, query))) && (
551
+ <ContextSubmenu
552
+ label={label}
553
+ icon={<IconTextRecognition size={16} />}
554
+ open={contextOpen}
555
+ onOpenChange={(next) => {
556
+ setContextOpen(next);
557
+ if (!next) {
558
+ dismissPage();
559
+ updatePath([]);
560
+ }
561
+ }}
562
+ >
563
+ <ContextMenuPanel placeholder={searchContext}>
564
+ {(contextQuery) =>
565
+ renderEntries(
566
+ items.filter((item) =>
567
+ matchesEntry(item, contextQuery),
568
+ ),
569
+ [],
570
+ )
571
+ }
572
+ </ContextMenuPanel>
573
+ </ContextSubmenu>
574
+ )}
575
+ </DropdownMenuGroup>
576
+ )}
577
+ </ContextMenuPanel>
578
+ </DropdownMenuContent>
579
+ </DropdownMenu>
580
+ {dialog &&
581
+ dialogAvailable &&
582
+ dialogAction?.picker &&
583
+ typeof dialogAction.picker.presentation === "object" && (
584
+ <ComposerContextPickerDialog
585
+ key={JSON.stringify([dialog.id, dialogAction.picker.scopeKey])}
586
+ title={dialogAction.label}
587
+ config={dialogAction.picker}
588
+ onClose={() => {
589
+ if (dialogRef.current !== dialog) return;
590
+ dialogRef.current = null;
591
+ setDialog(null);
592
+ try {
593
+ dialogAction.onDismiss?.();
594
+ } catch (cause) {
595
+ reportError(cause);
596
+ }
597
+ }}
598
+ onRestoreFocus={() => {
599
+ if (!dialogRef.current) triggerRef.current?.focus();
600
+ }}
601
+ />
602
+ )}
603
+ {error && (
604
+ <span role="alert" className="text-xs text-destructive">
605
+ {error}
606
+ </span>
607
+ )}
608
+ </>
609
+ );
610
+ }
@@ -0,0 +1,69 @@
1
+ import { useEffect, useRef, type ReactNode } from "react";
2
+
3
+ import { MenuSearchInput } from "../ui/command.js";
4
+
5
+ /** Integrated search with keyboard navigation owned by its Radix menu. */
6
+ export function ComposerContextMenuSearch({
7
+ placeholder,
8
+ value,
9
+ onValueChange,
10
+ leading,
11
+ invalid,
12
+ onSubmit,
13
+ }: {
14
+ placeholder: string;
15
+ value: string;
16
+ onValueChange: (value: string) => void;
17
+ leading?: ReactNode;
18
+ invalid?: boolean;
19
+ onSubmit?: () => void;
20
+ }) {
21
+ const input = useRef<HTMLInputElement>(null);
22
+ useEffect(() => {
23
+ const frame = requestAnimationFrame(() => {
24
+ const menu = input.current?.closest('[role="menu"]');
25
+ // Ancestors can mount in the same frame when a source is resumed.
26
+ if (!menu?.querySelector('[aria-haspopup="menu"][aria-expanded="true"]'))
27
+ input.current?.focus();
28
+ });
29
+ return () => cancelAnimationFrame(frame);
30
+ }, [placeholder]);
31
+ return (
32
+ <MenuSearchInput
33
+ ref={input}
34
+ placeholder={placeholder}
35
+ aria-label={placeholder}
36
+ aria-invalid={invalid}
37
+ value={value}
38
+ onChange={(event) => onValueChange(event.target.value)}
39
+ leading={leading}
40
+ onKeyDown={(event) => {
41
+ const menu = input.current?.closest('[role="menu"]');
42
+ const rows = Array.from(
43
+ menu?.querySelectorAll<HTMLElement>(
44
+ '[role^="menuitem"]:not([data-disabled])',
45
+ ) ?? [],
46
+ ).filter((row) => row.closest('[role="menu"]') === menu);
47
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
48
+ event.preventDefault();
49
+ event.stopPropagation();
50
+ (event.key === "ArrowDown" ? rows[0] : rows.at(-1))?.focus();
51
+ } else if (event.key === "Enter") {
52
+ event.preventDefault();
53
+ event.stopPropagation();
54
+ if (onSubmit) onSubmit();
55
+ else rows[0]?.click();
56
+ } else if (
57
+ event.key.length === 1 ||
58
+ event.key === "ArrowLeft" ||
59
+ event.key === "ArrowRight" ||
60
+ event.key === "Home" ||
61
+ event.key === "End"
62
+ ) {
63
+ // Text entry must not activate Radix's menu typeahead.
64
+ event.stopPropagation();
65
+ }
66
+ }}
67
+ />
68
+ );
69
+ }