@agent-native/toolkit 0.198.7-nightly-20260930213124 → 0.198.7

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 (50) hide show
  1. package/dist/app/chat/AgentPanel.d.ts +4 -3
  2. package/dist/app/chat/AgentPanel.d.ts.map +1 -1
  3. package/dist/app/chat/AgentPanel.js +14 -24
  4. package/dist/app/chat/AgentPanel.js.map +1 -1
  5. package/dist/app/chat/ConnectBuilderCard.d.ts.map +1 -1
  6. package/dist/app/chat/ConnectBuilderCard.js +2 -1
  7. package/dist/app/chat/ConnectBuilderCard.js.map +1 -1
  8. package/dist/app/chat/MultiTabAssistantChat.d.ts.map +1 -1
  9. package/dist/app/chat/MultiTabAssistantChat.js +21 -2
  10. package/dist/app/chat/MultiTabAssistantChat.js.map +1 -1
  11. package/dist/composer/ComposerContextMenu.d.ts +2 -2
  12. package/dist/composer/ComposerContextMenu.d.ts.map +1 -1
  13. package/dist/composer/ComposerContextMenu.js +54 -66
  14. package/dist/composer/ComposerContextMenu.js.map +1 -1
  15. package/dist/composer/ComposerPlusMenu.d.ts +2 -3
  16. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -1
  17. package/dist/composer/ComposerPlusMenu.js +13 -278
  18. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  19. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  20. package/dist/composer/TiptapComposer.js +3 -12
  21. package/dist/composer/TiptapComposer.js.map +1 -1
  22. package/dist/composer/use-composer-panel-placement.d.ts +0 -1
  23. package/dist/composer/use-composer-panel-placement.d.ts.map +1 -1
  24. package/dist/composer/use-composer-panel-placement.js +4 -5
  25. package/dist/composer/use-composer-panel-placement.js.map +1 -1
  26. package/package.json +4 -4
  27. package/src/app/chat/AgentPanel.header.spec.ts +25 -27
  28. package/src/app/chat/AgentPanel.overlay.spec.tsx +14 -51
  29. package/src/app/chat/AgentPanel.tsx +15 -26
  30. package/src/app/chat/ConnectBuilderCard.spec.tsx +45 -0
  31. package/src/app/chat/ConnectBuilderCard.tsx +3 -2
  32. package/src/app/chat/MultiTabAssistantChat.spec.tsx +12 -0
  33. package/src/app/chat/MultiTabAssistantChat.tsx +26 -2
  34. package/src/app/chat/composer/use-agentkit-integration-menu.spec.tsx +4 -2
  35. package/src/composer/ComposerContextMenu.spec.tsx +68 -60
  36. package/src/composer/ComposerContextMenu.tsx +93 -110
  37. package/src/composer/ComposerContextPicker.spec.tsx +16 -14
  38. package/src/composer/ComposerContextPickerDialog.spec.tsx +8 -6
  39. package/src/composer/ComposerPlusMenu.parity.spec.tsx +37 -75
  40. package/src/composer/ComposerPlusMenu.tsx +26 -450
  41. package/src/composer/PromptComposer.context.spec.tsx +112 -18
  42. package/src/composer/TiptapComposer.spec.ts +15 -0
  43. package/src/composer/TiptapComposer.tsx +2 -12
  44. package/src/composer/use-composer-panel-placement.ts +7 -5
  45. package/dist/composer/asset-picker-url.d.ts +0 -10
  46. package/dist/composer/asset-picker-url.d.ts.map +0 -1
  47. package/dist/composer/asset-picker-url.js +0 -56
  48. package/dist/composer/asset-picker-url.js.map +0 -1
  49. package/src/composer/asset-picker-url.spec.ts +0 -56
  50. package/src/composer/asset-picker-url.ts +0 -72
@@ -129,25 +129,32 @@ describe("connected composer menus", () => {
129
129
  await new Promise((resolve) => setTimeout(resolve, 0));
130
130
  });
131
131
  }
132
- const open = () =>
132
+ const openRoot = () =>
133
133
  key(
134
134
  container.querySelector('button[aria-label="Add context"]')!,
135
135
  "ArrowDown",
136
136
  );
