mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-29 20:52:51 +00:00
fix(app): let Escape close an open popover before it interrupts the agent
The agent.interrupt shortcut listens on the window in the capture phase, so it sees Escape before anything else. Registered overlays get first refusal through dispatchTopWebOverlayKeyDown, but plugins cannot register, and draw their floating panels with the Popover API instead. With such a panel open and the composer focused while an agent runs, Escape closed the panel and interrupted the agent in the same press. While a popover is open, Escape is left to it and no app shortcut runs. A second Escape, with nothing open, interrupts as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: RoundRedDot <roundreddot@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
RoundRedDot
parent
e5124b039a
commit
388099ebbf
@@ -40,7 +40,7 @@ import {
|
||||
navigateToLastWorkspace,
|
||||
useActiveWorkspaceSelection,
|
||||
} from "@/stores/navigation-active-workspace-store";
|
||||
import { dispatchTopWebOverlayKeyDown } from "@/lib/overlay-root";
|
||||
import { dispatchTopWebOverlayKeyDown, hasOpenDomPopover } from "@/lib/overlay-root";
|
||||
|
||||
export function useKeyboardShortcuts({
|
||||
enabled,
|
||||
@@ -321,6 +321,13 @@ export function useKeyboardShortcuts({
|
||||
return;
|
||||
}
|
||||
|
||||
// An open popover (a plugin's panel) is the top layer and closes on its
|
||||
// own Escape listener, which runs after this one. Escape is not an app
|
||||
// shortcut then: it must not also interrupt the running agent.
|
||||
if (event.key === "Escape" && hasOpenDomPopover()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// During IME composition, Enter confirms the candidate selection and must
|
||||
// not route through global shortcuts like message send.
|
||||
if (isImeComposingKeyboardEvent(event)) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { type RefCallback } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { useWebOverlayRegistration } from "./overlay-root";
|
||||
import { hasOpenDomPopover, useWebOverlayRegistration } from "./overlay-root";
|
||||
|
||||
function nextFrame(): Promise<void> {
|
||||
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
||||
@@ -68,3 +68,20 @@ describe("useWebOverlayRegistration in the browser", () => {
|
||||
expect(openerFocusCalls).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasOpenDomPopover", () => {
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
});
|
||||
|
||||
it("is true only while a popover is shown", () => {
|
||||
const panel = document.createElement("div");
|
||||
panel.setAttribute("popover", "manual");
|
||||
document.body.append(panel);
|
||||
expect(hasOpenDomPopover()).toBe(false);
|
||||
panel.showPopover();
|
||||
expect(hasOpenDomPopover()).toBe(true);
|
||||
panel.hidePopover();
|
||||
expect(hasOpenDomPopover()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,6 +112,21 @@ export function hasActiveWebOverlay(): boolean {
|
||||
return getTopWebOverlay() !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* True while an element shown with the Popover API is open. Such a layer
|
||||
* floats above the app like a registered overlay but lives outside the
|
||||
* registry: plugins draw their panels this way, since they cannot register.
|
||||
*/
|
||||
export function hasOpenDomPopover(): boolean {
|
||||
if (typeof document === "undefined") return false;
|
||||
try {
|
||||
return document.querySelector(":popover-open") !== null;
|
||||
} catch {
|
||||
// An engine without the Popover API rejects the selector, and has none open.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getFocusableElements(scope: HTMLElement): HTMLElement[] {
|
||||
const selector = [
|
||||
"a[href]",
|
||||
|
||||
Reference in New Issue
Block a user