diff --git a/apps/app/src/components/plugin/AppFileExternalNavigationHost.test.tsx b/apps/app/src/components/plugin/AppFileExternalNavigationHost.test.tsx
index fbd27cbe52..6f120d58fb 100644
--- a/apps/app/src/components/plugin/AppFileExternalNavigationHost.test.tsx
+++ b/apps/app/src/components/plugin/AppFileExternalNavigationHost.test.tsx
@@ -12,11 +12,12 @@ import { useAppNavigationHost } from "@/lib/app-navigation-host";
import { AppFileExternalNavigationHost } from "./AppFileExternalNavigationHost";
const openPreferred = vi.hoisted(() => vi.fn());
+const recordAccepted = vi.fn();
vi.mock("@/hooks/useResolvedLiveFileTarget", () => ({
- useResolvedLiveFileTarget: () => ({
+ useResolvedLiveFileTarget: (target: { path: string }) => ({
status: "available",
- absolutePath: "/workspace/src/example.ts",
+ absolutePath: `/workspace/${target.path}`,
hostId: "host_1",
openContext: { kind: "local" },
}),
@@ -32,21 +33,47 @@ vi.mock("@/hooks/useLocalOpenTargets", () => ({
function Probe() {
const navigation = useAppNavigationHost();
return (
-
+ <>
+
+
+ >
);
}
@@ -54,6 +81,7 @@ afterEach(() => {
cleanup();
openPreferred.mockReset();
openPreferred.mockResolvedValue(true);
+ recordAccepted.mockReset();
});
describe("AppFileExternalNavigationHost", () => {
@@ -74,4 +102,28 @@ describe("AppFileExternalNavigationHost", () => {
{ timeout: 5_000 },
);
});
+
+ it("dispatches queued intents once each in FIFO order", async () => {
+ render(
+
+
+ ,
+ );
+ fireEvent.click(screen.getByRole("button", { name: "Open two external" }));
+
+ expect(recordAccepted).toHaveBeenCalledWith(true, true);
+ await waitFor(() => expect(openPreferred).toHaveBeenCalledTimes(2), {
+ timeout: 5_000,
+ });
+ expect(openPreferred).toHaveBeenNthCalledWith(1, {
+ columnNumber: 2,
+ lineNumber: 10,
+ path: "/workspace/src/first.ts",
+ });
+ expect(openPreferred).toHaveBeenNthCalledWith(2, {
+ columnNumber: 4,
+ lineNumber: 20,
+ path: "/workspace/src/second.ts",
+ });
+ });
});
diff --git a/apps/app/src/components/plugin/AppFileExternalNavigationHost.tsx b/apps/app/src/components/plugin/AppFileExternalNavigationHost.tsx
index bbed6792b9..390ede239d 100644
--- a/apps/app/src/components/plugin/AppFileExternalNavigationHost.tsx
+++ b/apps/app/src/components/plugin/AppFileExternalNavigationHost.tsx
@@ -19,15 +19,21 @@ const LazyAppFileExternalNavigationDispatcher = lazy(() =>
),
);
+interface ExternalFileIntentRequest {
+ id: number;
+ intent: ExperimentalFileOpenOptions;
+}
+
/** App-wide preferred-external file dispatcher; discovery starts on activation. */
export function AppFileExternalNavigationHost({
children,
}: {
children: ReactNode;
}) {
- const [queue, setQueue] = useState([]);
+ const [queue, setQueue] = useState([]);
const queueRef = useRef(queue);
- const replaceQueue = useCallback((next: ExperimentalFileOpenOptions[]) => {
+ const nextRequestIdRef = useRef(0);
+ const replaceQueue = useCallback((next: ExternalFileIntentRequest[]) => {
queueRef.current = next;
setQueue(next);
}, []);
@@ -38,7 +44,9 @@ export function AppFileExternalNavigationHost({
}
// Public SDK callers are parsed by useBbNavigate before capabilities are
// invoked; this host only queues that already-normalized internal value.
- replaceQueue([...queueRef.current, intent]);
+ const request = { id: nextRequestIdRef.current, intent };
+ nextRequestIdRef.current += 1;
+ replaceQueue([...queueRef.current, request]);
return true;
},
[replaceQueue],
@@ -58,7 +66,8 @@ export function AppFileExternalNavigationHost({
{current === null ? null : (