137
+ const open = async () => {
138
+ await openRoot();
139
+ if (document.querySelector('[role="menuitem"][aria-haspopup="menu"]'))
140
+ await key(row("Add context"), "ArrowRight");
141
+ };
137
142
  async function click(label: string) {
138
143
  await act(async () => row(label).click());
139
144
  }
140
- it("shows every context choice at the root without menu icons", async () => {
145
+ it("keeps the root menu compact and puts context choices in nested menus", async () => {
141
146
  await render(items, { addAttachment: vi.fn() });
142
- await open();
143
- expect(document.querySelectorAll('[role="searchbox"]')).toHaveLength(1);
144
- expect(row("Upload File").querySelector("svg")).toBeNull();
145
- expect(row("Project brief").querySelector("svg")).toBeNull();
146
- expect(row("Meeting notes")).toBeDefined();
147
- expect(row("Launch reference")).toBeDefined();
148
- expect(menus()[0].textContent).toContain("Documents");
147
+ await openRoot();
148
+ expect(document.querySelectorAll('[role="searchbox"]')).toHaveLength(0);
149
+ expect(row("Upload File").querySelector("svg")).not.toBeNull();
150
+ expect(row("Add context").getAttribute("aria-haspopup")).toBe("menu");
149
151
  expect(menus()).toHaveLength(1);
150
- expect(menus()[0].style.width).toBe("320px");
152
+ expect(menus()[0].classList.contains("w-64")).toBe(true);
153
+ await key(row("Add context"), "ArrowRight");
154
+ expect(row("Documents")).toBeDefined();
155
+ expect(row("Library")).toBeDefined();
156
+ expect(document.querySelectorAll('[role="searchbox"]')).toHaveLength(0);
157
+ expect(menus()).toHaveLength(2);
151
158
  });
152
159
  it("hides the Add context tooltip while the host storage popover is open", async () => {
153
160
  const warning = vi.spyOn(console, "warn").mockImplementation(() => {});
@@ -164,7 +171,7 @@ describe("connected composer menus", () => {
164
171
  ),
165
172
  );
166
173
  });
167
- it("searches descriptions and categories without adding another navigation level", async () => {
174
+ it("navigates nested category menus without a root search field", async () => {
168
175
  await render([
169
176
  {
170
177
  id: "apps",
@@ -180,23 +187,16 @@ describe("connected composer menus", () => {
180
187
  },
181
188
  ]);
182
189
  await open();
183
- const search =
184
- document.querySelector<HTMLInputElement>('[role="searchbox"]')!;
185
- await act(async () => {
186
- Object.getOwnPropertyDescriptor(
187
- HTMLInputElement.prototype,
188
- "value",
189
- )!.set!.call(search, "prototypes");
190
- search.dispatchEvent(new Event("input", { bubbles: true }));
191
- });
190
+ expect(document.querySelector('[role="searchbox"]')).toBeNull();
191
+ expect(row("Workspace apps")).toBeDefined();
192
+ await key(row("Workspace apps"), "ArrowRight");
192
193
  expect(row("DesignInteractive prototypes")).toBeDefined();
193
- expect(menus()[0].textContent).toContain("Workspace apps");
194
- expect(menus()).toHaveLength(1);
195
- await key(search, "Enter");
194
+ expect(menus()).toHaveLength(3);
195
+ await key(row("DesignInteractive prototypes"), "Enter");
196
196
  expect(menus()).toHaveLength(0);
197
197
  });
198
198
  it.each([280, 324, 348, 700])(
199
- "sizes the shared panel to its %ipx composer frame",
199
+ "keeps the shared menu compact in a %ipx composer frame",
200
200
  async (width) => {
201
201
  container.dataset.agentComposerSlot = "root";
202
202
  await render();
@@ -225,8 +225,9 @@ describe("connected composer menus", () => {
225
225
  height: 28,
226
226
  toJSON() {},
227
227
  });
228
- await open();
229
- expect(menus()[0].style.width).toBe(`${width}px`);
228
+ await openRoot();
229
+ expect(menus()[0].classList.contains("w-64")).toBe(true);
230
+ expect(menus()[0].style.width).toBe("");
230
231
  expect(menus()[0].style.maxHeight).toBe("276px");
231
232
  },
232
233
  );
@@ -258,7 +259,7 @@ describe("connected composer menus", () => {
258
259
  DOMRect.fromRect({ x: 24, y: top + 60, width: 28, height: 28 }),
259
260
  );
260
261
  try {
261
- await open();
262
+ await openRoot();
262
263
  expect(menus()[0].style.maxHeight).toBe(`${maxHeight}px`);
263
264
  expect(menus()[0].getAttribute("data-side")).toBe(side);
264
265
  expect(menus()[0].querySelector('[role="menuitem"]')).not.toBeNull();
@@ -297,9 +298,7 @@ describe("connected composer menus", () => {
297
298
  DOMRect.fromRect({ x: 24, y: 150, width: 28, height: 28 }),
298
299
  );
299
300
  try {
300
- await open();
301
- const search =
302
- menus()[0].querySelector<HTMLInputElement>('[role="searchbox"]')!;
301
+ await openRoot();
303
302
  expect(menus()[0].style.maxHeight).toBe("226px");
304
303
  await act(async () => {
305
304
  viewport.offsetTop = 0;
@@ -307,13 +306,12 @@ describe("connected composer menus", () => {
307
306
  viewport.dispatchEvent(new Event("resize"));
308
307
  });
309
308
  expect(menus()[0].style.maxHeight).toBe("66px");
310
- expect(menus()[0].querySelector('[role="searchbox"]')).toBe(search);
311
309
  await act(async () => {
312
310
  viewport.offsetTop = 80;
313
311
  viewport.dispatchEvent(new Event("scroll"));
314
312
  });
315
313
  expect(menus()[0].style.maxHeight).toBe("106px");
316
- await key(search, "Escape");
314
+ await key(row("Add context"), "Escape");
317
315
  expect(removeListener).toHaveBeenCalledWith(
318
316
  "resize",
319
317
  expect.any(Function),
@@ -345,13 +343,14 @@ describe("connected composer menus", () => {
345
343
  },
346
344
  },
347
345
  ]);
348
- await open();
346
+ await openRoot();
347
+ await key(row("Add context"), "ArrowLeft");
349
348
  expect(menus()[0].getAttribute("dir")).toBe("rtl");
350
349
  await key(row("Source"), "ArrowLeft");
351
350
  expect(row("One")).toBeDefined();
352
351
  expect(menus()[1].getAttribute("dir")).toBe("rtl");
353
352
  });
354
- it("follows host resizing while keeping the context search open", async () => {
353
+ it("follows host resizing while keeping nested context menus open", async () => {
355
354
  const observations: {
356
355
  target: Element;
357
356
  resize: () => void;
@@ -379,27 +378,20 @@ describe("connected composer menus", () => {
379
378
  );
380
379
  await render();
381
380
  await open();
382
- const search =
383
- document.querySelector<HTMLInputElement>('[role="searchbox"]')!;
384
- await act(async () => {
385
- Object.getOwnPropertyDescriptor(
386
- HTMLInputElement.prototype,
387
- "value",
388
- )!.set!.call(search, "brief");
389
- search.dispatchEvent(new Event("input", { bubbles: true }));
390
- });
381
+ await key(row("Documents"), "ArrowRight");
391
382
  const observation = observations.find(
392
383
  ({ target }) => target === container,
393
384
  )!;
394
385
  expect(observation).toBeDefined();
395
386
  for (width of [348, 280, 700]) {
396
387
  await act(async () => observation.resize());
397
- expect(menus()[0].style.width).toBe(`${width}px`);
398
- expect(document.querySelector('[role="searchbox"]')).toBe(search);
399
- expect(search.value).toBe("brief");
388
+ expect(menus()[0].style.width).toBe("");
389
+ expect(menus()[0].classList.contains("w-64")).toBe(true);
400
390
  expect(row("Project brief")).toBeDefined();
401
391
  }
402
- await key(search, "Escape");
392
+ await key(row("Project brief"), "Escape");
393
+ await key(row("Documents"), "Escape");
394
+ await key(row("Add context"), "Escape");
403
395
  expect(observation.disconnect).toHaveBeenCalled();
404
396
  });
405
397
  it("closes a context picker when its composer becomes disabled", async () => {
@@ -443,7 +435,7 @@ describe("connected composer menus", () => {
443
435
  expect(onDisabledFocus).toHaveBeenCalledOnce();
444
436
  expect(document.activeElement).toBe(draft);
445
437
  });
446
- it("flattens nested categories into one root menu", async () => {
438
+ it("preserves nested category levels through selection", async () => {
447
439
  const select = vi.fn();
448
440
  await render(
449
441
  [
@@ -461,22 +453,38 @@ describe("connected composer menus", () => {
461
453
  { addAttachment: vi.fn() },
462
454
  );
463
455
  await open();
456
+ expect(row("Documents")).toBeDefined();
457
+ expect(row("Library")).toBeDefined();
458
+ expect(
459
+ menus().some((menu) => menu.textContent?.includes("Project brief")),
460
+ ).toBe(false);
461
+ expect(document.querySelectorAll('[role="searchbox"]')).toHaveLength(0);
462
+ await key(row("Documents"), "ArrowRight");
464
463
  expect(row("Project brief")).toBeDefined();
465
464
  expect(row("Notes")).toBeDefined();
466
- expect(row("Launch reference")).toBeDefined();
467
- expect(menus()[0].textContent).toMatch(/Documents.*Library/);
468
- expect(document.querySelectorAll('[role="searchbox"]')).toHaveLength(1);
465
+ expect(menus()).toHaveLength(3);
469
466
  await click("Project brief");
470
467
  expect(select).toHaveBeenCalledOnce();
471
468
  expect(menus()).toHaveLength(0);
472
469
  });
473
- it("keeps native keyboard navigation within the root menu", async () => {
470
+ it("supports native keyboard navigation across nested context menus", async () => {
474
471
  await render();
475
- await open();
476
- expect(menus()).toHaveLength(1);
472
+ await openRoot();
473
+ await key(row("Add context"), "ArrowRight");
474
+ await key(row("Documents"), "ArrowRight");
475
+ expect(menus()).toHaveLength(3);
477
476
  await key(row("Project brief"), "ArrowDown");
478
- expect(document.activeElement).toBe(row("Meeting notes"));
477
+ expect(document.activeElement).toBe(row("Archive"));
478
+ await key(row("Archive"), "ArrowRight");
479
+ expect(row("Meeting notes")).toBeDefined();
480
+ expect(menus()).toHaveLength(4);
479
481
  await key(row("Meeting notes"), "Escape");
482
+ expect(menus()).toHaveLength(3);
483
+ await key(row("Archive"), "Escape");
484
+ expect(menus()).toHaveLength(2);
485
+ await key(row("Documents"), "Escape");
486
+ expect(menus()).toHaveLength(1);
487
+ await key(row("Add context"), "Escape");
480
488
  expect(menus()).toHaveLength(0);
481
489
  });
482
490
  it("preserves native upload accepts, cancellation, multiple files and errors", async () => {
@@ -495,7 +503,7 @@ describe("connected composer menus", () => {
495
503
  const picker = vi.spyOn(input, "click");
496
504
  expect(input.multiple).toBe(true);
497
505
  expect(input.accept).toBe("text/plain");
498
- await open();
506
+ await openRoot();
499
507
  await click("Upload File");
500
508
  expect(picker).toHaveBeenCalledOnce();
501
509
  await act(async () =>
@@ -513,7 +521,7 @@ describe("connected composer menus", () => {
513
521
  it("requests gated upload after the menu closes", async () => {
514
522
  const onAttachmentRequest = vi.fn();
515
523
  await render([], { onAttachmentRequest });
516
- await open();
524
+ await openRoot();
517
525
  await click("Upload File");
518
526
  await act(
519
527
  async () =>
@@ -555,7 +563,7 @@ describe("connected composer menus", () => {
555
563
  expect(select).toHaveBeenCalledOnce();
556
564
  expect(container.querySelector('[aria-label="Prompt draft"]')).toBe(draft);
557
565
  await act(async () => controls.onBack());
558
- expect(menus()).toHaveLength(1);
566
+ expect(menus()).toHaveLength(2);
559
567
  expect(dismiss).toHaveBeenCalledTimes(2);
560
568
  });
561
569
  it("ignores stale legacy close and reports resume of a removed source", async () => {
@@ -578,7 +586,7 @@ describe("connected composer menus", () => {
578
586
  const old = controls;
579
587
  await act(async () => controls.onBack());
580
588
  await act(async () => old.onClose());
581
- expect(menus()).toHaveLength(1);
589
+ expect(menus()).toHaveLength(2);
582
590
  await render([]);
583
591
  await act(async () => old.onResume());
584
592
  expect(document.querySelector('[role="alert"]')?.textContent).toBe(
@@ -602,7 +610,7 @@ describe("connected composer menus", () => {
602
610
  ]);
603
611
  await open();
604
612
  await click("Reference");
605
- expect(menus()).toHaveLength(2);
613
+ expect(menus()).toHaveLength(3);
606
614
  expect(document.querySelector('[role="alert"]')?.textContent).toBe(
607
615
  "Reference unavailable",
608
616
  );
@@ -1,4 +1,4 @@
1
- import { IconPlus } from "@tabler/icons-react";
1
+ import { IconFile, IconPlus, IconTextRecognition } from "@tabler/icons-react";
2
2
  import {
3
3
  useCallback,
4
4
  useEffect,
@@ -14,7 +14,6 @@ import {
14
14
  DropdownMenuCheckboxItem,
15
15
  DropdownMenuGroup,
16
16
  DropdownMenuItem,
17
- DropdownMenuLabel,
18
17
  DropdownMenuSub,
19
18
  DropdownMenuSubContent,
20
19
  DropdownMenuSubTrigger,
@@ -23,7 +22,6 @@ import {
23
22
  import { Skeleton } from "../ui/skeleton.js";
24
23
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
25
24
  import { formatAttachmentError } from "./attachment-accept.js";
26
- import { ComposerContextMenuSearch } from "./ComposerContextMenuSearch.js";
27
25
  import {
28
26
  ComposerContextPicker,
29
27
  type ComposerContextPickerConfig,
@@ -83,7 +81,7 @@ export type ComposerContextMenuItem =
83
81
  | ComposerContextMenuCategory;
84
82
  export interface ComposerContextMenuProps {
85
83
  items: readonly ComposerContextMenuItem[];
86
- onSearchChange?: (search: string) => void;
84
+ menuActionItems?: readonly ComposerContextMenuItem[];
87
85
  loading?: boolean;
88
86
  error?: string;
89
87
  onRetry?: () => void;
@@ -123,17 +121,6 @@ function findAction(
123
121
  }
124
122
  }
125
123
 
126
- function flattenMenuItems(
127
- items: readonly ComposerContextMenuItem[],
128
- inheritedDisabled = false,
129
- ): ComposerContextMenuAction[] {
130
- return items.flatMap((item) => {
131
- const disabled = inheritedDisabled || item.disabled === true;
132
- if (item.children) return flattenMenuItems(item.children, disabled);
133
- return [disabled ? { ...item, disabled: true } : item];
134
- });
135
- }
136
-
137
124
  export function getComposerContextMenuEntries(
138
125
  items: readonly ComposerContextMenuItem[],
139
126
  path: readonly string[],
@@ -176,12 +163,14 @@ export function getComposerContextMenuEntries(
176
163
 
177
164
  function ContextSubmenu({
178
165
  label,
166
+ icon,
179
167
  disabled,
180
168
  open,
181
169
  onOpenChange,
182
170
  children,
183
171
  }: {
184
172
  label: string;
173
+ icon?: ReactNode;
185
174
  disabled?: boolean;
186
175
  open: boolean;
187
176
  onOpenChange: (open: boolean) => void;
@@ -191,6 +180,7 @@ function ContextSubmenu({
191
180
  return (
192
181
  <DropdownMenuSub open={open} onOpenChange={onOpenChange}>
193
182
  <DropdownMenuSubTrigger ref={trigger} disabled={disabled}>
183
+ {icon}
194
184
  <span className="min-w-0 truncate">{label}</span>
195
185
  </DropdownMenuSubTrigger>
196
186
  <DropdownMenuSubContent
@@ -258,7 +248,7 @@ function ContextEntryLabel({ label, description }: ComposerContextMenuEntry) {
258
248
 
259
249
  export function ComposerContextMenu({
260
250
  items,
261
- onSearchChange,
251
+ menuActionItems = [],
262
252
  loading,
263
253
  error: searchError,
264
254
  onRetry,
@@ -274,6 +264,7 @@ export function ComposerContextMenu({
274
264
  disabled,
275
265
  }: ComposerContextMenuProps) {
276
266
  const t = useComposerRuntimeAdapters().translate!;
267
+ const allItems = [...menuActionItems, ...items];
277
268
  const onDisabledFocusRef = useRef(onDisabledFocus);
278
269
  onDisabledFocusRef.current = onDisabledFocus;
279
270
  const disabledFocusFrame = useRef<number | null>(null);
@@ -285,15 +276,14 @@ export function ComposerContextMenu({
285
276
  [],
286
277
  );
287
278
  const [internalOpen, setInternalOpen] = useState(false);
288
- const [search, setSearch] = useState("");
289
279
  const open = controlledOpen ?? internalOpen;
290
280
  const [triggerTooltipOpen, setTriggerTooltipOpen] = useState(false);
291
281
  const [path, setPath] = useState<string[]>([]);
292
282
  const pathRef = useRef(path);
293
283
  const [page, setPage] = useState<ComposerContextPage | null>(null);
294
284
  const pageRef = useRef(page);
295
- const itemsRef = useRef(items);
296
- itemsRef.current = items;
285
+ const itemsRef = useRef(allItems);
286
+ itemsRef.current = allItems;
297
287
  const [error, setError] = useState<string | null>(null);
298
288
  const inputRef = useRef<HTMLInputElement>(null);
299
289
  const triggerRef = useRef<HTMLButtonElement>(null);
@@ -305,7 +295,7 @@ export function ComposerContextMenu({
305
295
  );
306
296
  const dialogRef = useRef(dialog);
307
297
  dialogRef.current = dialog;
308
- const dialogAction = dialog ? findAction(items, dialog.id) : undefined;
298
+ const dialogAction = dialog ? findAction(allItems, dialog.id) : undefined;
309
299
  const dialogAvailable =
310
300
  !dialogAction?.disabled &&
311
301
  dialogAction?.picker &&
@@ -361,13 +351,11 @@ export function ComposerContextMenu({
361
351
  if (controlledOpen === undefined) setInternalOpen(next);
362
352
  onOpenChange?.(next);
363
353
  if (!next) {
364
- setSearch("");
365
- onSearchChange?.("");
366
354
  dismissPage();
367
355
  updatePath([]);
368
356
  }
369
357
  },
370
- [controlledOpen, dismissPage, onOpenChange, onSearchChange, updatePath],
358
+ [controlledOpen, dismissPage, onOpenChange, updatePath],
371
359
  );
372
360
  useEffect(() => {
373
361
  if (open || !pendingAttachmentRequest.current) return;
@@ -419,7 +407,7 @@ export function ComposerContextMenu({
419
407
  updatePath([...origin, action.id]);
420
408
  if (select && !action.picker) selectAction(action);
421
409
  };
422
- const currentAction = page ? findAction(items, page.id) : undefined;
410
+ const currentAction = page ? findAction(allItems, page.id) : undefined;
423
411
  useEffect(() => {
424
412
  if (
425
413
  page &&
@@ -432,11 +420,38 @@ export function ComposerContextMenu({
432
420
  }, [page, currentAction, dismissPage, updatePath]);
433
421
 
434
422
  const renderEntries = (
435
- entries: readonly ComposerContextMenuAction[],
423
+ entries: readonly ComposerContextMenuItem[],
436
424
  origin: string[],
437
425
  ): ReactNode => (
438
426
  <>
439
427
  {entries.map((entry) => {
428
+ const branch = [...origin, entry.id];
429
+ const expanded = branch.every((id, index) => path[index] === id);
430
+ if (entry.children) {
431
+ return (
432
+ <ContextSubmenu
433
+ key={entry.id}
434
+ label={entry.label}
435
+ disabled={entry.disabled}
436
+ open={expanded}
437
+ onOpenChange={(next) => {
438
+ const isOpen = branch.every(
439
+ (id, index) => pathRef.current[index] === id,
440
+ );
441
+ if (next === isOpen) return;
442
+ if (!next) {
443
+ dismissPage();
444
+ updatePath(origin);
445
+ return;
446
+ }
447
+ setError(null);
448
+ updatePath(branch);
449
+ }}
450
+ >
451
+ {renderEntries(entry.children, branch)}
452
+ </ContextSubmenu>
453
+ );
454
+ }
440
455
  if (entry.picker && typeof entry.picker.presentation === "object") {
441
456
  return (
442
457
  <DropdownMenuItem
@@ -454,8 +469,6 @@ export function ComposerContextMenu({
454
469
  </DropdownMenuItem>
455
470
  );
456
471
  }
457
- const branch = [...origin, entry.id];
458
- const expanded = branch.every((id, index) => path[index] === id);
459
472
  if (entry.picker || entry.render) {
460
473
  const back = () => {
461
474
  dismissPage();
@@ -551,23 +564,6 @@ export function ComposerContextMenu({
551
564
  </>
552
565
  );
553
566
 
554
- const matches = (entries: readonly ComposerContextMenuItem[]) =>
555
- flattenMenuItems(getComposerContextMenuEntries(entries, [], search));
556
- const rootEntries = matches(items.filter((item) => !item.children));
557
- const groups = items.filter((item): item is ComposerContextMenuCategory =>
558
- Boolean(item.children),
559
- );
560
- const showUpload =
561
- (addAttachment || onAttachmentRequest) &&
562
- (!search.trim() ||
563
- uploadLabel
564
- .toLocaleLowerCase()
565
- .includes(search.trim().toLocaleLowerCase()));
566
- const hasMatches =
567
- showUpload ||
568
- rootEntries.length > 0 ||
569
- groups.some((group) => matches([group]).length > 0);
570
-
571
567
  return (
572
568
  <>
573
569
  {addAttachment && (
@@ -624,11 +620,9 @@ export function ComposerContextMenu({
624
620
  alignOffset={placement.alignOffset}
625
621
  collisionPadding={12}
626
622
  style={{
627
- width: placement.width,
628
- maxWidth: "calc(100vw - 24px)",
629
623
  maxHeight: placement.maxHeight,
630
624
  }}
631
- className="@container flex flex-col rounded-xl p-1 [&_[role^=menuitem]]:min-h-10 [&_[role^=menuitem]]:px-3 [&_[role^=menuitem]]:rounded-lg"
625
+ className="@container flex w-64 max-w-[calc(100vw-24px)] flex-col rounded-xl p-1 [&_[role^=menuitem]]:min-h-10 [&_[role^=menuitem]]:px-3 [&_[role^=menuitem]]:rounded-lg"
632
626
  data-agent-native-composer-popover="true"
633
627
  onCloseAutoFocus={(event) => {
634
628
  if (pendingDialog.current) {
@@ -645,24 +639,9 @@ export function ComposerContextMenu({
645
639
  restoreFocusOnClose.current = true;
646
640
  }}
647
641
  >
648
- <ComposerContextMenuSearch
649
- placeholder={t("agentChat.composer.searchContext", {
650
- defaultValue: "Search context…",
651
- })}
652
- value={search}
653
- onValueChange={(value) => {
654
- setSearch(value);
655
- onSearchChange?.(value);
656
- }}
657
- />
658
642
  <div className="min-h-0 overflow-y-auto">
659
643
  <DropdownMenuGroup>
660
- {(showUpload || rootEntries.length > 0) && (
661
- <DropdownMenuLabel className="px-3 font-normal text-muted-foreground">
662
- {label}
663
- </DropdownMenuLabel>
664
- )}
665
- {showUpload && (
644
+ {(addAttachment || onAttachmentRequest) && (
666
645
  <DropdownMenuItem
667
646
  onSelect={() => {
668
647
  if (addAttachment) {
@@ -674,57 +653,61 @@ export function ComposerContextMenu({
674
653
  }
675
654
  }}
676
655
  >
656
+ <IconFile size={16} />
677
657
  {uploadLabel}
678
658
  </DropdownMenuItem>
679
659
  )}
680
- {renderEntries(rootEntries, [])}
660
+ {(items.length > 0 || loading || searchError) && (
661
+ <ContextSubmenu
662
+ label={label}
663
+ icon={<IconTextRecognition size={16} />}
664
+ open={path[0] === "add-context"}
665
+ onOpenChange={(next) => {
666
+ if (next === (pathRef.current[0] === "add-context")) return;
667
+ if (!next) {
668
+ dismissPage();
669
+ updatePath([]);
670
+ return;
671
+ }
672
+ setError(null);
673
+ updatePath(["add-context"]);
674
+ }}
675
+ >
676
+ {renderEntries(items, ["add-context"])}
677
+ {loading && (
678
+ <div
679
+ role="status"
680
+ aria-label={t("agentChat.composer.contextPending", {
681
+ defaultValue: "Loading context…",
682
+ })}
683
+ className="grid gap-2 p-3"
684
+ >
685
+ <Skeleton className="h-5 w-2/3" />
686
+ <Skeleton className="h-5 w-1/2" />
687
+ </div>
688
+ )}
689
+ {searchError && (
690
+ <div
691
+ role="alert"
692
+ className="px-3 py-2 text-sm text-destructive"
693
+ >
694
+ {searchError}
695
+ </div>
696
+ )}
697
+ {searchError && onRetry && (
698
+ <DropdownMenuItem
699
+ onSelect={(event) => {
700
+ event.preventDefault();
701
+ onRetry();
702
+ }}
703
+ >
704
+ {t("agentChat.common.retry", { defaultValue: "Retry" })}
705
+ </DropdownMenuItem>
706
+ )}
707
+ </ContextSubmenu>
708
+ )}
709
+ {renderEntries(menuActionItems, [])}
681
710
  </DropdownMenuGroup>
682
- {groups.map((group) => {
683
- const entries = matches([group]);
684
- if (!entries.length) return null;
685
- return (
686
- <DropdownMenuGroup key={group.id}>
687
- <DropdownMenuLabel className="px-3 font-normal text-muted-foreground">
688
- {group.label}
689
- </DropdownMenuLabel>
690
- {renderEntries(entries, [])}
691
- </DropdownMenuGroup>
692
- );
693
- })}
694
- {loading && (
695
- <div
696
- role="status"
697
- aria-label={t("agentChat.composer.contextPending", {
698
- defaultValue: "Loading context…",
699
- })}
700
- className="grid gap-2 p-3"
701
- >
702
- <Skeleton className="h-5 w-2/3" />
703
- <Skeleton className="h-5 w-1/2" />
704
- </div>
705
- )}
706
- {searchError && (
707
- <div role="alert" className="px-3 py-2 text-sm text-destructive">
708
- {searchError}
709
- </div>
710
- )}
711
- {searchError && onRetry && (
712
- <DropdownMenuItem
713
- onSelect={(event) => {
714
- event.preventDefault();
715
- onRetry();
716
- }}
717
- >
718
- {t("agentChat.common.retry", { defaultValue: "Retry" })}
719
- </DropdownMenuItem>
720
- )}
721
- {!hasMatches && !loading && !searchError && (
722
- <div role="status" className="p-3 text-sm text-muted-foreground">
723
- {t("agentChat.composer.noContextResults", {
724
- defaultValue: "No matching context.",
725
- })}
726
- </div>
727
- )}
728
711
  </div>
729
712
  </DropdownMenuContent>
730
713
  </DropdownMenu>