diff --git a/apps/extension/src/content/__tests__/BorrowConfirmationOverlay.test.tsx b/apps/extension/src/content/__tests__/BorrowConfirmationOverlay.test.tsx index d273ef8..f1a8368 100644 --- a/apps/extension/src/content/__tests__/BorrowConfirmationOverlay.test.tsx +++ b/apps/extension/src/content/__tests__/BorrowConfirmationOverlay.test.tsx @@ -23,7 +23,7 @@ describe("BorrowConfirmationOverlay", () => { id: "req-1", isActiveTab: true, tabTitle: "Example", - timeoutMs: 5000, + timeoutMs: 60_000, onAllow, onDeny, }, @@ -32,7 +32,7 @@ describe("BorrowConfirmationOverlay", () => { ); expect(screen.getByText("允许借用标签页?")).toBeTruthy(); - for (let i = 0; i < 5; i++) { + for (let i = 0; i < 60; i++) { await act(async () => { await vi.advanceTimersByTimeAsync(1000); }); @@ -66,7 +66,7 @@ describe("BorrowConfirmationOverlay", () => { id: "req-fallback", isActiveTab: true, tabTitle: "Example", - timeoutMs: 5000, + timeoutMs: 60_000, onAllow, onDeny, }, @@ -74,7 +74,7 @@ describe("BorrowConfirmationOverlay", () => { />, ); - for (let i = 0; i < 5; i++) { + for (let i = 0; i < 60; i++) { await act(async () => { await vi.advanceTimersByTimeAsync(1000); }); diff --git a/apps/extension/src/tools/__tests__/borrow-confirmation.test.ts b/apps/extension/src/tools/__tests__/borrow-confirmation.test.ts index 0cb106b..784fa80 100644 --- a/apps/extension/src/tools/__tests__/borrow-confirmation.test.ts +++ b/apps/extension/src/tools/__tests__/borrow-confirmation.test.ts @@ -127,6 +127,38 @@ describe("requestBorrowConfirmation", () => { expect(message.requestId.length).toBeGreaterThan(0); }); + it("proactively focuses the user window before sending the overlay message", async () => { + tabs.get.mockResolvedValueOnce({ id: 42, title: "Tab" }); + windows.getLastFocused.mockResolvedValueOnce( + userWindowWithActiveTab({ windowId: 77, tabId: 7, url: "https://app.example/" }), + ); + tabs.sendMessage.mockResolvedValueOnce({ type: "borrow-response", allowed: true }); + + const pending = requestBorrowConfirmation(42, { + deps: { tabs, windows, notifications }, + }); + await vi.runAllTimersAsync(); + await pending; + + expect(windows.update).toHaveBeenCalledWith(77, { focused: true }); + }); + + it("still proceeds when proactive window focus fails", async () => { + tabs.get.mockResolvedValueOnce({ id: 42, title: "Tab" }); + windows.getLastFocused.mockResolvedValueOnce( + userWindowWithActiveTab({ windowId: 77, tabId: 7, url: "https://app.example/" }), + ); + windows.update.mockRejectedValueOnce(new Error("window gone")); + tabs.sendMessage.mockResolvedValueOnce({ type: "borrow-response", allowed: true }); + + const pending = requestBorrowConfirmation(42, { + deps: { tabs, windows, notifications }, + }); + await vi.runAllTimersAsync(); + + expect(await pending).toBe(true); + }); + it("sends confirmation to the borrow target when it is the active tab", async () => { tabs.get.mockResolvedValueOnce({ id: 42, title: "Active Tab" }); windows.getLastFocused.mockResolvedValueOnce( diff --git a/apps/extension/src/tools/borrow-confirmation.ts b/apps/extension/src/tools/borrow-confirmation.ts index 68a889f..b575084 100644 --- a/apps/extension/src/tools/borrow-confirmation.ts +++ b/apps/extension/src/tools/borrow-confirmation.ts @@ -24,7 +24,7 @@ export interface BorrowResponseMessage { allowed: boolean; } -export const CONFIRMATION_TIMEOUT_MS = 5000; +export const CONFIRMATION_TIMEOUT_MS = 60_000; const EXIT_ANIMATION_MS = 150; /** Matches BorrowConfirmationOverlay progress ring/bar transition (duration-1000). */ export const PROGRESS_TRANSITION_MS = 1000; @@ -480,6 +480,13 @@ export async function requestBorrowConfirmation( settle(false); }, BACKGROUND_TIMEOUT_MS); + // Bring the user window to the front so the overlay is visible even + // when the Agent Window has stolen focus. This is fire-and-forget: if + // it fails the overlay / OS notification still give the user a path. + void windowsApi.update(notificationAnchor.windowId, { focused: true }).catch((err) => { + console.debug("[bsk borrow] proactive focus of user window failed", err); + }); + // Surface the OS notification *before* messaging any candidate so the // user has a parallel signal even if every content script is missing. // The Allow / Deny buttons let the user authorize explicitly when the