fix(extension): CI problem fixed, tab logic fixed

This commit is contained in:
Ljy-0827
2026-07-24 14:07:33 +08:00
parent f73a0e9fb3
commit c095128218
5 changed files with 56 additions and 40 deletions
@@ -197,7 +197,7 @@ describe("handleRequestHelp", () => {
expect(res).toMatchObject({ outcome: "completed", completed_by: "system", tab_id: 5 });
});
it("keeps user control active on new tabs opened in the agent window", async () => {
it("keeps user control active on new tabs without moving completion off the primary tab", async () => {
vi.useFakeTimers();
const chromeEvents = installHelpLifecycleChrome();
const sendToTab = vi.fn(async () => ({ type: "bsk-help-ack", ok: true }));
@@ -246,9 +246,8 @@ describe("handleRequestHelp", () => {
await vi.advanceTimersByTimeAsync(1_000);
await expect(pending).resolves.toMatchObject({
outcome: "completed",
completed_by: "system",
tab_id: 6,
outcome: "timed_out",
tab_id: 5,
});
expect(sendToTab).toHaveBeenCalledWith(
6,
@@ -262,12 +261,13 @@ describe("handleRequestHelp", () => {
it("lets same-window tabs query active help after content-script load", async () => {
const chromeEvents = installHelpLifecycleChrome();
const ac = new AbortController();
const cdpSend = vi.fn(async () => ({ root: { nodeId: 1 }, nodeIds: [] }));
const deps = baseDeps({
autoAttachLifecycle: undefined,
signal: ac.signal,
sendToTab: vi.fn(async () => ({ type: "bsk-help-ack", ok: true })),
cdp: {
send: vi.fn(async () => ({ root: { nodeId: 1 }, nodeIds: [] })),
send: cdpSend,
} as unknown as RequestHelpDeps["cdp"],
});
@@ -315,6 +315,16 @@ describe("handleRequestHelp", () => {
ac.abort();
await expect(pending).resolves.toMatchObject({ code: "cancelled" });
expect(cdpSend).toHaveBeenCalledWith(
5,
"DOM.querySelectorAll",
expect.objectContaining({ selector: "[data-bsk-help]" }),
);
expect(cdpSend).toHaveBeenCalledWith(
6,
"DOM.querySelectorAll",
expect.objectContaining({ selector: "[data-bsk-help]" }),
);
});
it("tags ref targets via CDP and reports them matched", async () => {
+38 -32
View File
@@ -68,7 +68,6 @@ export interface RequestHelpDeps {
interface ActiveHelpRequest {
ctx: SessionContext;
requestId: string;
subjectTabId: number;
primaryTabId: number;
overlayTabIds: Set<number>;
prompt: string;
@@ -126,8 +125,8 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function helpRequestMessage(help: ActiveHelpRequest): HelpRequestMessage {
const isPrimaryTab = help.subjectTabId === help.primaryTabId;
function helpRequestMessage(help: ActiveHelpRequest, tabId: number): HelpRequestMessage {
const isPrimaryTab = tabId === help.primaryTabId;
return {
type: HELP_REQUEST,
requestId: help.requestId,
@@ -277,10 +276,10 @@ async function resolveHelpTargets(
}
async function refreshHelpTargets(help: ActiveHelpRequest): Promise<void> {
await clearRefTags(help.deps.cdp, help.subjectTabId);
await clearRefTags(help.deps.cdp, help.primaryTabId);
const { selectors, resolvedTargets } = await resolveHelpTargets(
help.ctx,
help.subjectTabId,
help.primaryTabId,
help.targets,
help.deps,
);
@@ -289,14 +288,18 @@ async function refreshHelpTargets(help: ActiveHelpRequest): Promise<void> {
}
async function cleanupHelp(help: ActiveHelpRequest): Promise<void> {
await clearRefTags(help.deps.cdp, help.subjectTabId);
if (help.deps.notifications) {
await help.deps.notifications.clear(help.notificationId).catch(() => {});
}
const tabsToCancel = new Set([help.subjectTabId, ...help.overlayTabIds]);
const tabsToCancel = new Set([help.primaryTabId, ...help.overlayTabIds]);
await Promise.all(
[...tabsToCancel].map((tabId) =>
help.deps.sendToTab(tabId, { type: HELP_CANCEL, requestId: help.requestId }).catch(() => {}),
Promise.all([
clearRefTags(help.deps.cdp, tabId),
help.deps
.sendToTab(tabId, { type: HELP_CANCEL, requestId: help.requestId })
.catch(() => {}),
]),
),
);
}
@@ -312,15 +315,15 @@ function finishHelp(
clearTimeout(help.timeoutTimer);
if (help.completionTimer) clearInterval(help.completionTimer);
if (help.abortHandler) help.deps.signal?.removeEventListener("abort", help.abortHandler);
clearRearmTimer(help.subjectTabId);
clearRearmTimer(help.primaryTabId);
for (const tabId of help.overlayTabIds) clearRearmTimer(tabId);
if (notifyContent) void cleanupHelp(help);
help.resolve(value);
}
function findHelpBySubjectTabId(tabId: number): ActiveHelpRequest | null {
function findHelpByKnownTabId(tabId: number): ActiveHelpRequest | null {
for (const help of activeHelpRequests.values()) {
if (!help.settled && (help.subjectTabId === tabId || help.overlayTabIds.has(tabId))) {
if (!help.settled && (help.primaryTabId === tabId || help.overlayTabIds.has(tabId))) {
return help;
}
}
@@ -331,7 +334,7 @@ async function findHelpForTab(
tabId: number,
deps: RequestHelpDeps,
): Promise<ActiveHelpRequest | null> {
const direct = findHelpBySubjectTabId(tabId);
const direct = findHelpByKnownTabId(tabId);
if (direct) return direct;
try {
@@ -356,30 +359,37 @@ function clearRearmTimer(tabId: number): void {
async function sendCurrentHelpOverlay(help: ActiveHelpRequest): Promise<HelpFinishMessage | null> {
const legacyFinish = await sendHelpRequestWithAck(
help.subjectTabId,
helpRequestMessage(help),
help.primaryTabId,
helpRequestMessage(help, help.primaryTabId),
help.deps,
);
help.overlayTabIds.add(help.subjectTabId);
help.overlayTabIds.add(help.primaryTabId);
return legacyFinish;
}
async function refreshAndSendHelpOverlay(
help: ActiveHelpRequest,
tabId: number,
): Promise<HelpFinishMessage | null> {
await refreshHelpTargets(help);
return sendCurrentHelpOverlay(help);
if (tabId === help.primaryTabId) await refreshHelpTargets(help);
const legacyFinish = await sendHelpRequestWithAck(
tabId,
helpRequestMessage(help, tabId),
help.deps,
);
help.overlayTabIds.add(tabId);
return legacyFinish;
}
async function rearmHelp(help: ActiveHelpRequest): Promise<boolean> {
async function rearmHelp(help: ActiveHelpRequest, tabId: number): Promise<boolean> {
for (let attempt = 0; attempt < HELP_REARM_MAX_ATTEMPTS; attempt += 1) {
try {
const legacyFinish = await refreshAndSendHelpOverlay(help);
const legacyFinish = await refreshAndSendHelpOverlay(help, tabId);
if (legacyFinish && !help.settled) {
finishHelp(help, {
outcome: legacyFinish.outcome,
...(legacyFinish.note ? { note: legacyFinish.note } : {}),
tab_id: help.subjectTabId,
tab_id: help.primaryTabId,
resolved_targets: help.resolvedTargets,
});
}
@@ -401,8 +411,7 @@ function scheduleRearmForTab(tabId: number, deps: RequestHelpDeps): void {
void (async () => {
const help = await findHelpForTab(tabId, deps);
if (!help) return;
help.subjectTabId = tabId;
await rearmHelp(help);
await rearmHelp(help, tabId);
})();
}, HELP_REARM_DEBOUNCE_MS),
);
@@ -457,8 +466,7 @@ function attachHelpRuntimeListener(deps: RequestHelpDeps): () => void {
sendResponse({ active: false });
return;
}
help.subjectTabId = tabId;
await refreshHelpTargets(help);
if (tabId === help.primaryTabId) await refreshHelpTargets(help);
help.overlayTabIds.add(tabId);
sendResponse({
active: true,
@@ -492,14 +500,13 @@ async function finishHelpFromContent(
if (tabId !== undefined) {
const match = await findHelpForTab(tabId, deps);
if (match !== help) return;
help.subjectTabId = tabId;
}
finishHelp(
help,
{
outcome: message.outcome,
...(message.note ? { note: message.note } : {}),
tab_id: help.subjectTabId,
tab_id: help.primaryTabId,
resolved_targets: help.resolvedTargets,
},
false,
@@ -551,7 +558,7 @@ async function evaluateCompletionCondition(
if (condition.url_contains || condition.url_matches) {
let url = "";
try {
const tab = await help.deps.tabsApi.get(help.subjectTabId);
const tab = await help.deps.tabsApi.get(help.primaryTabId);
url = tab.url ?? "";
} catch {
return false;
@@ -587,7 +594,7 @@ async function evaluateCompletionCondition(
})()`;
try {
const result = await help.deps.cdp.send<{ result?: { value?: boolean } }>(
help.subjectTabId,
help.primaryTabId,
"Runtime.evaluate",
{ expression, returnByValue: true },
);
@@ -639,7 +646,7 @@ function startCompletionPolling(help: ActiveHelpRequest): void {
finishHelp(help, {
outcome: "completed",
completed_by: "system",
tab_id: help.subjectTabId,
tab_id: help.primaryTabId,
resolved_targets: help.resolvedTargets,
});
};
@@ -697,7 +704,7 @@ export async function handleRequestHelp(
const timeoutTimer = setTimeout(() => {
finishHelp(help, {
outcome: "timed_out",
tab_id: help.subjectTabId,
tab_id: help.primaryTabId,
resolved_targets: help.resolvedTargets,
});
}, timeoutMs);
@@ -705,7 +712,6 @@ export async function handleRequestHelp(
const help: ActiveHelpRequest = {
ctx,
requestId,
subjectTabId: tabId,
primaryTabId: tabId,
overlayTabIds: new Set(),
prompt: params.prompt,
@@ -741,7 +747,7 @@ export async function handleRequestHelp(
finishHelp(help, {
outcome: legacyFinish.outcome,
...(legacyFinish.note ? { note: legacyFinish.note } : {}),
tab_id: help.subjectTabId,
tab_id: help.primaryTabId,
resolved_targets: help.resolvedTargets,
});
})
+1 -1
View File
@@ -77,7 +77,7 @@ pub fn dispatch(args: RequestHelpArgs, format: Format) -> Result<(), CliError> {
let completion_criteria = args
.completion_criteria
.as_deref()
.map(|raw| serde_json::from_str::<HelpCompletionCriteria>(raw))
.map(serde_json::from_str::<HelpCompletionCriteria>)
.transpose()
.context("parse --completion-criteria JSON")?;
let params = RequestHelpParams {
@@ -74,7 +74,7 @@
},
"helpRequest": {
"title": "Agent needs your help",
"compactStatus": "Agent is still waiting for you to finish logging in",
"compactStatus": "Agent is still waiting for you to finish this step",
"noteLabel": "Optional note back to the agent",
"notePlaceholder": "Add a note for the agent (optional)",
"continue": "Done, return control",
@@ -74,7 +74,7 @@
},
"helpRequest": {
"title": "Agent 需要你的帮助",
"compactStatus": "当前 Agent 仍在等待你完成登录",
"compactStatus": "当前 Agent 仍在等待你完成此步骤",
"noteLabel": "给 Agent 的备注(可选)",
"notePlaceholder": "给 Agent 留言(可选)",
"continue": "完成并交还控制权",