Skip to content

Commit f57fcbf

Browse files
shnpdcursoragent
andcommitted
fix(record): record switch_tab and keep Trace v3 states per tab
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a7c0737 commit f57fcbf

10 files changed

Lines changed: 672 additions & 32 deletions

File tree

apps/extension/src/lib/__tests__/record-observation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ describe("applyTargetMatching while the previous action is still settling", () =
130130
url: URL,
131131
vomText: '@vom 1\n@e1 textbox "Search"',
132132
};
133-
obs.settles.set(0, { draftIndex: 0, cancelled: false });
133+
obs.settles.set(0, { draftIndex: 0, tabId: 4, cancelled: false });
134134
const draft: DraftTraceStep = {
135135
op: "click",
136136
target: { unmatched: true },

apps/extension/src/lib/__tests__/trace-reducer-v2.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@ describe("trace-reducer-v2", () => {
4141
expect(trace.steps).toHaveLength(0);
4242
});
4343

44+
it("drops v3-only tab switch steps", () => {
45+
const trace = buildTraceV2({
46+
steps: [
47+
{
48+
op: "switch_tab",
49+
preStateId: "s1",
50+
postStateId: "s2",
51+
page_url: "https://example.com/second",
52+
},
53+
],
54+
startedAt: "2026-01-01T00:00:00.000Z",
55+
startUrl: "https://example.com/",
56+
});
57+
58+
expect(trace.steps).toHaveLength(0);
59+
});
60+
4461
it("maps captureTarget to v2 target with required tag", () => {
4562
const steps: DraftTraceStep[] = [
4663
{

apps/extension/src/lib/record-observation.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface LastSettledObservation {
2626
/** A post-action observation that has been queued but not yet written down. */
2727
interface PendingSettle {
2828
draftIndex: number;
29+
tabId: number;
2930
/** Set when a newer action has already decided where this step landed. */
3031
cancelled: boolean;
3132
}
@@ -40,6 +41,7 @@ export interface PendingRedirectLanding {
4041
export interface RecordingObservationState {
4142
stateRegistry: Map<string, StateRegistryEntry>;
4243
lastSettled: LastSettledObservation | null;
44+
settledByTab: Map<number, LastSettledObservation>;
4345
maxPageTokens: number;
4446
redactValues: boolean;
4547
lastCaptureAtMs: number;
@@ -67,6 +69,7 @@ export function createObservationState(options?: {
6769
return {
6870
stateRegistry: new Map(),
6971
lastSettled: null,
72+
settledByTab: new Map(),
7073
maxPageTokens: options?.maxPageTokens ?? DEFAULT_MAX_PAGE_TOKENS,
7174
redactValues: options?.redactValues ?? false,
7275
lastCaptureAtMs: 0,
@@ -134,6 +137,7 @@ export async function captureAndRegisterObservation(
134137
vomText: rendered.text,
135138
};
136139
obs.lastSettled = settled;
140+
obs.settledByTab.set(tabId, settled);
137141
return settled;
138142
}
139143

@@ -499,11 +503,13 @@ export async function settleUnsettledDrafts(
499503
function supersedeEarlierSettles(
500504
obs: RecordingObservationState,
501505
draftIndex: number,
506+
tabId: number,
502507
steps: DraftTraceStep[],
503508
): void {
504509
const landing = steps[draftIndex]?.preStateId;
505510
for (const [index, pending] of obs.settles) {
506511
if (index >= draftIndex) continue;
512+
if (pending.tabId !== tabId) continue;
507513
pending.cancelled = true;
508514
obs.settles.delete(index);
509515

@@ -527,14 +533,14 @@ export function scheduleDraftSettle(
527533
tabsApi: ChromeTabsApi,
528534
steps: DraftTraceStep[],
529535
): void {
530-
supersedeEarlierSettles(obs, draftIndex, steps);
536+
supersedeEarlierSettles(obs, draftIndex, tabId, steps);
531537

532538
// Rescheduling the same step (a navigation showed up after the action)
533539
// replaces the pending observation rather than racing it.
534540
const superseded = obs.settles.get(draftIndex);
535541
if (superseded) superseded.cancelled = true;
536542

537-
const pending: PendingSettle = { draftIndex, cancelled: false };
543+
const pending: PendingSettle = { draftIndex, tabId, cancelled: false };
538544
obs.settles.set(draftIndex, pending);
539545

540546
enqueueSettle(obs, async () => {

apps/extension/src/lib/trace-reducer-v2.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function shouldIncludeDraft(step: DraftTraceStep): boolean {
8585
// `trace_version` omitted means a pre-v3 client. Released v2 readers
8686
// (including cli-v0.1.10) do not have hover in their tagged Step enum,
8787
// so emitting it makes the entire trace fail to deserialize.
88-
if (step.op === "scroll" || step.op === "hover") return false;
88+
if (step.op === "scroll" || step.op === "hover" || step.op === "switch_tab") return false;
8989
if (step.op === "fill" && !(step.value ?? "").trim() && !step.redacted) return false;
9090
if (step.op === "press" && !shouldRecordPress(step.key, step.modifiers)) return false;
9191
return true;
@@ -260,6 +260,7 @@ function toV2Step(
260260
};
261261
}
262262
case "scroll":
263+
case "switch_tab":
263264
return null;
264265
}
265266
}

apps/extension/src/lib/trace-reducer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ function toV3Step(draft: DraftTraceStep, id: number): StepV3 | null {
152152
to: draft.url,
153153
cause: navigateCauseForDraft(draft),
154154
};
155+
case "switch_tab":
156+
return { op: "switch_tab", ...common };
155157
case "click":
156158
return { op: "click", ...common, target: draft.target };
157159
case "hover":

0 commit comments

Comments
 (0)