mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
fix(extension): keep record overlay timer across navigations
This commit is contained in:
@@ -10,6 +10,12 @@ import recordIconUrl from "../../assets/record-icon.png";
|
||||
|
||||
export interface RecordRequestData {
|
||||
id: string;
|
||||
/**
|
||||
* Epoch ms when the recording began. Supplied by the background so the timer
|
||||
* keeps counting the whole session across navigations; falls back to mount
|
||||
* time when the background could not report it.
|
||||
*/
|
||||
startedAtMs?: number;
|
||||
onFinish: () => void;
|
||||
}
|
||||
|
||||
@@ -71,20 +77,25 @@ export function RecordOverlay({ request }: Props) {
|
||||
setShow(false);
|
||||
}, [request]);
|
||||
|
||||
const requestStartedAtMs = request?.startedAtMs;
|
||||
|
||||
useEffect(() => {
|
||||
setElapsedSeconds(0);
|
||||
setDragPos(null);
|
||||
setDragging(false);
|
||||
dragStartRef.current = null;
|
||||
if (!request) return;
|
||||
if (!request) {
|
||||
setElapsedSeconds(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
const startedAt = requestStartedAtMs ?? Date.now();
|
||||
const updateElapsed = () => {
|
||||
setElapsedSeconds(Math.floor((Date.now() - startedAt) / 1_000));
|
||||
setElapsedSeconds(Math.max(0, Math.floor((Date.now() - startedAt) / 1_000)));
|
||||
};
|
||||
updateElapsed();
|
||||
const timer = window.setInterval(updateElapsed, TIMER_REFRESH_MS);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [request?.id]);
|
||||
}, [request?.id, requestStartedAtMs]);
|
||||
|
||||
const onPillPointerDown = useCallback((e: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (e.button !== 0) return;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createElement } from "react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { RecordOverlay } from "../RecordOverlay";
|
||||
|
||||
function renderOverlay(request: { id: string; onFinish: () => void } | null) {
|
||||
function renderOverlay(request: { id: string; startedAtMs?: number; onFinish: () => void } | null) {
|
||||
return render(
|
||||
createElement(I18nextProvider, { i18n }, createElement(RecordOverlay, { request })),
|
||||
);
|
||||
@@ -129,6 +129,20 @@ describe("RecordOverlay", () => {
|
||||
expect(label.textContent).toBe("00:10");
|
||||
});
|
||||
|
||||
it("continues the session timer when the overlay remounts on a new page", () => {
|
||||
vi.useFakeTimers();
|
||||
const startedAtMs = Date.now() - 65_000;
|
||||
const { container } = renderOverlay({ id: "rec-1", startedAtMs, onFinish: vi.fn() });
|
||||
|
||||
const pill = container.querySelector("[data-slot='record-overlay-pill']") as HTMLElement;
|
||||
const label = container.querySelector("[data-slot='record-overlay-label']") as HTMLElement;
|
||||
expect(pill.dataset.phase).toBe("timer");
|
||||
expect(label.textContent).toBe("01:05");
|
||||
|
||||
advanceRecording(5_000);
|
||||
expect(label.textContent).toBe("01:10");
|
||||
});
|
||||
|
||||
it("does not start drag when pointer down on finish", () => {
|
||||
const { container } = renderOverlay({ id: "rec-1", onFinish: vi.fn() });
|
||||
const pill = container.querySelector("[data-slot='record-overlay-pill']") as HTMLElement;
|
||||
|
||||
@@ -468,7 +468,7 @@ export function handleRecordContentMessage(
|
||||
capture: RecordCaptureController | null;
|
||||
setActiveRequestId(id: string | null): void;
|
||||
setCapture(capture: RecordCaptureController | null): void;
|
||||
onStart(requestId: string): void;
|
||||
onStart(requestId: string, startedAtMs?: number): void;
|
||||
onStop(): void;
|
||||
},
|
||||
sendResponse?: (response: RecordStartAck | RecordStopAck) => void,
|
||||
@@ -485,7 +485,7 @@ export function handleRecordContentMessage(
|
||||
}),
|
||||
);
|
||||
state.setActiveRequestId(message.requestId);
|
||||
state.onStart(message.requestId);
|
||||
state.onStart(message.requestId, message.startedAtMs);
|
||||
sendResponse?.({ ok: true });
|
||||
return sendResponse !== undefined;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import { sendInterrupt } from "@/lib/overlay-interrupt-client";
|
||||
import {
|
||||
RECORD_FINISH,
|
||||
RECORD_QUERY,
|
||||
type RecordQueryResponse,
|
||||
type RecordStartAck,
|
||||
type RecordStopAck,
|
||||
} from "@/lib/record-bridge";
|
||||
@@ -255,9 +256,10 @@ export default defineContentScript({
|
||||
setCapture: (capture) => {
|
||||
recordCapture = capture;
|
||||
},
|
||||
onStart: (requestId) => {
|
||||
onStart: (requestId, startedAtMs) => {
|
||||
overlays.setAgentRecordRequest({
|
||||
id: requestId,
|
||||
...(typeof startedAtMs === "number" ? { startedAtMs } : {}),
|
||||
onFinish: () => {
|
||||
void chrome.runtime.sendMessage({
|
||||
type: RECORD_FINISH,
|
||||
@@ -415,15 +417,17 @@ export default defineContentScript({
|
||||
try {
|
||||
const recordQuery = (await chrome.runtime.sendMessage({
|
||||
type: RECORD_QUERY,
|
||||
})) as { active?: boolean; requestId?: string } | undefined;
|
||||
})) as RecordQueryResponse | undefined;
|
||||
if (
|
||||
recordQuery?.active &&
|
||||
typeof recordQuery.requestId === "string" &&
|
||||
overlays.snapshot().activeRecord === null
|
||||
) {
|
||||
const requestId = recordQuery.requestId;
|
||||
const startedAtMs = recordQuery.startedAtMs;
|
||||
overlays.setAgentRecordRequest({
|
||||
id: requestId,
|
||||
...(typeof startedAtMs === "number" ? { startedAtMs } : {}),
|
||||
onFinish: () => {
|
||||
void chrome.runtime.sendMessage({
|
||||
type: RECORD_FINISH,
|
||||
|
||||
@@ -25,11 +25,19 @@ export interface RecordQueryMessage {
|
||||
export interface RecordQueryResponse {
|
||||
active: boolean;
|
||||
requestId?: string;
|
||||
/** Epoch ms when the recording began; see `RecordStartMessage.startedAtMs`. */
|
||||
startedAtMs?: number;
|
||||
}
|
||||
|
||||
export interface RecordStartMessage {
|
||||
type: typeof RECORD_START;
|
||||
requestId: string;
|
||||
/**
|
||||
* Epoch ms when the whole recording began, not when this tab was armed.
|
||||
* The overlay timer must span the session, so it survives navigations and
|
||||
* content-script remounts instead of restarting per page.
|
||||
*/
|
||||
startedAtMs?: number;
|
||||
}
|
||||
|
||||
export interface RecordStepPayload {
|
||||
|
||||
@@ -49,6 +49,7 @@ interface ActiveRecording {
|
||||
purpose?: string;
|
||||
steps: DraftTraceStep[];
|
||||
startedAt: string;
|
||||
startedAtMs: number;
|
||||
finishPromise: Promise<Trace>;
|
||||
resolveFinish: (trace: Trace) => void;
|
||||
rejectFinish: (err: Error) => void;
|
||||
@@ -373,7 +374,11 @@ async function rearmRecording(
|
||||
// stuck with pointer-events:none (page usable, Interrupt dead). RecordOverlay
|
||||
// already hides the control chrome while activeRecord is set.
|
||||
for (let attempt = 0; attempt < RECORD_REARM_MAX_ATTEMPTS; attempt += 1) {
|
||||
const startMsg: RecordStartMessage = { type: RECORD_START, requestId: recording.requestId };
|
||||
const startMsg: RecordStartMessage = {
|
||||
type: RECORD_START,
|
||||
requestId: recording.requestId,
|
||||
startedAtMs: recording.startedAtMs,
|
||||
};
|
||||
try {
|
||||
await sendRecordStartWithAck(targetTabId, startMsg, deps.sendToTab);
|
||||
recording.tabId = targetTabId;
|
||||
@@ -501,7 +506,11 @@ export function attachRecordQueryListener(deps: RecordDeps = getDefaultDeps()):
|
||||
return;
|
||||
}
|
||||
await rearmRecording(recording, tabId, deps);
|
||||
sendResponse({ active: true, requestId: recording.requestId });
|
||||
sendResponse({
|
||||
active: true,
|
||||
requestId: recording.requestId,
|
||||
startedAtMs: recording.startedAtMs,
|
||||
});
|
||||
})();
|
||||
return true;
|
||||
};
|
||||
@@ -572,6 +581,7 @@ export async function handleRecordStart(
|
||||
rejectFinish = reject;
|
||||
});
|
||||
const navigateUrl = params.url ?? RECORD_DEFAULT_START_URL;
|
||||
const startedAtMs = Date.now();
|
||||
recordings.set(params.session_id, {
|
||||
requestId,
|
||||
tabId: target.tabId,
|
||||
@@ -579,7 +589,8 @@ export async function handleRecordStart(
|
||||
startUrl: navigateUrl,
|
||||
...(params.purpose ? { purpose: params.purpose } : {}),
|
||||
steps: [],
|
||||
startedAt: new Date().toISOString(),
|
||||
startedAt: new Date(startedAtMs).toISOString(),
|
||||
startedAtMs,
|
||||
finishPromise,
|
||||
resolveFinish,
|
||||
rejectFinish,
|
||||
@@ -707,7 +718,7 @@ export async function handleRecordStart(
|
||||
if (cancelled) return cancelled;
|
||||
}
|
||||
|
||||
const startMsg: RecordStartMessage = { type: RECORD_START, requestId };
|
||||
const startMsg: RecordStartMessage = { type: RECORD_START, requestId, startedAtMs };
|
||||
|
||||
try {
|
||||
await sendRecordStartWithAck(target.tabId, startMsg, deps.sendToTab);
|
||||
|
||||
Reference in New Issue
Block a user