Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions apps/extension/src/lib/trace-reducer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DraftTraceStep, PageRef, SelectedOption, Step } from "@/transport/types";
import type { DraftTraceStep, PageRefV2, SelectedOptionV2, StepV2 } from "@/transport/types";

const CLIPBOARD_KEYS = new Set(["a", "c", "v", "x", "A", "C", "V", "X"]);
const MODIFIER_ONLY_KEYS = new Set(["Meta", "Control", "Alt", "Shift", "OS", "Hyper", "Super"]);
Expand Down Expand Up @@ -60,7 +60,7 @@ function collectUrls(steps: DraftTraceStep[], startUrl?: string): string[] {
function buildPageRegistry(
steps: DraftTraceStep[],
startUrl?: string,
): { pages: PageRef[]; urlToId: Map<string, string> } {
): { pages: PageRefV2[]; urlToId: Map<string, string> } {
const urls = collectUrls(steps, startUrl);
const urlToId = new Map<string, string>();
const pages = urls.map((url, index) => {
Expand Down Expand Up @@ -90,19 +90,19 @@ function pageUrlForDraft(step: DraftTraceStep, fallbackUrl?: string): string | u
function effectForNavigation(
navigatedTo: string | undefined,
urlToId: Map<string, string>,
): Step["effect"] {
): StepV2["effect"] {
if (!navigatedTo) return undefined;
const pageId = urlToId.get(navigatedTo);
if (!pageId) return undefined;
return { navigated_to: pageId };
}

function withEffect(step: Step, effect: Step["effect"]): Step {
function withEffect(step: StepV2, effect: StepV2["effect"]): StepV2 {
if (!effect) return step;
return { ...step, effect };
}

function toSelection(values: string[], labels?: string[]): SelectedOption[] {
function toSelection(values: string[], labels?: string[]): SelectedOptionV2[] {
return values.map((value, index) => ({
value,
...(labels?.[index] ? { label: labels[index] } : {}),
Expand All @@ -114,7 +114,7 @@ function toV2Step(
id: number,
urlToId: Map<string, string>,
fallbackUrl?: string,
): Step | null {
): StepV2 | null {
if (!shouldIncludeDraft(step)) return null;

const pageUrl = pageUrlForDraft(step, fallbackUrl);
Expand Down Expand Up @@ -181,8 +181,8 @@ function toV2Step(
}

export interface ReducedTrace {
pages: PageRef[];
steps: Step[];
pages: PageRefV2[];
steps: StepV2[];
}

/**
Expand All @@ -192,7 +192,7 @@ export interface ReducedTrace {
export function reduceTraceSteps(steps: DraftTraceStep[], startUrl?: string): ReducedTrace {
const collapsed = collapseNavigations(steps);
const { pages, urlToId } = buildPageRegistry(collapsed, startUrl);
const out: Step[] = [];
const out: StepV2[] = [];
let id = 1;
let lastUrl = startUrl;
for (const draft of collapsed) {
Expand All @@ -210,7 +210,7 @@ export function reduceTraceSteps(steps: DraftTraceStep[], startUrl?: string): Re
export function resolveTraceStartUrl(
drafts: DraftTraceStep[],
startUrl?: string,
pages?: PageRef[],
pages?: PageRefV2[],
): string {
if (startUrl) return startUrl;
const navigate = drafts.find((step): step is Extract<DraftTraceStep, { op: "navigate" }> => {
Expand Down
18 changes: 9 additions & 9 deletions apps/extension/src/tools/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import type {
RecordStopParams,
RecordStopResult,
RpcError,
Trace,
TraceV2,
} from "@/transport/types";
import { handleNavigate } from "./navigation";
import {
Expand All @@ -50,8 +50,8 @@ interface ActiveRecording {
steps: DraftTraceStep[];
startedAt: string;
startedAtMs: number;
finishPromise: Promise<Trace>;
resolveFinish: (trace: Trace) => void;
finishPromise: Promise<TraceV2>;
resolveFinish: (trace: TraceV2) => void;
rejectFinish: (err: Error) => void;
settled: boolean;
finishing: boolean;
Expand Down Expand Up @@ -154,7 +154,7 @@ async function sendRecordStartWithAck(
throw lastError ?? new Error("failed to start recording in content script");
}

function buildTrace(recording: ActiveRecording): Trace {
function buildTrace(recording: ActiveRecording): TraceV2 {
const { pages, steps } = reduceTraceSteps(recording.steps, recording.startUrl);
const startUrl = resolveTraceStartUrl(recording.steps, recording.startUrl, pages);
return {
Expand Down Expand Up @@ -532,7 +532,7 @@ async function finishRecordingByRequest(
}
}

async function finishRecording(sessionId: string, deps: RecordDeps): Promise<Trace | null> {
async function finishRecording(sessionId: string, deps: RecordDeps): Promise<TraceV2 | null> {
const recording = recordings.get(sessionId);
if (!recording || recording.settled || recording.finishing) return null;
recording.finishing = true;
Expand Down Expand Up @@ -574,9 +574,9 @@ export async function handleRecordStart(
// on the destination page can RECORD_QUERY → rearm → show RecordOverlay
// instead of flashing ControlOverlay ("Agent 正在控制").
const requestId = makeRequestId(target.tabId);
let resolveFinish!: (trace: Trace) => void;
let resolveFinish!: (trace: TraceV2) => void;
let rejectFinish!: (err: Error) => void;
const finishPromise = new Promise<Trace>((resolve, reject) => {
const finishPromise = new Promise<TraceV2>((resolve, reject) => {
resolveFinish = resolve;
rejectFinish = reject;
});
Expand Down Expand Up @@ -785,9 +785,9 @@ export async function handleRecordAwait(
return { code: "cancelled", message: "record_await aborted" };
}

const outcome = await new Promise<{ trace: Trace } | { error: RpcError }>((resolve) => {
const outcome = await new Promise<{ trace: TraceV2 } | { error: RpcError }>((resolve) => {
let settled = false;
const finish = (result: { trace: Trace } | { error: RpcError }) => {
const finish = (result: { trace: TraceV2 } | { error: RpcError }) => {
if (settled) return;
settled = true;
if (timer) clearTimeout(timer);
Expand Down
146 changes: 118 additions & 28 deletions apps/extension/src/transport/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,10 +657,60 @@ export interface EmulateResult {
}

// --------------------------------------------------------------------------
// Semantic record payloads mirror bsk-protocol record.rs (Trace v2)
// Semantic record payloads mirror the versioned Rust protocol models.
// --------------------------------------------------------------------------
Comment thread
shnpd marked this conversation as resolved.

export interface TargetDescriptor {
export const TRACE_VERSION_V3 = 3;
export const TRACE_VERSION_V2 = 2;
export const DEFAULT_TRACE_VERSION = 2;
export const VOM_FORMAT_VERSION = 1;

export interface TargetDescriptorV3 {
ref?: string;
role?: string;
name?: string;
ctx?: string;
unmatched?: boolean;
}

export interface RecorderInfo {
bsk: string;
vom: number;
}

export type StopReason = "user_finish" | "cli_stop";

export interface TraceStateV3 {
id: string;
url: string;
title?: string;
body: string;
truncated?: boolean;
}

export interface StepResultV3 {
state: string;
}

export interface StepCommonV3 {
id: number;
state: string;
result: StepResultV3;
}

export type NavigationCause =
| "user_typed"
| "link"
| "form_submit"
| "reload"
| "history"
| "script"
| "browser";

export type FillCommit = "enter" | "suggestion" | "blur";

/** Legacy v2 target shape retained for existing record producers. */
export interface TargetDescriptorV2 {
role?: string;
name?: string;
tag: string;
Expand All @@ -673,58 +723,58 @@ export interface TraceEntry {
start_url: string;
}

export interface PageRef {
export interface PageRefV2 {
id: string;
url: string;
title?: string;
}

export interface SelectedOption {
export interface SelectedOptionV2 {
value: string;
label?: string;
}

export interface StepEffect {
export interface StepEffectV2 {
navigated_to: string;
}

export interface StepCommon {
export interface StepCommonV2 {
id: number;
page: string;
effect?: StepEffect;
effect?: StepEffectV2;
}

/** Capture/buffer draft before v2 reduction. */
export type DraftTraceStep =
| {
op: "click";
target: TargetDescriptor;
target: TargetDescriptorV2;
navigated_to?: string;
page_url?: string;
}
| {
op: "hover";
target: TargetDescriptor;
target: TargetDescriptorV2;
page_url?: string;
}
| {
op: "fill";
target: TargetDescriptor;
target: TargetDescriptorV2;
value: string;
redacted?: boolean;
page_url?: string;
}
| {
op: "press";
key: string;
target?: TargetDescriptor;
target?: TargetDescriptorV2;
modifiers?: KeyModifier[];
navigated_to?: string;
page_url?: string;
}
| {
op: "select";
target: TargetDescriptor;
target: TargetDescriptorV2;
values: string[];
labels?: string[];
navigated_to?: string;
Expand All @@ -737,34 +787,74 @@ export type DraftTraceStep =
};

/** Exported record-only step (trace v2). */
export type Step =
| ({ op: "navigate" } & StepCommon & { to: string })
| ({ op: "click" } & StepCommon & { target: TargetDescriptor })
| ({ op: "hover" } & StepCommon & { target: TargetDescriptor })
| ({ op: "fill" } & StepCommon & {
target: TargetDescriptor;
export type StepV2 =
| ({ op: "navigate" } & StepCommonV2 & { to: string })
| ({ op: "click" } & StepCommonV2 & { target: TargetDescriptorV2 })
| ({ op: "hover" } & StepCommonV2 & { target: TargetDescriptorV2 })
| ({ op: "fill" } & StepCommonV2 & {
target: TargetDescriptorV2;
value: string;
redacted?: boolean;
})
| ({ op: "select" } & StepCommon & {
target: TargetDescriptor;
selection: SelectedOption[];
| ({ op: "select" } & StepCommonV2 & {
target: TargetDescriptorV2;
selection: SelectedOptionV2[];
})
| ({ op: "press" } & StepCommon & {
| ({ op: "press" } & StepCommonV2 & {
key: string;
modifiers?: KeyModifier[];
target?: TargetDescriptor;
target?: TargetDescriptorV2;
});

export interface Trace {
export interface TraceV2 {
recorded_at: string;
started_at?: string;
purpose?: string;
entry: TraceEntry;
pages: PageRefV2[];
steps: StepV2[];
}

export interface SelectedOptionV3 {
value: string;
label?: string;
}

export type StepV3 =
| ({ op: "navigate" } & StepCommonV3 & { to: string; cause: NavigationCause })
| ({ op: "click" } & StepCommonV3 & { target: TargetDescriptorV3 })
| ({ op: "hover" } & StepCommonV3 & { target: TargetDescriptorV3 })
| ({ op: "fill" } & StepCommonV3 & {
target: TargetDescriptorV3;
value: string;
commit: FillCommit;
redacted?: boolean;
})
| ({ op: "select" } & StepCommonV3 & {
target: TargetDescriptorV3;
selection?: SelectedOptionV3[];
})
| ({ op: "press" } & StepCommonV3 & {
key: string;
modifiers?: KeyModifier[];
target?: TargetDescriptorV3;
})
| ({ op: "scroll" } & StepCommonV3);

export interface TraceV3 {
version: typeof TRACE_VERSION_V3;
recorded_at: string;
started_at?: string;
purpose?: string;
stopped_by: StopReason;
entry: TraceEntry;
pages: PageRef[];
steps: Step[];
recorder: RecorderInfo;
states: TraceStateV3[];
steps: StepV3[];
}

export type RecordedTrace = TraceV2 | TraceV3;

export interface RecordStartParams {
session_id: string;
tab_id?: number;
Expand All @@ -782,7 +872,7 @@ export interface RecordStopParams {
}

export interface RecordStopResult {
trace: Trace;
trace: RecordedTrace;
}

export interface RecordAwaitParams {
Expand All @@ -791,5 +881,5 @@ export interface RecordAwaitParams {
}

export interface RecordAwaitResult {
trace: Trace;
trace: RecordedTrace;
}
Loading
Loading