Skip to content

Commit b657ab5

Browse files
authored
Merge pull request #43 from Tencent/fix/early-control-return
fix(extension): fix early control return and preserve request-help handoff across tabs LGTM
2 parents c8963e3 + 959bfac commit b657ab5

17 files changed

Lines changed: 1390 additions & 358 deletions

File tree

apps/extension/src/content/HelpRequestOverlay.tsx

Lines changed: 170 additions & 114 deletions
Large diffs are not rendered by default.

apps/extension/src/content/__tests__/HelpRequestOverlay.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,31 @@ describe("HelpRequestOverlay", () => {
3838
expect(screen.getByText("Please complete the captcha")).toBeTruthy();
3939
});
4040

41+
it("renders a compact status without full request controls", () => {
42+
const el = document.createElement("div");
43+
el.id = "login";
44+
el.getBoundingClientRect = () =>
45+
({ top: 10, left: 10, width: 100, height: 40, right: 110, bottom: 50 }) as DOMRect;
46+
document.body.append(el);
47+
48+
const { container } = renderOverlay(
49+
baseRequest({
50+
displayMode: "compact",
51+
selectors: ["#login"],
52+
}),
53+
);
54+
55+
expect(screen.getByText(i18n.t("helpRequest.compactStatus", { ns: "extension" }))).toBeTruthy();
56+
expect(screen.queryByText("Please complete the captcha")).toBeNull();
57+
expect(screen.queryByRole("textbox")).toBeNull();
58+
expect(screen.queryByLabelText(i18n.t("helpRequest.collapse", { ns: "extension" }))).toBeNull();
59+
expect(container.querySelector("[data-slot='help-continue-button']")).toBeNull();
60+
expect(container.querySelector("[data-slot='help-cancel-button']")).toBeNull();
61+
expect(document.querySelectorAll("[data-slot='help-highlight']").length).toBe(0);
62+
63+
el.remove();
64+
});
65+
4166
it("renders a custom title when provided", () => {
4267
renderOverlay(baseRequest({ title: "Verify your identity" }));
4368
expect(screen.getByText("Verify your identity")).toBeTruthy();
@@ -204,6 +229,15 @@ describe("HelpRequestOverlay", () => {
204229
expect(styles).toContain("width: 0");
205230
});
206231

232+
it("uses natural body height in the expanded layout", () => {
233+
const { container } = renderOverlay(baseRequest());
234+
const styles = overlayStyles(container);
235+
236+
expect(styles).toMatch(/\.bsk-help-body\s*\{[^}]*display:\s*block;/s);
237+
expect(styles).not.toMatch(/\.bsk-help-body\s*\{[^}]*grid-template-rows/s);
238+
expect(styles).toMatch(/\.bsk-help-banner\s*\{[^}]*justify-content:\s*flex-start;/s);
239+
});
240+
207241
it("keeps the same banner width when collapsed", () => {
208242
const { container } = renderOverlay(baseRequest());
209243
const styles = overlayStyles(container);

apps/extension/src/entrypoints/content.ts

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ import {
1414
type RecordCaptureController,
1515
} from "@/content/record-capture";
1616
import {
17-
HELP_RESPONSE,
17+
HELP_ACK,
18+
HELP_FINISH,
19+
HELP_QUERY,
20+
type HelpAckMessage,
1821
type HelpCancelMessage,
22+
type HelpFinishMessage,
23+
type HelpQueryResponse,
1924
type HelpRequestMessage,
20-
type HelpResponseMessage,
2125
isHelpCancelMessage,
2226
isHelpRequestMessage,
2327
} from "@/lib/help-bridge";
@@ -55,8 +59,6 @@ export default defineContentScript({
5559
if (window.top !== window) return;
5660

5761
const overlays = new OverlayController();
58-
let activeHelpRespond: ((outcome: "continued" | "cancelled", note?: string) => void) | null =
59-
null;
6062
let recordCapture: RecordCaptureController | null = null;
6163
let activeRecordRequestId: string | null = null;
6264
let reactRoot: ReactDOM.Root | null = null;
@@ -116,8 +118,7 @@ export default defineContentScript({
116118
function resetAgentOverlayState(sessionId: string) {
117119
const previousHelp = overlays.resetAgentOverlays(sessionId);
118120
if (previousHelp) {
119-
activeHelpRespond?.("cancelled");
120-
activeHelpRespond = null;
121+
void sendHelpFinish(previousHelp.id, "cancelled");
121122
}
122123
recordCapture?.dispose();
123124
recordCapture = null;
@@ -156,7 +157,7 @@ export default defineContentScript({
156157
| OverlayAgentOverlayResetMessage
157158
| OverlayAutomationBypassMessage,
158159
_sender: chrome.runtime.MessageSender,
159-
sendResponse: (response: BorrowResponseMessage | HelpResponseMessage) => void,
160+
sendResponse: (response: BorrowResponseMessage | HelpAckMessage) => void,
160161
) => {
161162
if (isRecordContentMessage(message)) {
162163
const needsAsync = handleRecordContentMessage(
@@ -220,41 +221,30 @@ export default defineContentScript({
220221
if (isHelpCancelMessage(message)) {
221222
const state = overlays.snapshot();
222223
if (state.activeHelp && state.activeHelp.id === message.requestId) {
223-
activeHelpRespond?.("cancelled");
224+
overlays.clearAgentHelpRequest(message.requestId);
225+
renderOverlay();
224226
}
225227
return false;
226228
}
227229

228230
if (isHelpRequestMessage(message)) {
229231
const helpMsg = message as HelpRequestMessage;
230-
let responded = false;
231-
const respond = (outcome: "continued" | "cancelled", note?: string) => {
232-
if (responded) return;
233-
responded = true;
234-
const reply: HelpResponseMessage = {
235-
type: HELP_RESPONSE,
236-
outcome,
237-
...(note ? { note } : {}),
238-
};
239-
sendResponse(reply);
240-
activeHelpRespond = null;
241-
overlays.clearAgentHelpRequest(helpMsg.requestId);
242-
renderOverlay();
243-
};
244232
const previousHelp = overlays.setAgentHelpRequest({
245233
id: helpMsg.requestId,
246234
prompt: helpMsg.prompt,
247235
...(helpMsg.title ? { title: helpMsg.title } : {}),
236+
...(helpMsg.displayMode ? { displayMode: helpMsg.displayMode } : {}),
248237
selectors: helpMsg.selectors,
249-
onContinue: (note: string) => respond("continued", note.trim() ? note : undefined),
250-
onCancel: () => respond("cancelled"),
238+
onContinue: (note: string) =>
239+
void sendHelpFinish(helpMsg.requestId, "continued", note.trim() ? note : undefined),
240+
onCancel: () => void sendHelpFinish(helpMsg.requestId, "cancelled"),
251241
});
252-
if (previousHelp) {
253-
activeHelpRespond?.("cancelled");
242+
if (previousHelp && previousHelp.id !== helpMsg.requestId) {
243+
void sendHelpFinish(previousHelp.id, "cancelled");
254244
}
255-
activeHelpRespond = respond;
256245
renderOverlay();
257-
return true; // async sendResponse
246+
sendResponse({ type: HELP_ACK, ok: true });
247+
return false;
258248
}
259249

260250
if (message.type === "borrow-request") {
@@ -282,9 +272,60 @@ export default defineContentScript({
282272
return false;
283273
};
284274

275+
async function sendHelpFinish(
276+
requestId: string,
277+
outcome: "continued" | "cancelled",
278+
note?: string,
279+
): Promise<void> {
280+
const msg: HelpFinishMessage = {
281+
type: HELP_FINISH,
282+
requestId,
283+
outcome,
284+
...(note ? { note } : {}),
285+
};
286+
overlays.clearAgentHelpRequest(requestId);
287+
renderOverlay();
288+
await chrome.runtime.sendMessage(msg).catch((err) => {
289+
console.debug("[bsk overlay] help finish failed", err);
290+
});
291+
}
292+
293+
function mountHelpRequest(helpMsg: Omit<HelpRequestMessage, "type">): void {
294+
overlays.setAgentHelpRequest({
295+
id: helpMsg.requestId,
296+
prompt: helpMsg.prompt,
297+
...(helpMsg.title ? { title: helpMsg.title } : {}),
298+
...(helpMsg.displayMode ? { displayMode: helpMsg.displayMode } : {}),
299+
selectors: helpMsg.selectors,
300+
onContinue: (note: string) =>
301+
void sendHelpFinish(helpMsg.requestId, "continued", note.trim() ? note : undefined),
302+
onCancel: () => void sendHelpFinish(helpMsg.requestId, "cancelled"),
303+
});
304+
}
305+
306+
async function queryActiveHelpWithRetry(): Promise<boolean> {
307+
for (let attempt = 0; attempt < 6; attempt += 1) {
308+
try {
309+
const helpQuery = (await chrome.runtime.sendMessage({
310+
type: HELP_QUERY,
311+
})) as HelpQueryResponse | undefined;
312+
if (helpQuery?.active && helpQuery.request) {
313+
mountHelpRequest(helpQuery.request);
314+
renderOverlay();
315+
return true;
316+
}
317+
} catch (err) {
318+
console.debug("[bsk overlay] help query failed", err);
319+
}
320+
await new Promise((resolve) => window.setTimeout(resolve, 150));
321+
}
322+
return false;
323+
}
324+
285325
async function syncAgentOverlay(): Promise<void> {
286326
if (!(await anySessionLive())) return;
287327
try {
328+
const helpActive = await queryActiveHelpWithRetry();
288329
const reply = (await chrome.runtime.sendMessage({
289330
kind: OVERLAY_MSG_WHO_AM_I,
290331
})) as OverlayWhoAmIResponse | undefined;
@@ -319,6 +360,10 @@ export default defineContentScript({
319360
});
320361
}
321362

363+
if (!helpActive && overlays.snapshot().activeHelp === null) {
364+
void queryActiveHelpWithRetry();
365+
}
366+
322367
overlays.activateAgentSession(reply.sessionId);
323368
renderOverlay();
324369
} catch (err) {

apps/extension/src/lib/__tests__/help-bridge.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
import { describe, expect, it } from "vitest";
22
import {
3+
HELP_ACK,
34
HELP_CANCEL,
5+
HELP_FINISH,
6+
HELP_QUERY,
47
HELP_REQUEST,
58
HELP_RESPONSE,
9+
isHelpAckMessage,
610
isHelpCancelMessage,
11+
isHelpFinishMessage,
12+
isHelpQueryMessage,
713
isHelpRequestMessage,
14+
isHelpResponseMessage,
815
} from "../help-bridge";
916

1017
describe("help-bridge", () => {
1118
it("exposes stable message type constants", () => {
1219
expect(HELP_REQUEST).toBe("bsk-help-request");
1320
expect(HELP_RESPONSE).toBe("bsk-help-response");
1421
expect(HELP_CANCEL).toBe("bsk-help-cancel");
22+
expect(HELP_ACK).toBe("bsk-help-ack");
23+
expect(HELP_QUERY).toBe("bsk-help-query");
24+
expect(HELP_FINISH).toBe("bsk-help-finish");
1525
});
1626

1727
it("type-guards a help-request message", () => {
@@ -20,6 +30,7 @@ describe("help-bridge", () => {
2030
type: HELP_REQUEST,
2131
requestId: "r1",
2232
prompt: "log in",
33+
displayMode: "compact",
2334
selectors: ["#login"],
2435
timeoutMs: 1000,
2536
}),
@@ -55,4 +66,18 @@ describe("help-bridge", () => {
5566
expect(isHelpCancelMessage({ type: HELP_REQUEST, requestId: "r1" })).toBe(false);
5667
expect(isHelpCancelMessage(null)).toBe(false);
5768
});
69+
70+
it("type-guards lifecycle messages", () => {
71+
expect(isHelpAckMessage({ type: HELP_ACK, ok: true })).toBe(true);
72+
expect(isHelpQueryMessage({ type: HELP_QUERY })).toBe(true);
73+
expect(isHelpFinishMessage({ type: HELP_FINISH, requestId: "r1", outcome: "continued" })).toBe(
74+
true,
75+
);
76+
expect(isHelpResponseMessage({ type: HELP_RESPONSE, outcome: "cancelled", note: "no" })).toBe(
77+
true,
78+
);
79+
expect(isHelpFinishMessage({ type: HELP_FINISH, requestId: "r1", outcome: "weird" })).toBe(
80+
false,
81+
);
82+
});
5883
});

apps/extension/src/lib/help-bridge.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,34 @@
66
* Mirrors the borrow-confirmation message style (see
77
* `@/tools/borrow-confirmation`). The background asks a specific tab to
88
* enter "help mode" (render HelpRequestOverlay + hide ControlOverlay);
9-
* the content script replies once the user clicks Continue / Cancel.
9+
* the content script acks display, then later reports Done / Cancel.
1010
*/
1111

1212
export const HELP_REQUEST = "bsk-help-request";
1313
export const HELP_RESPONSE = "bsk-help-response";
1414
export const HELP_CANCEL = "bsk-help-cancel";
15+
export const HELP_ACK = "bsk-help-ack";
16+
export const HELP_QUERY = "bsk-help-query";
17+
export const HELP_FINISH = "bsk-help-finish";
1518

1619
export interface HelpRequestMessage {
1720
type: typeof HELP_REQUEST;
1821
requestId: string;
1922
prompt: string;
2023
/** Custom overlay title; omitted when the extension should use its default. */
2124
title?: string;
25+
/** Full task UI on the subject tab; compact status UI on related tabs. */
26+
displayMode?: "full" | "compact";
2227
/** CSS selectors to scroll to + flash-highlight (may be empty). */
2328
selectors: string[];
2429
timeoutMs: number;
2530
}
2631

32+
export interface HelpAckMessage {
33+
type: typeof HELP_ACK;
34+
ok: true;
35+
}
36+
2737
export interface HelpResponseMessage {
2838
type: typeof HELP_RESPONSE;
2939
outcome: "continued" | "cancelled";
@@ -35,6 +45,22 @@ export interface HelpCancelMessage {
3545
requestId: string;
3646
}
3747

48+
export interface HelpQueryMessage {
49+
type: typeof HELP_QUERY;
50+
}
51+
52+
export interface HelpQueryResponse {
53+
active: boolean;
54+
request?: Omit<HelpRequestMessage, "type">;
55+
}
56+
57+
export interface HelpFinishMessage {
58+
type: typeof HELP_FINISH;
59+
requestId: string;
60+
outcome: "continued" | "cancelled";
61+
note?: string;
62+
}
63+
3864
export function isHelpRequestMessage(msg: unknown): msg is HelpRequestMessage {
3965
if (typeof msg !== "object" || msg === null) {
4066
return false;
@@ -45,6 +71,7 @@ export function isHelpRequestMessage(msg: unknown): msg is HelpRequestMessage {
4571
typeof m.requestId === "string" &&
4672
typeof m.prompt === "string" &&
4773
(m.title === undefined || typeof m.title === "string") &&
74+
(m.displayMode === undefined || m.displayMode === "full" || m.displayMode === "compact") &&
4875
Array.isArray(m.selectors) &&
4976
m.selectors.every((selector) => typeof selector === "string") &&
5077
typeof m.timeoutMs === "number"
@@ -58,3 +85,36 @@ export function isHelpCancelMessage(msg: unknown): msg is HelpCancelMessage {
5885
const m = msg as Record<string, unknown>;
5986
return m.type === HELP_CANCEL && typeof m.requestId === "string";
6087
}
88+
89+
export function isHelpResponseMessage(msg: unknown): msg is HelpResponseMessage {
90+
if (typeof msg !== "object" || msg === null) return false;
91+
const m = msg as Record<string, unknown>;
92+
return (
93+
m.type === HELP_RESPONSE &&
94+
(m.outcome === "continued" || m.outcome === "cancelled") &&
95+
(m.note === undefined || typeof m.note === "string")
96+
);
97+
}
98+
99+
export function isHelpAckMessage(msg: unknown): msg is HelpAckMessage {
100+
if (typeof msg !== "object" || msg === null) return false;
101+
const m = msg as Record<string, unknown>;
102+
return m.type === HELP_ACK && m.ok === true;
103+
}
104+
105+
export function isHelpQueryMessage(msg: unknown): msg is HelpQueryMessage {
106+
if (typeof msg !== "object" || msg === null) return false;
107+
const m = msg as Record<string, unknown>;
108+
return m.type === HELP_QUERY;
109+
}
110+
111+
export function isHelpFinishMessage(msg: unknown): msg is HelpFinishMessage {
112+
if (typeof msg !== "object" || msg === null) return false;
113+
const m = msg as Record<string, unknown>;
114+
return (
115+
m.type === HELP_FINISH &&
116+
typeof m.requestId === "string" &&
117+
(m.outcome === "continued" || m.outcome === "cancelled") &&
118+
(m.note === undefined || typeof m.note === "string")
119+
);
120+
}

0 commit comments

Comments
 (0)