mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
feat(vom): update vom algorithm, fix overlay problem with background state machine
This commit is contained in:
@@ -1,153 +0,0 @@
|
||||
import { useTranslation } from "@browser-skill/i18n/react";
|
||||
import { RiStopCircleLine } from "@remixicon/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import logoUrl from "../../assets/logo.png";
|
||||
|
||||
export interface ControlOverlayProps {
|
||||
visible: boolean;
|
||||
interrupting: boolean;
|
||||
automationBypass: boolean;
|
||||
onInterrupt: () => void;
|
||||
}
|
||||
|
||||
export function ControlOverlay({
|
||||
visible,
|
||||
interrupting,
|
||||
automationBypass,
|
||||
onInterrupt,
|
||||
}: ControlOverlayProps) {
|
||||
const { t } = useTranslation("extension");
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const raf = requestAnimationFrame(() => setShow(true));
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}
|
||||
setShow(false);
|
||||
}, [visible]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
const pointerEvents = automationBypass ? "none" : "auto";
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
@keyframes bsk-breathe {
|
||||
0%, 100% {
|
||||
box-shadow: inset 0 0 20px 4px rgba(249,115,22,0.25);
|
||||
}
|
||||
50% {
|
||||
box-shadow: inset 0 0 40px 8px rgba(249,115,22,0.5);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div
|
||||
data-slot="control-overlay"
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
zIndex: 2147483646,
|
||||
pointerEvents: "none",
|
||||
animation: "bsk-breathe 3s ease-in-out infinite",
|
||||
opacity: show ? 1 : 0,
|
||||
transition: "opacity 300ms ease-out",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
data-slot="control-overlay-blocker"
|
||||
onPointerDown={(event) => {
|
||||
if (automationBypass) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onClick={(event) => {
|
||||
if (automationBypass) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
zIndex: 2147483646,
|
||||
pointerEvents,
|
||||
background: "transparent",
|
||||
opacity: show ? 1 : 0,
|
||||
transition: "opacity 300ms ease-out",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
data-slot="control-overlay-pill"
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: 32,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 2147483647,
|
||||
// Always receive clicks so Interrupt works even while the page
|
||||
// blocker is in automation-bypass mode (CDP pass-through).
|
||||
pointerEvents: "auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: 9999,
|
||||
padding: "10px 10px 10px 20px",
|
||||
boxShadow: "0 8px 32px rgba(124,45,18,0.16), 0 2px 8px rgba(0,0,0,0.1)",
|
||||
opacity: show ? 1 : 0,
|
||||
transition: "opacity 300ms ease-out",
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt="browser-skill"
|
||||
style={{ width: 24, height: 24, borderRadius: 4, flexShrink: 0 }}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: 500,
|
||||
color: "#333",
|
||||
whiteSpace: "nowrap",
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
{t("controlOverlay.status")}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
data-slot="control-overlay-stop-all"
|
||||
disabled={interrupting}
|
||||
onClick={onInterrupt}
|
||||
style={{
|
||||
pointerEvents: "auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
border: "none",
|
||||
borderRadius: 9999,
|
||||
padding: "8px 20px 8px 16px",
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
color: "#fff",
|
||||
backgroundColor: interrupting ? "#9ca3af" : "#f97316",
|
||||
cursor: interrupting ? "default" : "pointer",
|
||||
opacity: interrupting ? 0.7 : 1,
|
||||
transition: "background-color 150ms ease-out, opacity 150ms ease-out",
|
||||
whiteSpace: "nowrap",
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
<RiStopCircleLine size={18} color="#fff" />
|
||||
{interrupting ? t("controlOverlay.interrupting") : t("controlOverlay.interrupt")}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { ControlOverlay } from "../ControlOverlay";
|
||||
|
||||
describe("ControlOverlay", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("keeps page blocker none under automationBypass but Interrupt stays clickable", () => {
|
||||
const { container } = render(
|
||||
<ControlOverlay
|
||||
visible={true}
|
||||
interrupting={false}
|
||||
automationBypass={true}
|
||||
onInterrupt={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const blocker = container.querySelector("[data-slot='control-overlay-blocker']");
|
||||
expect(blocker).toBeTruthy();
|
||||
expect((blocker as HTMLElement).style.pointerEvents).toBe("none");
|
||||
|
||||
const pill = container.querySelector("[data-slot='control-overlay-pill']");
|
||||
expect(pill).toBeTruthy();
|
||||
expect((pill as HTMLElement).style.pointerEvents).toBe("auto");
|
||||
|
||||
const stopBtn = container.querySelector("[data-slot='control-overlay-stop-all']");
|
||||
expect(stopBtn).toBeTruthy();
|
||||
expect((stopBtn as HTMLElement).style.pointerEvents).toBe("auto");
|
||||
});
|
||||
|
||||
it("uses pointer-events auto on blocker when automationBypass is false", () => {
|
||||
const { container } = render(
|
||||
<ControlOverlay
|
||||
visible={true}
|
||||
interrupting={false}
|
||||
automationBypass={false}
|
||||
onInterrupt={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const blocker = container.querySelector("[data-slot='control-overlay-blocker']");
|
||||
expect(blocker).toBeTruthy();
|
||||
expect((blocker as HTMLElement).style.pointerEvents).toBe("auto");
|
||||
});
|
||||
});
|
||||
@@ -116,4 +116,18 @@ describe("OverlayController", () => {
|
||||
expect(controller.snapshot().suppressControlAfterRecord).toBe(false);
|
||||
expect(controller.snapshot().controlVisible).toBe(false);
|
||||
});
|
||||
|
||||
it("hides control while paused and shows it again only after authoritative control mode", () => {
|
||||
const controller = new OverlayController();
|
||||
|
||||
controller.applyAgentControlMode("sess-1", "paused");
|
||||
expect(controller.snapshot().activeSessionId).toBe("sess-1");
|
||||
expect(controller.snapshot().controlVisible).toBe(false);
|
||||
expect(shouldShowAgentControlOverlay(controller.snapshot())).toBe(false);
|
||||
|
||||
controller.applyAgentControlMode("sess-1", "control");
|
||||
expect(controller.snapshot().activeSessionId).toBe("sess-1");
|
||||
expect(controller.snapshot().controlVisible).toBe(true);
|
||||
expect(shouldShowAgentControlOverlay(controller.snapshot())).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { OverlayMode } from "@/lib/overlay-bridge";
|
||||
import type { BorrowRequestData } from "./BorrowConfirmationOverlay";
|
||||
import type { HelpRequestData } from "./HelpRequestOverlay";
|
||||
import type { RecordRequestData } from "./RecordOverlay";
|
||||
@@ -9,6 +10,7 @@ export interface OverlayState {
|
||||
controlVisible: boolean;
|
||||
interrupting: boolean;
|
||||
activeSessionId: string | null;
|
||||
controlMode: OverlayMode;
|
||||
automationBypassCount: number;
|
||||
/**
|
||||
* After record Finish/Stop clears `activeRecord`, the session is still
|
||||
@@ -31,6 +33,7 @@ export class OverlayController {
|
||||
activeRecord: null,
|
||||
interrupting: false,
|
||||
activeSessionId: null,
|
||||
controlMode: "hidden",
|
||||
automationBypassCount: 0,
|
||||
suppressControlAfterRecord: false,
|
||||
};
|
||||
@@ -44,7 +47,7 @@ export class OverlayController {
|
||||
}
|
||||
|
||||
isControlVisible(): boolean {
|
||||
return this.state.activeSessionId !== null;
|
||||
return this.state.activeSessionId !== null && this.state.controlMode === "control";
|
||||
}
|
||||
|
||||
addBorrowRequest(request: BorrowRequestData): void {
|
||||
@@ -61,6 +64,25 @@ export class OverlayController {
|
||||
activateAgentSession(sessionId: string): void {
|
||||
this.state.activeSessionId = sessionId;
|
||||
this.state.interrupting = false;
|
||||
this.state.controlMode = "control";
|
||||
}
|
||||
|
||||
applyAgentControlMode(sessionId: string | null, mode: OverlayMode): void {
|
||||
if (mode === "control" && sessionId) {
|
||||
this.state.activeSessionId = sessionId;
|
||||
this.state.interrupting = false;
|
||||
this.state.controlMode = "control";
|
||||
return;
|
||||
}
|
||||
if ((mode === "interrupting" || mode === "paused") && sessionId) {
|
||||
this.state.activeSessionId = sessionId;
|
||||
this.state.interrupting = mode === "interrupting";
|
||||
this.state.controlMode = mode;
|
||||
return;
|
||||
}
|
||||
this.state.activeSessionId = null;
|
||||
this.state.interrupting = false;
|
||||
this.state.controlMode = "hidden";
|
||||
}
|
||||
|
||||
setInterrupting(interrupting: boolean): void {
|
||||
@@ -118,6 +140,7 @@ export class OverlayController {
|
||||
activeRecord: null,
|
||||
interrupting: false,
|
||||
activeSessionId: null,
|
||||
controlMode: "hidden",
|
||||
automationBypassCount: 0,
|
||||
suppressControlAfterRecord: false,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user