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