@agent-native/toolkit 0.219.0 → 0.220.0-nightly-20261023212848
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/AgentKitAssistantChat.d.ts +2 -0
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +10 -2
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/MultiTabAssistantChat.js +3 -1
- package/dist/app/chat/MultiTabAssistantChat.js.map +1 -1
- package/dist/app/chat/chat/surface-types.d.ts +2 -0
- package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
- package/dist/app/chat/chat/surface-types.js.map +1 -1
- package/dist/composer/ComposerContextMenu.d.ts.map +1 -1
- package/dist/composer/ComposerContextMenu.js +27 -2
- package/dist/composer/ComposerContextMenu.js.map +1 -1
- package/dist/composer/ComposerPlusMenu.d.ts.map +1 -1
- package/dist/composer/ComposerPlusMenu.js +50 -4
- package/dist/composer/ComposerPlusMenu.js.map +1 -1
- package/package.json +4 -4
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +57 -0
- package/src/app/chat/AgentKitAssistantChat.tsx +14 -2
- package/src/app/chat/MultiTabAssistantChat.spec.tsx +17 -0
- package/src/app/chat/MultiTabAssistantChat.tsx +5 -0
- package/src/app/chat/chat/surface-types.ts +2 -0
- package/src/composer/ComposerContextMenu.tsx +32 -3
- package/src/composer/ComposerPlusMenu.parity.spec.tsx +96 -2
- package/src/composer/ComposerPlusMenu.tsx +48 -4
|
@@ -62,6 +62,8 @@ export interface AssistantChatSendOptions {
|
|
|
62
62
|
approvedToolCalls?: string[];
|
|
63
63
|
/** Send as a protocol continuation that stays out of visible history. */
|
|
64
64
|
hideUserMessage?: boolean;
|
|
65
|
+
/** The app surface this send came from, reported with the turn's analytics. */
|
|
66
|
+
surface?: string;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
export type AssistantChatSubmitResult =
|
|
@@ -215,7 +215,7 @@ function ContextSubmenu({
|
|
|
215
215
|
icon?: ReactNode;
|
|
216
216
|
disabled?: boolean;
|
|
217
217
|
open: boolean;
|
|
218
|
-
onOpenChange: (open: boolean) => void;
|
|
218
|
+
onOpenChange: (open: boolean, reason?: "escape") => void;
|
|
219
219
|
children: ReactNode;
|
|
220
220
|
}) {
|
|
221
221
|
const trigger = useRef<HTMLDivElement>(null);
|
|
@@ -244,7 +244,7 @@ function ContextSubmenu({
|
|
|
244
244
|
onEscapeKeyDown={(event) => {
|
|
245
245
|
event.preventDefault();
|
|
246
246
|
event.stopPropagation();
|
|
247
|
-
onOpenChange(false);
|
|
247
|
+
onOpenChange(false, "escape");
|
|
248
248
|
trigger.current?.focus();
|
|
249
249
|
}}
|
|
250
250
|
>
|
|
@@ -312,10 +312,14 @@ export function ComposerContextMenu({
|
|
|
312
312
|
const onDisabledFocusRef = useRef(onDisabledFocus);
|
|
313
313
|
onDisabledFocusRef.current = onDisabledFocus;
|
|
314
314
|
const disabledFocusFrame = useRef<number | null>(null);
|
|
315
|
+
const resumedPageClose = useRef<string | null>(null);
|
|
316
|
+
const resumedPageCloseFrame = useRef<number | null>(null);
|
|
315
317
|
useEffect(
|
|
316
318
|
() => () => {
|
|
317
319
|
if (disabledFocusFrame.current !== null)
|
|
318
320
|
window.cancelAnimationFrame(disabledFocusFrame.current);
|
|
321
|
+
if (resumedPageCloseFrame.current !== null)
|
|
322
|
+
window.cancelAnimationFrame(resumedPageCloseFrame.current);
|
|
319
323
|
},
|
|
320
324
|
[],
|
|
321
325
|
);
|
|
@@ -545,7 +549,19 @@ export function ComposerContextMenu({
|
|
|
545
549
|
label={entry.label}
|
|
546
550
|
disabled={entry.disabled}
|
|
547
551
|
open={expanded}
|
|
548
|
-
onOpenChange={(next) => {
|
|
552
|
+
onOpenChange={(next, reason) => {
|
|
553
|
+
if (
|
|
554
|
+
!next &&
|
|
555
|
+
reason !== "escape" &&
|
|
556
|
+
resumedPageClose.current === entry.id
|
|
557
|
+
) {
|
|
558
|
+
resumedPageClose.current = null;
|
|
559
|
+
if (resumedPageCloseFrame.current !== null) {
|
|
560
|
+
window.cancelAnimationFrame(resumedPageCloseFrame.current);
|
|
561
|
+
resumedPageCloseFrame.current = null;
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
549
565
|
const isOpen = branch.every(
|
|
550
566
|
(id, index) => pathRef.current[index] === id,
|
|
551
567
|
);
|
|
@@ -588,6 +604,19 @@ export function ComposerContextMenu({
|
|
|
588
604
|
);
|
|
589
605
|
return;
|
|
590
606
|
}
|
|
607
|
+
// Radix can emit one stale close after native-picker focus returns.
|
|
608
|
+
resumedPageClose.current = entry.id;
|
|
609
|
+
if (resumedPageCloseFrame.current !== null)
|
|
610
|
+
window.cancelAnimationFrame(
|
|
611
|
+
resumedPageCloseFrame.current,
|
|
612
|
+
);
|
|
613
|
+
resumedPageCloseFrame.current =
|
|
614
|
+
window.requestAnimationFrame(() => {
|
|
615
|
+
resumedPageCloseFrame.current = null;
|
|
616
|
+
if (resumedPageClose.current === entry.id)
|
|
617
|
+
resumedPageClose.current = null;
|
|
618
|
+
});
|
|
619
|
+
triggerRef.current?.focus();
|
|
591
620
|
changeOpen(true);
|
|
592
621
|
activate(action, origin, false);
|
|
593
622
|
},
|
|
@@ -71,7 +71,10 @@ afterEach(() => {
|
|
|
71
71
|
async function settle() {
|
|
72
72
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
73
73
|
}
|
|
74
|
-
async function mount(
|
|
74
|
+
async function mount(
|
|
75
|
+
props: Partial<PromptComposerProps> = {},
|
|
76
|
+
settleAfterRender = true,
|
|
77
|
+
) {
|
|
75
78
|
const composerRef = React.createRef<TiptapComposerHandle>();
|
|
76
79
|
const onSubmit = vi.fn();
|
|
77
80
|
await act(async () => {
|
|
@@ -92,7 +95,7 @@ async function mount(props: Partial<PromptComposerProps> = {}) {
|
|
|
92
95
|
/>
|
|
93
96
|
</ComposerRuntimeAdaptersProvider>,
|
|
94
97
|
);
|
|
95
|
-
await settle();
|
|
98
|
+
if (settleAfterRender) await settle();
|
|
96
99
|
});
|
|
97
100
|
return { composerRef, onSubmit };
|
|
98
101
|
}
|
|
@@ -722,4 +725,95 @@ describe("shared default action preservation", () => {
|
|
|
722
725
|
expect(document.querySelector('[role="status"]')).toBeNull();
|
|
723
726
|
expect(item("Upload skill file")).toBeDefined();
|
|
724
727
|
});
|
|
728
|
+
|
|
729
|
+
it("resumes skill-file review after the native picker dismisses the menu", async () => {
|
|
730
|
+
await mount();
|
|
731
|
+
await open();
|
|
732
|
+
await choose("Create Skill");
|
|
733
|
+
await choose("Upload skill file");
|
|
734
|
+
|
|
735
|
+
await act(async () => {
|
|
736
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const input = container.querySelector<HTMLInputElement>(
|
|
740
|
+
'input[accept=".md,text/markdown"]',
|
|
741
|
+
)!;
|
|
742
|
+
const file = new File(["# Test skill\nDo the test."], "Test Skill.md", {
|
|
743
|
+
type: "text/markdown",
|
|
744
|
+
});
|
|
745
|
+
Object.defineProperty(input, "files", {
|
|
746
|
+
configurable: true,
|
|
747
|
+
value: [file],
|
|
748
|
+
});
|
|
749
|
+
await act(async () => {
|
|
750
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
751
|
+
await settle();
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
const form = document.querySelector<HTMLFormElement>("[role=menu] form");
|
|
755
|
+
expect(form).not.toBeNull();
|
|
756
|
+
expect(form?.textContent).toContain(
|
|
757
|
+
"Review the content from Test Skill.md before saving.",
|
|
758
|
+
);
|
|
759
|
+
expect(form?.querySelector("textarea")?.value).toBe(
|
|
760
|
+
"# Test skill\nDo the test.",
|
|
761
|
+
);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it("restores the skill submenu when the native picker is canceled", async () => {
|
|
765
|
+
await mount();
|
|
766
|
+
await open();
|
|
767
|
+
await choose("Create Skill");
|
|
768
|
+
await choose("Upload skill file");
|
|
769
|
+
|
|
770
|
+
await act(async () => {
|
|
771
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
const input = container.querySelector<HTMLInputElement>(
|
|
775
|
+
'input[accept=".md,text/markdown"]',
|
|
776
|
+
)!;
|
|
777
|
+
await act(async () => {
|
|
778
|
+
input.dispatchEvent(new Event("cancel"));
|
|
779
|
+
await settle();
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
expect(labels()).toContain("Upload skill file");
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it("does not resume the skill page if the composer is disabled", async () => {
|
|
786
|
+
await mount();
|
|
787
|
+
await open();
|
|
788
|
+
await choose("Create Skill");
|
|
789
|
+
await choose("Upload skill file");
|
|
790
|
+
|
|
791
|
+
const input = container.querySelector<HTMLInputElement>(
|
|
792
|
+
'input[accept=".md,text/markdown"]',
|
|
793
|
+
)!;
|
|
794
|
+
vi.useFakeTimers();
|
|
795
|
+
try {
|
|
796
|
+
const scheduledTimeouts = vi.spyOn(globalThis, "setTimeout");
|
|
797
|
+
const clearedTimeouts = vi.spyOn(globalThis, "clearTimeout");
|
|
798
|
+
const timersBeforeCancel = vi.getTimerCount();
|
|
799
|
+
const timeoutCallIndex = scheduledTimeouts.mock.calls.length;
|
|
800
|
+
act(() => input.dispatchEvent(new Event("cancel")));
|
|
801
|
+
expect(vi.getTimerCount()).toBe(timersBeforeCancel + 1);
|
|
802
|
+
const resumeTimer =
|
|
803
|
+
scheduledTimeouts.mock.results[timeoutCallIndex]?.value;
|
|
804
|
+
expect(resumeTimer).toBeDefined();
|
|
805
|
+
|
|
806
|
+
await mount({ disabled: true }, false);
|
|
807
|
+
expect(clearedTimeouts).toHaveBeenCalledWith(resumeTimer);
|
|
808
|
+
|
|
809
|
+
await act(async () => {
|
|
810
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
expect(document.querySelector("[role=menu] form")).toBeNull();
|
|
814
|
+
expect(labels()).not.toContain("Upload skill file");
|
|
815
|
+
} finally {
|
|
816
|
+
vi.useRealTimers();
|
|
817
|
+
}
|
|
818
|
+
});
|
|
725
819
|
});
|
|
@@ -92,6 +92,9 @@ export function useComposerDefaultActions({
|
|
|
92
92
|
const [view, setView] = useState<View>("menu");
|
|
93
93
|
const skillFileInputRef = useRef<HTMLInputElement>(null);
|
|
94
94
|
const skillControls = useRef<ComposerContextPageControls | null>(null);
|
|
95
|
+
const skillFilePickerOpen = useRef(false);
|
|
96
|
+
const skillPageDismissedWhilePickerOpen = useRef(false);
|
|
97
|
+
const skillResumeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
95
98
|
const skillEpoch = useRef(0);
|
|
96
99
|
const skillRequest = useRef<AbortController | null>(null);
|
|
97
100
|
const skillCloseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -104,13 +107,48 @@ export function useComposerDefaultActions({
|
|
|
104
107
|
} | null>(null);
|
|
105
108
|
const [skillUploadBusy, setSkillUploadBusy] = useState(false);
|
|
106
109
|
const formId = useId();
|
|
107
|
-
const
|
|
110
|
+
const invalidateSkillWork = useCallback(() => {
|
|
108
111
|
skillEpoch.current++;
|
|
109
112
|
skillRequest.current?.abort();
|
|
110
113
|
skillRequest.current = null;
|
|
111
114
|
if (skillCloseTimer.current !== null) clearTimeout(skillCloseTimer.current);
|
|
112
115
|
skillCloseTimer.current = null;
|
|
113
116
|
}, []);
|
|
117
|
+
const cancelSkillWork = useCallback(() => {
|
|
118
|
+
skillFilePickerOpen.current = false;
|
|
119
|
+
skillPageDismissedWhilePickerOpen.current = false;
|
|
120
|
+
if (skillResumeTimer.current !== null)
|
|
121
|
+
clearTimeout(skillResumeTimer.current);
|
|
122
|
+
skillResumeTimer.current = null;
|
|
123
|
+
invalidateSkillWork();
|
|
124
|
+
}, [invalidateSkillWork]);
|
|
125
|
+
const finishSkillFilePicker = useCallback(() => {
|
|
126
|
+
skillFilePickerOpen.current = false;
|
|
127
|
+
if (!skillPageDismissedWhilePickerOpen.current) return;
|
|
128
|
+
skillPageDismissedWhilePickerOpen.current = false;
|
|
129
|
+
if (skillResumeTimer.current !== null)
|
|
130
|
+
clearTimeout(skillResumeTimer.current);
|
|
131
|
+
skillResumeTimer.current = setTimeout(() => {
|
|
132
|
+
skillResumeTimer.current = null;
|
|
133
|
+
skillControls.current?.onResume();
|
|
134
|
+
}, 0);
|
|
135
|
+
}, []);
|
|
136
|
+
const dismissSkillPage = useCallback(() => {
|
|
137
|
+
if (skillFilePickerOpen.current) {
|
|
138
|
+
skillPageDismissedWhilePickerOpen.current = true;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
cancelSkillWork();
|
|
142
|
+
}, [cancelSkillWork]);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const input = skillFileInputRef.current;
|
|
145
|
+
input?.addEventListener("cancel", finishSkillFilePicker);
|
|
146
|
+
return () => {
|
|
147
|
+
input?.removeEventListener("cancel", finishSkillFilePicker);
|
|
148
|
+
if (skillResumeTimer.current !== null)
|
|
149
|
+
clearTimeout(skillResumeTimer.current);
|
|
150
|
+
};
|
|
151
|
+
}, [finishSkillFilePicker]);
|
|
114
152
|
useEffect(() => cancelSkillWork, [cancelSkillWork, adapters.resolvePath]);
|
|
115
153
|
useEffect(() => {
|
|
116
154
|
if (!disabled) return;
|
|
@@ -128,7 +166,7 @@ export function useComposerDefaultActions({
|
|
|
128
166
|
setSkillUploadBusy(false);
|
|
129
167
|
};
|
|
130
168
|
const handleSkillFileSelected = async (file: File) => {
|
|
131
|
-
|
|
169
|
+
invalidateSkillWork();
|
|
132
170
|
const epoch = skillEpoch.current;
|
|
133
171
|
setSkillUploadStatus(null);
|
|
134
172
|
try {
|
|
@@ -298,7 +336,7 @@ export function useComposerDefaultActions({
|
|
|
298
336
|
}),
|
|
299
337
|
],
|
|
300
338
|
onSelect: resetSkillUpload,
|
|
301
|
-
onDismiss:
|
|
339
|
+
onDismiss: dismissSkillPage,
|
|
302
340
|
render: (controls) => {
|
|
303
341
|
skillControls.current = controls;
|
|
304
342
|
return view === "menu" ? (
|
|
@@ -317,7 +355,12 @@ export function useComposerDefaultActions({
|
|
|
317
355
|
<DropdownMenuItem
|
|
318
356
|
onSelect={(event) => {
|
|
319
357
|
event.preventDefault();
|
|
320
|
-
skillFileInputRef.current
|
|
358
|
+
const input = skillFileInputRef.current;
|
|
359
|
+
if (!input) return;
|
|
360
|
+
skillPageDismissedWhilePickerOpen.current = false;
|
|
361
|
+
skillFilePickerOpen.current = true;
|
|
362
|
+
controls.onClose({ restoreFocus: false });
|
|
363
|
+
input.click();
|
|
321
364
|
}}
|
|
322
365
|
>
|
|
323
366
|
{t("agentChat.composer.skill.uploadFile", {
|
|
@@ -451,6 +494,7 @@ export function useComposerDefaultActions({
|
|
|
451
494
|
onChange={(event) => {
|
|
452
495
|
const file = event.target.files?.[0];
|
|
453
496
|
event.target.value = "";
|
|
497
|
+
finishSkillFilePicker();
|
|
454
498
|
if (file) void handleSkillFileSelected(file);
|
|
455
499
|
}}
|
|
456
500
|
/>
|