feat(extension): add popup toggle to hide Agent control hints (#81)

Add a per-browser "Control hints" switch to the popup that hides the in-page control overlay (status pill + orange glow, along with its input blocker) while the Agent controls a tab. Preference lives in chrome.storage.local (bsk_control_hints_hidden, default: shown) and is applied live in content scripts via storage.onChanged.

Both popup settings rows share a single Switch component; the hint copy lives in an info-icon tooltip.

Refs #62
This commit is contained in:
BB-fat
2026-08-13 10:24:17 +08:00
committed by GitHub
parent f81a1e7796
commit 37fbaacc94
12 changed files with 373 additions and 20 deletions
@@ -1,6 +1,7 @@
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { SnapshotInfo } from "@/lib/connection-controller";
import { STORAGE_KEYS } from "@/lib/instance-id";
import { EXTENSION_VERSION } from "@/transport/handshake";
import { App } from "./App";
import { useConnectionState } from "./use-connection-state";
@@ -244,3 +245,94 @@ describe("App", () => {
expect(screen.getByText("连接后可用")).toBeTruthy();
});
});
describe("control hints toggle", () => {
function stubChromeStorage(initial: Record<string, unknown> = {}) {
const store = { ...initial };
vi.stubGlobal("chrome", {
runtime: { lastError: undefined },
storage: {
local: {
get: (keys: string | string[], cb: (items: Record<string, unknown>) => void) => {
const items: Record<string, unknown> = {};
for (const k of Array.isArray(keys) ? keys : [keys]) {
if (k in store) items[k] = store[k];
}
cb(items);
},
set: (items: Record<string, unknown>, cb?: () => void) => {
Object.assign(store, items);
cb?.();
},
},
onChanged: {
addListener: vi.fn(),
removeListener: vi.fn(),
},
},
});
return store;
}
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
it("renders the control hints toggle on when no preference is stored", async () => {
stubChromeStorage();
render(<App />);
const toggle = await screen.findByRole("switch", { name: "控制提示" });
expect(toggle.getAttribute("aria-checked")).toBe("true");
});
it("reflects the stored hidden preference", async () => {
stubChromeStorage({ [STORAGE_KEYS.CONTROL_HINTS_HIDDEN]: true });
render(<App />);
const toggle = await screen.findByRole("switch", { name: "控制提示" });
await waitFor(() => expect(toggle.getAttribute("aria-checked")).toBe("false"));
});
it("persists the hidden preference when the toggle is turned off", async () => {
const store = stubChromeStorage();
render(<App />);
const toggle = await screen.findByRole("switch", { name: "控制提示" });
fireEvent.click(toggle);
expect(store[STORAGE_KEYS.CONTROL_HINTS_HIDDEN]).toBe(true);
expect(toggle.getAttribute("aria-checked")).toBe("false");
});
it("keeps the hint copy in an accessible info tooltip", async () => {
stubChromeStorage();
render(<App />);
const info = await screen.findByRole("button", { name: "控制提示说明" });
expect(info).toBeTruthy();
const tooltip = screen.getByRole("tooltip");
expect(tooltip.textContent).toBe("Agent 控制页面时显示提示条和橙色闪光。");
// Hidden until the info button is hovered or focused.
expect(tooltip.className).toContain("opacity-0");
});
it("uses the same switch component and size for both settings rows", async () => {
stubChromeStorage();
render(<App />);
const hintsToggle = await screen.findByRole("switch", { name: "控制提示" });
const connectionToggle = screen.getByRole("switch", { name: "BrowserSkill 连接" });
// One shared Switch component, one size — hierarchy comes from copy and
// the info icon, not control size. Both rows default to checked, so the
// class strings must be identical.
expect(hintsToggle.className).toContain("h-5 w-9");
expect(hintsToggle.className).toBe(connectionToggle.className);
});
});