Fix Biome and Stylelint formatting so pnpm lint passes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
BB-fat
2026-07-10 00:21:19 +08:00
co-authored by Cursor
parent d395faa0ab
commit 3c936982a3
29 changed files with 167 additions and 143 deletions
@@ -1,6 +1,6 @@
import { useTranslation } from "@browser-skill/i18n/react"; import { useTranslation } from "@browser-skill/i18n/react";
import { RiCloseLine } from "@remixicon/react"; import { RiCloseLine } from "@remixicon/react";
import { useEffect, useRef, useState, type TransitionEvent } from "react"; import { type TransitionEvent, useEffect, useRef, useState } from "react";
import logoUrl from "../../assets/logo.png"; import logoUrl from "../../assets/logo.png";
export interface BorrowRequestData { export interface BorrowRequestData {
@@ -1,12 +1,12 @@
import { useTranslation } from "@browser-skill/i18n/react"; import { useTranslation } from "@browser-skill/i18n/react";
import { RiArrowDownSLine } from "@remixicon/react"; import { RiArrowDownSLine } from "@remixicon/react";
import { import {
type PointerEvent as ReactPointerEvent,
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
useRef, useRef,
useState, useState,
type PointerEvent as ReactPointerEvent,
} from "react"; } from "react";
import logoUrl from "../../assets/logo.png"; import logoUrl from "../../assets/logo.png";
@@ -239,25 +239,29 @@ export function HelpRequestOverlay({ request }: Props) {
}; };
}, [request]); }, [request]);
const onHeaderPointerDown = useCallback((e: ReactPointerEvent<HTMLDivElement>) => { const onHeaderPointerDown = useCallback(
if (e.button !== 0) return; (e: ReactPointerEvent<HTMLDivElement>) => {
if (e.target instanceof Element && e.target.closest("button, input, textarea, a, select")) return; if (e.button !== 0) return;
const banner = bannerRef.current; if (e.target instanceof Element && e.target.closest("button, input, textarea, a, select"))
if (!banner) return; return;
const rect = banner.getBoundingClientRect(); const banner = bannerRef.current;
dragStartRef.current = { if (!banner) return;
pointerX: e.clientX, const rect = banner.getBoundingClientRect();
pointerY: e.clientY, dragStartRef.current = {
originLeft: rect.left, pointerX: e.clientX,
originTop: rect.top, pointerY: e.clientY,
panelW: banner.offsetWidth || PANEL_WIDTH, originLeft: rect.left,
panelH: banner.offsetHeight || (collapsed ? FALLBACK_PANEL_H_COLLAPSED : FALLBACK_PANEL_H), originTop: rect.top,
}; panelW: banner.offsetWidth || PANEL_WIDTH,
userMovedRef.current = true; panelH: banner.offsetHeight || (collapsed ? FALLBACK_PANEL_H_COLLAPSED : FALLBACK_PANEL_H),
setDragPos({ top: rect.top, left: rect.left }); };
setDragging(true); userMovedRef.current = true;
e.preventDefault(); setDragPos({ top: rect.top, left: rect.left });
}, [collapsed]); setDragging(true);
e.preventDefault();
},
[collapsed],
);
useEffect(() => { useEffect(() => {
if (!dragging) return; if (!dragging) return;
@@ -636,7 +640,11 @@ export function HelpRequestOverlay({ request }: Props) {
</div> </div>
<div className="bsk-help-header"> <div className="bsk-help-header">
<img src={logoUrl} alt="browser-skill" style={{ width: 22, height: 22, borderRadius: 4 }} /> <img
src={logoUrl}
alt="browser-skill"
style={{ width: 22, height: 22, borderRadius: 4 }}
/>
<span className="bsk-help-title">{request.title ?? t("helpRequest.title")}</span> <span className="bsk-help-title">{request.title ?? t("helpRequest.title")}</span>
<div className="bsk-help-header-actions" aria-hidden={!collapsed}> <div className="bsk-help-header-actions" aria-hidden={!collapsed}>
<button <button
@@ -3,7 +3,7 @@ import { I18nextProvider } from "@browser-skill/i18n/react";
import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { createElement } from "react"; import { createElement } from "react";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { HelpRequestOverlay, type HelpRequestData } from "../HelpRequestOverlay"; import { type HelpRequestData, HelpRequestOverlay } from "../HelpRequestOverlay";
function renderOverlay(req: HelpRequestData) { function renderOverlay(req: HelpRequestData) {
return render( return render(
@@ -79,7 +79,7 @@ describe("HelpRequestOverlay", () => {
renderOverlay(baseRequest({ selectors: ["#login", "#missing"] })); renderOverlay(baseRequest({ selectors: ["#login", "#missing"] }));
// One highlight box rendered for the matched selector. // One highlight box rendered for the matched selector.
expect(document.querySelectorAll("[data-slot='help-highlight']").length).toBe(1); expect(document.querySelectorAll("[data-slot='help-highlight']").length).toBe(1);
expect((el.scrollIntoView as ReturnType<typeof vi.fn>)).toHaveBeenCalled(); expect(el.scrollIntoView as ReturnType<typeof vi.fn>).toHaveBeenCalled();
el.remove(); el.remove();
}); });
@@ -115,8 +115,8 @@ describe("HelpRequestOverlay", () => {
), ),
); );
expect((elA.scrollIntoView as ReturnType<typeof vi.fn>)).toHaveBeenCalled(); expect(elA.scrollIntoView as ReturnType<typeof vi.fn>).toHaveBeenCalled();
expect((elB.scrollIntoView as ReturnType<typeof vi.fn>)).toHaveBeenCalled(); expect(elB.scrollIntoView as ReturnType<typeof vi.fn>).toHaveBeenCalled();
elA.remove(); elA.remove();
elB.remove(); elB.remove();
}); });
@@ -286,9 +286,7 @@ describe("HelpRequestOverlay", () => {
it("does not start drag when pointer down on a button in the header", () => { it("does not start drag when pointer down on a button in the header", () => {
const { container } = renderOverlay(baseRequest()); const { container } = renderOverlay(baseRequest());
const banner = container.querySelector("[data-slot='help-request-banner']") as HTMLElement; const banner = container.querySelector("[data-slot='help-request-banner']") as HTMLElement;
const collapseBtn = screen.getByLabelText( const collapseBtn = screen.getByLabelText(i18n.t("helpRequest.collapse", { ns: "extension" }));
i18n.t("helpRequest.collapse", { ns: "extension" }),
);
fireEvent.pointerDown(collapseBtn, { button: 0, clientX: 10, clientY: 10, pointerId: 1 }); fireEvent.pointerDown(collapseBtn, { button: 0, clientX: 10, clientY: 10, pointerId: 1 });
@@ -1,4 +1,4 @@
import { describe, it, expect, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
// `background.ts` is a WXT entrypoint that calls the global `defineBackground` // `background.ts` is a WXT entrypoint that calls the global `defineBackground`
// at module load. WXT injects that helper at build time via auto-imports; // at module load. WXT injects that helper at build time via auto-imports;
+3 -3
View File
@@ -167,9 +167,9 @@ export default defineBackground(() => {
// silently doing nothing (review M4/M5 C2). // silently doing nothing (review M4/M5 C2).
console.warn("[browser-skill] set_port is not wired yet; ignoring", msg.value); console.warn("[browser-skill] set_port is not wired yet; ignoring", msg.value);
} else if (msg.kind === "set_connection_enabled") { } else if (msg.kind === "set_connection_enabled") {
void controller.setConnectionEnabled(msg.value).then(() => void controller
persistConnectionEnabled(msg.value), .setConnectionEnabled(msg.value)
); .then(() => persistConnectionEnabled(msg.value));
} }
} }
}); });
+6 -9
View File
@@ -1,3 +1,7 @@
import { i18n } from "@browser-skill/i18n";
import { I18nextProvider } from "@browser-skill/i18n/react";
import React from "react";
import ReactDOM from "react-dom/client";
import { BorrowConfirmationOverlay } from "@/content/BorrowConfirmationOverlay"; import { BorrowConfirmationOverlay } from "@/content/BorrowConfirmationOverlay";
import { ControlOverlay } from "@/content/ControlOverlay"; import { ControlOverlay } from "@/content/ControlOverlay";
import { HelpRequestOverlay } from "@/content/HelpRequestOverlay"; import { HelpRequestOverlay } from "@/content/HelpRequestOverlay";
@@ -12,12 +16,12 @@ import {
isHelpRequestMessage, isHelpRequestMessage,
} from "@/lib/help-bridge"; } from "@/lib/help-bridge";
import { import {
isOverlayAgentOverlayResetMessage,
OVERLAY_AUTOMATION_BYPASS, OVERLAY_AUTOMATION_BYPASS,
OVERLAY_MSG_WHO_AM_I, OVERLAY_MSG_WHO_AM_I,
type OverlayAgentOverlayResetMessage, type OverlayAgentOverlayResetMessage,
type OverlayAutomationBypassMessage, type OverlayAutomationBypassMessage,
type OverlayWhoAmIResponse, type OverlayWhoAmIResponse,
isOverlayAgentOverlayResetMessage,
} from "@/lib/overlay-bridge"; } from "@/lib/overlay-bridge";
import { sendInterrupt } from "@/lib/overlay-interrupt-client"; import { sendInterrupt } from "@/lib/overlay-interrupt-client";
import { SESSIONS_LIVE_FLAG_KEY } from "@/lib/sessions-live-flag"; import { SESSIONS_LIVE_FLAG_KEY } from "@/lib/sessions-live-flag";
@@ -26,10 +30,6 @@ import type {
BorrowRequestMessage, BorrowRequestMessage,
BorrowResponseMessage, BorrowResponseMessage,
} from "@/tools/borrow-confirmation"; } from "@/tools/borrow-confirmation";
import { i18n } from "@browser-skill/i18n";
import { I18nextProvider } from "@browser-skill/i18n/react";
import React from "react";
import ReactDOM from "react-dom/client";
// Run at document_end so the overlay does not block first paint. Only attach // Run at document_end so the overlay does not block first paint. Only attach
// in the top-level frame so iframes do not double-render overlays. // in the top-level frame so iframes do not double-render overlays.
@@ -117,10 +117,7 @@ export default defineContentScript({
} }
overlays.setInterrupting(true); overlays.setInterrupting(true);
renderOverlay(); renderOverlay();
void sendInterrupt( void sendInterrupt((msg) => chrome.runtime.sendMessage(msg), sessionId).then((reply) => {
(msg) => chrome.runtime.sendMessage(msg),
sessionId,
).then((reply) => {
// Always retract the mask after the round trip resolves // Always retract the mask after the round trip resolves
// (success, failure, or timeout). Cancellation is fire-and- // (success, failure, or timeout). Cancellation is fire-and-
// forget on the daemon side; the user must not be stuck // forget on the daemon side; the user must not be stuck
@@ -132,8 +132,8 @@ describe("App", () => {
render(<App />); render(<App />);
expect(screen.getByText("连接已关闭")).toBeTruthy(); expect(screen.getByText("连接已关闭")).toBeTruthy();
expect(screen.getByRole("switch", { name: "BrowserSkill 连接" }).getAttribute("aria-checked")).toBe( expect(
"false", screen.getByRole("switch", { name: "BrowserSkill 连接" }).getAttribute("aria-checked"),
); ).toBe("false");
}); });
}); });
+3 -11
View File
@@ -1,5 +1,5 @@
import { useTranslation } from "@browser-skill/i18n/react"; import { useTranslation } from "@browser-skill/i18n/react";
import { Badge, Button, Input, Label, cn } from "@browser-skill/ui"; import { Badge, Button, cn, Input, Label } from "@browser-skill/ui";
import { RiCheckLine, RiFileCopyLine } from "@remixicon/react"; import { RiCheckLine, RiFileCopyLine } from "@remixicon/react";
import { type ChangeEvent, useEffect, useState } from "react"; import { type ChangeEvent, useEffect, useState } from "react";
import { PROTOCOL_VERSION } from "@/transport/handshake"; import { PROTOCOL_VERSION } from "@/transport/handshake";
@@ -75,12 +75,7 @@ export function App() {
data-version-skew={isSkewed ? "true" : undefined} data-version-skew={isSkewed ? "true" : undefined}
> >
<header className="flex items-center gap-2" data-slot="popup-brand-header"> <header className="flex items-center gap-2" data-slot="popup-brand-header">
<img <img src={getLogoSrc()} alt="" className="size-7 rounded-lg" data-slot="popup-brand-logo" />
src={getLogoSrc()}
alt=""
className="size-7 rounded-lg"
data-slot="popup-brand-logo"
/>
<h1 className="text-sm font-semibold tracking-tight">{t("popup.brandName")}</h1> <h1 className="text-sm font-semibold tracking-tight">{t("popup.brandName")}</h1>
</header> </header>
@@ -91,10 +86,7 @@ export function App() {
<div className="flex items-center justify-between gap-2" data-slot="popup-status"> <div className="flex items-center justify-between gap-2" data-slot="popup-status">
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<ConnectionStatusIndicator state={statusState} /> <ConnectionStatusIndicator state={statusState} />
<span <span className="truncate text-sm font-medium" data-slot="popup-state-label">
className="truncate text-sm font-medium"
data-slot="popup-state-label"
>
{t(STATE_LABEL_KEYS[statusState])} {t(STATE_LABEL_KEYS[statusState])}
</span> </span>
</div> </div>
@@ -1,10 +1,8 @@
import { ConnectionController, __testing__ } from "../connection-controller"; import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Transport } from "../../transport/transport";
import type { ConnectionState } from "../../transport/types";
import type { ConnectionStateHandler } from "../../transport/transport";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { MIN_COMPATIBLE_PROTOCOL } from "../../transport/handshake"; import { MIN_COMPATIBLE_PROTOCOL } from "../../transport/handshake";
import type { HandshakeResult } from "../../transport/types"; import type { ConnectionStateHandler, Transport } from "../../transport/transport";
import type { ConnectionState, HandshakeResult } from "../../transport/types";
import { __testing__, ConnectionController } from "../connection-controller";
vi.mock("../instance-id", () => ({ vi.mock("../instance-id", () => ({
getOrCreateInstanceId: vi.fn(async () => "a1b2c3d4"), getOrCreateInstanceId: vi.fn(async () => "a1b2c3d4"),
@@ -51,12 +51,8 @@ describe("help-bridge", () => {
}); });
it("type-guards a help-cancel message", () => { it("type-guards a help-cancel message", () => {
expect( expect(isHelpCancelMessage({ type: HELP_CANCEL, requestId: "r1" })).toBe(true);
isHelpCancelMessage({ type: HELP_CANCEL, requestId: "r1" }), expect(isHelpCancelMessage({ type: HELP_REQUEST, requestId: "r1" })).toBe(false);
).toBe(true);
expect(isHelpCancelMessage({ type: HELP_REQUEST, requestId: "r1" })).toBe(
false,
);
expect(isHelpCancelMessage(null)).toBe(false); expect(isHelpCancelMessage(null)).toBe(false);
}); });
}); });
@@ -1,10 +1,10 @@
import { describe, it, expect } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
isOverlayAgentOverlayResetMessage,
OVERLAY_AGENT_OVERLAY_RESET, OVERLAY_AGENT_OVERLAY_RESET,
OVERLAY_MSG_INTERRUPT, OVERLAY_MSG_INTERRUPT,
type OverlayInterruptRequest, type OverlayInterruptRequest,
type OverlayInterruptResponse, type OverlayInterruptResponse,
isOverlayAgentOverlayResetMessage,
} from "@/lib/overlay-bridge"; } from "@/lib/overlay-bridge";
describe("OVERLAY_MSG_INTERRUPT", () => { describe("OVERLAY_MSG_INTERRUPT", () => {
@@ -1,4 +1,4 @@
import { describe, it, expect, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { sendInterrupt } from "@/lib/overlay-interrupt-client"; import { sendInterrupt } from "@/lib/overlay-interrupt-client";
describe("sendInterrupt", () => { describe("sendInterrupt", () => {
@@ -284,6 +284,9 @@ describe("handleEvaluate", () => {
{ session_id: "aa11", expression: "1+1", tab_id: 11 }, { session_id: "aa11", expression: "1+1", tab_id: 11 },
{ cdp: fake.cdp, tabsApi: fake.tabsApi }, { cdp: fake.cdp, tabsApi: fake.tabsApi },
); );
expect(res).toMatchObject({ code: "permission_denied", data: { reason: "agent_window_scope" } }); expect(res).toMatchObject({
code: "permission_denied",
data: { reason: "agent_window_scope" },
});
}); });
}); });
@@ -36,13 +36,21 @@ function baseDeps(over: Partial<RequestHelpDeps> = {}): RequestHelpDeps {
describe("handleRequestHelp", () => { describe("handleRequestHelp", () => {
it("rejects unknown session", async () => { it("rejects unknown session", async () => {
const res = await handleRequestHelp(fakeManager("abcd", 99, 5), baseParams({ session_id: "zzzz" }), baseDeps()); const res = await handleRequestHelp(
fakeManager("abcd", 99, 5),
baseParams({ session_id: "zzzz" }),
baseDeps(),
);
expect("code" in res && res.code).toBe("not_found"); expect("code" in res && res.code).toBe("not_found");
}); });
it("brings the tab to the foreground and returns the user outcome", async () => { it("brings the tab to the foreground and returns the user outcome", async () => {
const deps = baseDeps(); const deps = baseDeps();
const res = await handleRequestHelp(fakeManager("abcd", 99, 5), baseParams({ tab_id: 5 }), deps); const res = await handleRequestHelp(
fakeManager("abcd", 99, 5),
baseParams({ tab_id: 5 }),
deps,
);
expect(deps.windows.update).toHaveBeenCalledWith(99, { focused: true }); expect(deps.windows.update).toHaveBeenCalledWith(99, { focused: true });
expect(deps.activateTab).toHaveBeenCalledWith(5); expect(deps.activateTab).toHaveBeenCalledWith(5);
expect(res).toMatchObject({ outcome: "continued", note: "ok", tab_id: 5 }); expect(res).toMatchObject({ outcome: "continued", note: "ok", tab_id: 5 });
@@ -79,7 +87,11 @@ describe("handleRequestHelp", () => {
return unwatch; return unwatch;
}), }),
}); });
const res = await handleRequestHelp(fakeManager("abcd", 99, 5), baseParams({ tab_id: 5 }), deps); const res = await handleRequestHelp(
fakeManager("abcd", 99, 5),
baseParams({ tab_id: 5 }),
deps,
);
expect(res).toMatchObject({ outcome: "navigated", tab_id: 5 }); expect(res).toMatchObject({ outcome: "navigated", tab_id: 5 });
expect(unwatch).toHaveBeenCalled(); expect(unwatch).toHaveBeenCalled();
}); });
@@ -107,12 +119,19 @@ describe("handleRequestHelp", () => {
const mgr = { const mgr = {
get: (id: string) => get: (id: string) =>
id === "abcd" id === "abcd"
? { sessionId: "abcd", agentWindowId: 99, refStore: { resolve: () => 42 }, borrowedTabs: new Map() } ? {
sessionId: "abcd",
agentWindowId: 99,
refStore: { resolve: () => 42 },
borrowedTabs: new Map(),
}
: null, : null,
findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null), findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null),
} as unknown as SessionManager; } as unknown as SessionManager;
const deps = baseDeps({ const deps = baseDeps({
cdp: { send: vi.fn(async () => ({ object: { objectId: "obj-1" } })) } as unknown as RequestHelpDeps["cdp"], cdp: {
send: vi.fn(async () => ({ object: { objectId: "obj-1" } })),
} as unknown as RequestHelpDeps["cdp"],
}); });
const res = await handleRequestHelp( const res = await handleRequestHelp(
mgr, mgr,
@@ -177,7 +196,11 @@ describe("handleRequestHelp", () => {
throw new Error("no receiver"); throw new Error("no receiver");
}), }),
}); });
const res = await handleRequestHelp(fakeManager("abcd", 99, 5), baseParams({ tab_id: 5 }), deps); const res = await handleRequestHelp(
fakeManager("abcd", 99, 5),
baseParams({ tab_id: 5 }),
deps,
);
expect("code" in res && res.code).toBe("protocol_error"); expect("code" in res && res.code).toBe("protocol_error");
}); });
@@ -254,7 +277,11 @@ describe("handleRequestHelp", () => {
it("returns cancelled when the signal aborts", async () => { it("returns cancelled when the signal aborts", async () => {
const ac = new AbortController(); const ac = new AbortController();
const deps = baseDeps({ signal: ac.signal, sendToTab: vi.fn(() => new Promise(() => {})) }); const deps = baseDeps({ signal: ac.signal, sendToTab: vi.fn(() => new Promise(() => {})) });
const p = handleRequestHelp(fakeManager("abcd", 99, 5), baseParams({ tab_id: 5, timeout_ms: 60_000 }), deps); const p = handleRequestHelp(
fakeManager("abcd", 99, 5),
baseParams({ tab_id: 5, timeout_ms: 60_000 }),
deps,
);
ac.abort(); ac.abort();
const res = await p; const res = await p;
expect("code" in res ? res.code : (res as { outcome: string }).outcome).toMatch(/cancelled/); expect("code" in res ? res.code : (res as { outcome: string }).outcome).toMatch(/cancelled/);
@@ -982,4 +982,3 @@ describe("handleSelect", () => {
expect(fake.sent.some((c) => c.method === "Runtime.callFunctionOn")).toBe(false); expect(fake.sent.some((c) => c.method === "Runtime.callFunctionOn")).toBe(false);
}); });
}); });
@@ -2,12 +2,12 @@ import { describe, expect, it, vi } from "vitest";
import { SessionManager } from "@/session-manager/manager"; import { SessionManager } from "@/session-manager/manager";
import type { CdpRunner } from "@/tools/shared"; import type { CdpRunner } from "@/tools/shared";
import { import {
eventLoaderIsRelevant,
handleNavigate, handleNavigate,
handleNavigateBack, handleNavigateBack,
handleNavigateForward, handleNavigateForward,
handleReload, handleReload,
lifecycleAlreadyReached, lifecycleAlreadyReached,
eventLoaderIsRelevant,
lifecycleMeetsOrExceeds, lifecycleMeetsOrExceeds,
shouldTrustReadyStateProbe, shouldTrustReadyStateProbe,
} from "../navigation"; } from "../navigation";
@@ -60,10 +60,7 @@ function makeFakeCdp(opts?: {
}; };
const events: EventListener[] = []; const events: EventListener[] = [];
const sent: Array<{ tabId: number; method: string; params?: object }> = []; const sent: Array<{ tabId: number; method: string; params?: object }> = [];
const methodHandlers: Record< const methodHandlers: Record<string, (tabId: number, params?: object) => object> = {
string,
(tabId: number, params?: object) => object
> = {
"Page.enable": () => ({}), "Page.enable": () => ({}),
"Page.setLifecycleEventsEnabled": () => ({}), "Page.setLifecycleEventsEnabled": () => ({}),
"Page.navigate": () => { "Page.navigate": () => {
@@ -72,9 +69,7 @@ function makeFakeCdp(opts?: {
name: opts.fireLifecycleDuringNavigate, name: opts.fireLifecycleDuringNavigate,
frameId: opts.fireLifecycleDuringNavigateFrameId ?? opts.navigateFrameId ?? "frame-1", frameId: opts.fireLifecycleDuringNavigateFrameId ?? opts.navigateFrameId ?? "frame-1",
loaderId: loaderId:
opts.fireLifecycleDuringNavigateLoaderId ?? opts.fireLifecycleDuringNavigateLoaderId ?? opts.navigateLoaderId ?? "loader-after",
opts.navigateLoaderId ??
"loader-after",
}; };
for (const listener of [...events]) { for (const listener of [...events]) {
listener({ tabId: 4 }, "Page.lifecycleEvent", payload); listener({ tabId: 4 }, "Page.lifecycleEvent", payload);
@@ -208,9 +203,7 @@ describe("lifecycleMeetsOrExceeds", () => {
describe("eventLoaderIsRelevant", () => { describe("eventLoaderIsRelevant", () => {
it("rejects events from the pre-navigation loader", () => { it("rejects events from the pre-navigation loader", () => {
expect( expect(eventLoaderIsRelevant("loader-before", { beforeLoaderId: "loader-before" })).toBe(false);
eventLoaderIsRelevant("loader-before", { beforeLoaderId: "loader-before" }),
).toBe(false);
expect( expect(
eventLoaderIsRelevant("loader-after", { eventLoaderIsRelevant("loader-after", {
beforeLoaderId: "loader-before", beforeLoaderId: "loader-before",
@@ -42,8 +42,7 @@ function makeScreenshotDeps(
opts.get ?? opts.get ??
vi.fn(async (tabId: number) => ({ id: tabId, windowId: 100, active: true }) as chrome.tabs.Tab); vi.fn(async (tabId: number) => ({ id: tabId, windowId: 100, active: true }) as chrome.tabs.Tab);
const query = const query =
opts.query ?? opts.query ?? vi.fn(async () => [{ id: 7, windowId: 100, active: true } as chrome.tabs.Tab]);
vi.fn(async () => [{ id: 7, windowId: 100, active: true } as chrome.tabs.Tab]);
const captureVisibleTab = const captureVisibleTab =
opts.captureVisibleTab ?? vi.fn(async () => `data:image/png;base64,${TINY_PNG}`); opts.captureVisibleTab ?? vi.fn(async () => `data:image/png;base64,${TINY_PNG}`);
const tabsApi = { get, query }; const tabsApi = { get, query };
@@ -118,7 +117,11 @@ describe("handleScreenshot", () => {
const res = await handleScreenshot( const res = await handleScreenshot(
sm, sm,
{ session_id: "aa11" }, { session_id: "aa11" },
makeScreenshotDeps({ captureVisibleTab: vi.fn(), get: emptyGet, query: vi.fn(async () => []) }), makeScreenshotDeps({
captureVisibleTab: vi.fn(),
get: emptyGet,
query: vi.fn(async () => []),
}),
); );
expect(res).toMatchObject({ code: "not_found" }); expect(res).toMatchObject({ code: "not_found" });
}); });
@@ -139,9 +139,13 @@ describe("handleSessionStop with auto-return", () => {
resetAgentOverlays: vi.fn(async () => {}), resetAgentOverlays: vi.fn(async () => {}),
} satisfies AgentOverlayResetApi; } satisfies AgentOverlayResetApi;
const res = await handleSessionStop(sm, { session_id: "aa11" }, { const res = await handleSessionStop(
tabManagement: { tabs, windows, agentOverlayReset }, sm,
}); { session_id: "aa11" },
{
tabManagement: { tabs, windows, agentOverlayReset },
},
);
if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`);
expect(agentOverlayReset.resetAgentOverlays).toHaveBeenCalledWith(7, "aa11"); expect(agentOverlayReset.resetAgentOverlays).toHaveBeenCalledWith(7, "aa11");
@@ -20,9 +20,7 @@ describe("lookupSession", () => {
expect(lookupSession(sm, { session_id: "" }, "tool.test")).toMatchObject({ expect(lookupSession(sm, { session_id: "" }, "tool.test")).toMatchObject({
code: "invalid_params", code: "invalid_params",
}); });
expect( expect(lookupSession(sm, { session_id: 42 as unknown as string }, "tool.test")).toMatchObject({
lookupSession(sm, { session_id: 42 as unknown as string }, "tool.test"),
).toMatchObject({
code: "invalid_params", code: "invalid_params",
}); });
}); });
@@ -292,7 +292,10 @@ describe("handleTabClose", () => {
}; };
const { api } = makeTabMutationApi(state); const { api } = makeTabMutationApi(state);
const res = await handleTabClose(sm, { session_id: "aa11", tab_id: 9 }, { tabs: api }); const res = await handleTabClose(sm, { session_id: "aa11", tab_id: 9 }, { tabs: api });
expect(res).toMatchObject({ code: "permission_denied", data: { reason: "agent_window_scope" } }); expect(res).toMatchObject({
code: "permission_denied",
data: { reason: "agent_window_scope" },
});
}); });
it("rejects when another session has borrowed the tab", async () => { it("rejects when another session has borrowed the tab", async () => {
@@ -338,7 +341,10 @@ describe("handleTabSelect", () => {
}; };
const { api } = makeTabMutationApi(state); const { api } = makeTabMutationApi(state);
const res = await handleTabSelect(sm, { session_id: "aa11", tab_id: 3 }, { tabs: api }); const res = await handleTabSelect(sm, { session_id: "aa11", tab_id: 3 }, { tabs: api });
expect(res).toMatchObject({ code: "permission_denied", data: { reason: "agent_window_scope" } }); expect(res).toMatchObject({
code: "permission_denied",
data: { reason: "agent_window_scope" },
});
}); });
it("rejects a borrowed tab that is no longer in the Agent Window", async () => { it("rejects a borrowed tab that is no longer in the Agent Window", async () => {
@@ -352,7 +358,10 @@ describe("handleTabSelect", () => {
}; };
const { api } = makeTabMutationApi(state); const { api } = makeTabMutationApi(state);
const res = await handleTabSelect(sm, { session_id: "aa11", tab_id: 3 }, { tabs: api }); const res = await handleTabSelect(sm, { session_id: "aa11", tab_id: 3 }, { tabs: api });
expect(res).toMatchObject({ code: "permission_denied", data: { reason: "agent_window_scope" } }); expect(res).toMatchObject({
code: "permission_denied",
data: { reason: "agent_window_scope" },
});
}); });
}); });
@@ -444,7 +453,10 @@ describe("handleTabBorrow", () => {
}; };
const { api } = makeTabMutationApi(state); const { api } = makeTabMutationApi(state);
const res = await handleTabBorrow(sm, { session_id: "aa11", tab_id: 7 }, { tabs: api }); const res = await handleTabBorrow(sm, { session_id: "aa11", tab_id: 7 }, { tabs: api });
expect(res).toMatchObject({ code: "permission_denied", data: { reason: "agent_window_scope" } }); expect(res).toMatchObject({
code: "permission_denied",
data: { reason: "agent_window_scope" },
});
}); });
it("returns cancelled when the approver declines", async () => { it("returns cancelled when the approver declines", async () => {
+1 -1
View File
@@ -18,6 +18,7 @@
import { ChromiumCdp } from "@/browser-driver/chromium-cdp"; import { ChromiumCdp } from "@/browser-driver/chromium-cdp";
import type { SessionManager } from "@/session-manager/manager"; import type { SessionManager } from "@/session-manager/manager";
import type { EvaluateError, EvaluateParams, EvaluateResult, RpcError } from "@/transport/types"; import type { EvaluateError, EvaluateParams, EvaluateResult, RpcError } from "@/transport/types";
import { attachDialogs, markDialogCursor } from "./dialogs";
import { import {
type CdpRunner, type CdpRunner,
type ChromeTabsApi, type ChromeTabsApi,
@@ -27,7 +28,6 @@ import {
lookupSession, lookupSession,
resolveTargetTab, resolveTargetTab,
} from "./shared"; } from "./shared";
import { attachDialogs, markDialogCursor } from "./dialogs";
export interface EvaluateDeps { export interface EvaluateDeps {
cdp: CdpRunner; cdp: CdpRunner;
+11 -9
View File
@@ -6,7 +6,6 @@
// until the user clicks Continue / Cancel, the wait times out, or the // until the user clicks Continue / Cancel, the wait times out, or the
// daemon cancels the RPC. // daemon cancels the RPC.
import type { SessionManager } from "@/session-manager/manager";
import { import {
HELP_CANCEL, HELP_CANCEL,
HELP_REQUEST, HELP_REQUEST,
@@ -15,6 +14,7 @@ import {
type HelpRequestMessage, type HelpRequestMessage,
type HelpResponseMessage, type HelpResponseMessage,
} from "@/lib/help-bridge"; } from "@/lib/help-bridge";
import type { SessionManager } from "@/session-manager/manager";
import type { import type {
HelpTarget, HelpTarget,
RequestHelpParams, RequestHelpParams,
@@ -22,7 +22,6 @@ import type {
ResolvedTarget, ResolvedTarget,
RpcError, RpcError,
} from "@/transport/types"; } from "@/transport/types";
import { lookupSnapshotRef } from "./snapshot-ref";
import { import {
type CdpRunner, type CdpRunner,
type ChromeTabsApi, type ChromeTabsApi,
@@ -32,6 +31,7 @@ import {
lookupSession, lookupSession,
resolveTargetTab, resolveTargetTab,
} from "./shared"; } from "./shared";
import { lookupSnapshotRef } from "./snapshot-ref";
const DEFAULT_HELP_TIMEOUT_MS = 300_000; const DEFAULT_HELP_TIMEOUT_MS = 300_000;
/** Attribute the overlay highlights for resolved `ref` targets. */ /** Attribute the overlay highlights for resolved `ref` targets. */
@@ -46,7 +46,9 @@ export type TabNavigationUnsubscribe = () => void;
export interface RequestHelpDeps { export interface RequestHelpDeps {
tabsApi: ChromeTabsApi; tabsApi: ChromeTabsApi;
windows: { update(windowId: number, info: { focused?: boolean }): Promise<chrome.windows.Window> }; windows: {
update(windowId: number, info: { focused?: boolean }): Promise<chrome.windows.Window>;
};
/** Activate a tab inside its window. */ /** Activate a tab inside its window. */
activateTab(tabId: number): Promise<void>; activateTab(tabId: number): Promise<void>;
/** Send the help-request to the tab's content script and await reply. */ /** Send the help-request to the tab's content script and await reply. */
@@ -68,10 +70,7 @@ export function defaultWatchTabNavigation(
tabId: number, tabId: number,
onNavigated: () => void, onNavigated: () => void,
): TabNavigationUnsubscribe { ): TabNavigationUnsubscribe {
const listener = ( const listener = (updatedTabId: number, changeInfo: chrome.tabs.TabChangeInfo) => {
updatedTabId: number,
changeInfo: chrome.tabs.TabChangeInfo,
) => {
if (updatedTabId !== tabId) return; if (updatedTabId !== tabId) return;
if (changeInfo.url !== undefined || changeInfo.status === "loading") { if (changeInfo.url !== undefined || changeInfo.status === "loading") {
chrome.tabs.onUpdated.removeListener(listener); chrome.tabs.onUpdated.removeListener(listener);
@@ -102,7 +101,8 @@ function getDefaultDeps(): RequestHelpDeps {
else resolve(rid ?? id); else resolve(rid ?? id);
}), }),
), ),
clear: (id) => new Promise((resolve) => chrome.notifications.clear(id, (c) => resolve(c ?? false))), clear: (id) =>
new Promise((resolve) => chrome.notifications.clear(id, (c) => resolve(c ?? false))),
}, },
}; };
} }
@@ -167,7 +167,9 @@ async function selectorExists(
async function clearRefTags(cdp: CdpRunner | undefined, tabId: number): Promise<void> { async function clearRefTags(cdp: CdpRunner | undefined, tabId: number): Promise<void> {
if (!cdp) return; if (!cdp) return;
try { try {
const doc = await cdp.send<{ root?: { nodeId?: number } }>(tabId, "DOM.getDocument", { depth: 0 }); const doc = await cdp.send<{ root?: { nodeId?: number } }>(tabId, "DOM.getDocument", {
depth: 0,
});
const rootId = doc.root?.nodeId; const rootId = doc.root?.nodeId;
if (rootId === undefined) return; if (rootId === undefined) return;
const { nodeIds } = await cdp.send<{ nodeIds: number[] }>(tabId, "DOM.querySelectorAll", { const { nodeIds } = await cdp.send<{ nodeIds: number[] }>(tabId, "DOM.querySelectorAll", {
+6 -4
View File
@@ -21,10 +21,11 @@ import type {
MouseButton, MouseButton,
PressParams, PressParams,
PressResult, PressResult,
RpcError,
SelectParams, SelectParams,
SelectResult, SelectResult,
RpcError,
} from "@/transport/types"; } from "@/transport/types";
import { attachDialogs, markDialogCursor } from "./dialogs";
import { import {
backendNodeToObject, backendNodeToObject,
boxCentre, boxCentre,
@@ -33,7 +34,6 @@ import {
scrollNodeIntoView, scrollNodeIntoView,
} from "./element-geometry"; } from "./element-geometry";
import { rpcError } from "./errors"; import { rpcError } from "./errors";
import { resolveSnapshotRef } from "./snapshot-ref";
import { import {
type CdpRunner, type CdpRunner,
type ChromeTabsApi, type ChromeTabsApi,
@@ -43,7 +43,7 @@ import {
lookupSession, lookupSession,
resolveTargetTab, resolveTargetTab,
} from "./shared"; } from "./shared";
import { attachDialogs, markDialogCursor } from "./dialogs"; import { resolveSnapshotRef } from "./snapshot-ref";
export interface InteractionDeps { export interface InteractionDeps {
cdp: CdpRunner; cdp: CdpRunner;
@@ -858,7 +858,9 @@ export async function handleSelect(
); );
} }
const attrs = described.node.attributes ?? []; const attrs = described.node.attributes ?? [];
const isMultiple = attrs.some((attr, idx) => idx % 2 === 0 && attr.toLowerCase() === "multiple"); const isMultiple = attrs.some(
(attr, idx) => idx % 2 === 0 && attr.toLowerCase() === "multiple",
);
if (!isMultiple && params.values.length !== 1) { if (!isMultiple && params.values.length !== 1) {
return rpcError( return rpcError(
"invalid_params", "invalid_params",
+9 -5
View File
@@ -28,6 +28,7 @@ import type {
RpcError, RpcError,
WaitUntil, WaitUntil,
} from "@/transport/types"; } from "@/transport/types";
import { attachDialogs, markDialogCursor } from "./dialogs";
import { import {
type CdpRunner, type CdpRunner,
type ChromeTabsApi, type ChromeTabsApi,
@@ -37,7 +38,6 @@ import {
lookupSession, lookupSession,
resolveTargetTab, resolveTargetTab,
} from "./shared"; } from "./shared";
import { attachDialogs, markDialogCursor } from "./dialogs";
export interface NavigationDeps { export interface NavigationDeps {
cdp: CdpRunner; cdp: CdpRunner;
@@ -120,9 +120,7 @@ export function lifecycleAlreadyReached(readyState: string, targetName: string):
case "DOMContentLoaded": case "DOMContentLoaded":
return readyState === "interactive" || readyState === "complete"; return readyState === "interactive" || readyState === "complete";
case "commit": case "commit":
return ( return readyState === "interactive" || readyState === "complete" || readyState === "loading";
readyState === "interactive" || readyState === "complete" || readyState === "loading"
);
case "networkIdle": case "networkIdle":
// Must come from `Page.lifecycleEvent`; readyState does not imply network idle. // Must come from `Page.lifecycleEvent`; readyState does not imply network idle.
return false; return false;
@@ -285,7 +283,13 @@ function startLifecycleWait(
const maybeFinishPending = () => { const maybeFinishPending = () => {
if (settled || !pendingLifecycle) return; if (settled || !pendingLifecycle) return;
if (!acceptLifecycleEvent(pendingLifecycle.name, pendingLifecycle.frameId, pendingLifecycle.loaderId)) { if (
!acceptLifecycleEvent(
pendingLifecycle.name,
pendingLifecycle.frameId,
pendingLifecycle.loaderId,
)
) {
return; return;
} }
if (lifecycleMeetsOrExceeds(pendingLifecycle.name, targetName)) { if (lifecycleMeetsOrExceeds(pendingLifecycle.name, targetName)) {
+2 -2
View File
@@ -14,8 +14,8 @@ import type {
SnapshotParams, SnapshotParams,
SnapshotResult, SnapshotResult,
} from "@/transport/types"; } from "@/transport/types";
import { attachDialogs, markDialogCursor } from "./dialogs";
import { nodeBoundingRect, scrollNodeIntoView } from "./element-geometry"; import { nodeBoundingRect, scrollNodeIntoView } from "./element-geometry";
import { resolveSnapshotRef } from "./snapshot-ref";
import { rpcError } from "./errors"; import { rpcError } from "./errors";
import { import {
type ChromeTabsApi, type ChromeTabsApi,
@@ -25,7 +25,7 @@ import {
type CdpRunner as SharedCdpRunner, type CdpRunner as SharedCdpRunner,
normaliseRef as sharedNormaliseRef, normaliseRef as sharedNormaliseRef,
} from "./shared"; } from "./shared";
import { attachDialogs, markDialogCursor } from "./dialogs"; import { resolveSnapshotRef } from "./snapshot-ref";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Shared helpers (legacy aliases — observation.ts kept exporting these // Shared helpers (legacy aliases — observation.ts kept exporting these
+1 -1
View File
@@ -20,6 +20,7 @@ import type {
WaitForNavigationResult, WaitForNavigationResult,
WaitUntil, WaitUntil,
} from "@/transport/types"; } from "@/transport/types";
import { attachDialogs, markDialogCursor } from "./dialogs";
import { cdpLifecycleName, ensureCdpReady, waitForLifecyclePassive } from "./navigation"; import { cdpLifecycleName, ensureCdpReady, waitForLifecyclePassive } from "./navigation";
import { import {
type CdpRunner, type CdpRunner,
@@ -30,7 +31,6 @@ import {
lookupSession, lookupSession,
resolveTargetTab, resolveTargetTab,
} from "./shared"; } from "./shared";
import { attachDialogs, markDialogCursor } from "./dialogs";
export interface WaitForNavigationDeps { export interface WaitForNavigationDeps {
cdp: CdpRunner; cdp: CdpRunner;
-3
View File
@@ -43,7 +43,6 @@
--input: oklch(0.35 0.02 60); --input: oklch(0.35 0.02 60);
--ring: oklch(0.7 0.18 45); --ring: oklch(0.7 0.18 45);
} }
@theme inline { @theme inline {
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
@@ -68,7 +67,6 @@
--color-input: var(--input); --color-input: var(--input);
--color-ring: var(--ring); --color-ring: var(--ring);
} }
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
@@ -84,7 +82,6 @@
body { body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }
+1 -4
View File
@@ -85,10 +85,7 @@ function assetEntry(platformKey, triple) {
} }
const assets = Object.fromEntries( const assets = Object.fromEntries(
Object.entries(PLATFORMS).map(([key, triple]) => [ Object.entries(PLATFORMS).map(([key, triple]) => [key, assetEntry(key, triple)]),
key,
assetEntry(key, triple),
]),
); );
const manifest = { const manifest = {
+4 -10
View File
@@ -1,11 +1,11 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { execFileSync } from "node:child_process";
import test from "node:test"; import test from "node:test";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url)); const scriptDir = dirname(fileURLToPath(import.meta.url));
const scriptPath = join(scriptDir, "render-version-json.mjs"); const scriptPath = join(scriptDir, "render-version-json.mjs");
@@ -20,14 +20,8 @@ test("renders asset URLs with sha256 checksums from dist files", () => {
mkdirSync(dist); mkdirSync(dist);
writeFileSync(join(dist, archive), archiveBytes); writeFileSync(join(dist, archive), archiveBytes);
writeFileSync(join(dist, `bsk-v${version}-x86_64-apple-darwin.tar.gz`), "darwin x64\n"); writeFileSync(join(dist, `bsk-v${version}-x86_64-apple-darwin.tar.gz`), "darwin x64\n");
writeFileSync( writeFileSync(join(dist, `bsk-v${version}-x86_64-unknown-linux-musl.tar.gz`), "linux x64\n");
join(dist, `bsk-v${version}-x86_64-unknown-linux-musl.tar.gz`), writeFileSync(join(dist, `bsk-v${version}-aarch64-unknown-linux-musl.tar.gz`), "linux arm64\n");
"linux x64\n",
);
writeFileSync(
join(dist, `bsk-v${version}-aarch64-unknown-linux-musl.tar.gz`),
"linux arm64\n",
);
writeFileSync(join(dist, `bsk-v${version}-x86_64-pc-windows-msvc.zip`), "windows x64\n"); writeFileSync(join(dist, `bsk-v${version}-x86_64-pc-windows-msvc.zip`), "windows x64\n");
execFileSync(process.execPath, [ execFileSync(process.execPath, [