@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.
- package/dist/app/chat/AgentPanel.d.ts +4 -3
- package/dist/app/chat/AgentPanel.d.ts.map +1 -1
- package/dist/app/chat/AgentPanel.js +14 -24
- package/dist/app/chat/AgentPanel.js.map +1 -1
- package/dist/app/chat/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/app/chat/ConnectBuilderCard.js +2 -1
- package/dist/app/chat/ConnectBuilderCard.js.map +1 -1
- package/dist/app/chat/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/MultiTabAssistantChat.js +21 -2
- package/dist/app/chat/MultiTabAssistantChat.js.map +1 -1
- package/dist/composer/ComposerContextMenu.d.ts +2 -2
- package/dist/composer/ComposerContextMenu.d.ts.map +1 -1
- package/dist/composer/ComposerContextMenu.js +54 -66
- package/dist/composer/ComposerContextMenu.js.map +1 -1
- package/dist/composer/ComposerPlusMenu.d.ts +2 -3
- package/dist/composer/ComposerPlusMenu.d.ts.map +1 -1
- package/dist/composer/ComposerPlusMenu.js +13 -278
- package/dist/composer/ComposerPlusMenu.js.map +1 -1
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +3 -12
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/composer/use-composer-panel-placement.d.ts +0 -1
- package/dist/composer/use-composer-panel-placement.d.ts.map +1 -1
- package/dist/composer/use-composer-panel-placement.js +4 -5
- package/dist/composer/use-composer-panel-placement.js.map +1 -1
- package/package.json +4 -4
- package/src/app/chat/AgentPanel.header.spec.ts +25 -27
- package/src/app/chat/AgentPanel.overlay.spec.tsx +14 -51
- package/src/app/chat/AgentPanel.tsx +15 -26
- package/src/app/chat/ConnectBuilderCard.spec.tsx +45 -0
- package/src/app/chat/ConnectBuilderCard.tsx +3 -2
- package/src/app/chat/MultiTabAssistantChat.spec.tsx +12 -0
- package/src/app/chat/MultiTabAssistantChat.tsx +26 -2
- package/src/app/chat/composer/use-agentkit-integration-menu.spec.tsx +4 -2
- package/src/composer/ComposerContextMenu.spec.tsx +68 -60
- package/src/composer/ComposerContextMenu.tsx +93 -110
- package/src/composer/ComposerContextPicker.spec.tsx +16 -14
- package/src/composer/ComposerContextPickerDialog.spec.tsx +8 -6
- package/src/composer/ComposerPlusMenu.parity.spec.tsx +37 -75
- package/src/composer/ComposerPlusMenu.tsx +26 -450
- package/src/composer/PromptComposer.context.spec.tsx +112 -18
- package/src/composer/TiptapComposer.spec.ts +15 -0
- package/src/composer/TiptapComposer.tsx +2 -12
- package/src/composer/use-composer-panel-placement.ts +7 -5
- package/dist/composer/asset-picker-url.d.ts +0 -10
- package/dist/composer/asset-picker-url.d.ts.map +0 -1
- package/dist/composer/asset-picker-url.js +0 -56
- package/dist/composer/asset-picker-url.js.map +0 -1
- package/src/composer/asset-picker-url.spec.ts +0 -56
- 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
|
|
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("
|
|
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
|
|
143
|
-
expect(document.querySelectorAll('[role="searchbox"]')).toHaveLength(
|
|
144
|
-
expect(row("Upload File").querySelector("svg")).toBeNull();
|
|
145
|
-
expect(row("
|
|
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].
|
|
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("
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
await
|
|
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()
|
|
194
|
-
|
|
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
|
-
"
|
|
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
|
|
229
|
-
expect(menus()[0].
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
398
|
-
expect(
|
|
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(
|
|
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("
|
|
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(
|
|
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("
|
|
470
|
+
it("supports native keyboard navigation across nested context menus", async () => {
|
|
474
471
|
await render();
|
|
475
|
-
await
|
|
476
|
-
|
|
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("
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
296
|
-
itemsRef.current =
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
|
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
|
-
{(
|
|
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
|
-
{
|
|
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>
|