feat(cli): add browse macro record and replay commands#2257
feat(cli): add browse macro record and replay commands#2257optimusbuilder wants to merge 2 commits into
Conversation
Let users capture successful driver commands into reusable macros and replay them in a session. Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: 6aa7337 The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
This PR is from an external contributor and must be approved by a stagehand team member with write access before CI can run. |
There was a problem hiding this comment.
3 issues found across 14 files
Confidence score: 3/5
- In
packages/cli/src/lib/driver/command-cli.ts, macro recording can throw beforeoutputJsonruns, which can turn a successful driver operation into a reported CLI failure and hide valid output from users/automation. Make recording persistence best-effort (or move it after result emission) so filesystem errors don’t mask command success before merging. - In
packages/cli/src/commands/macro/delete.ts, throwingnew Error()bypasses the typedCommandFailureflow and can skip centralized handling inBrowseCommand.catch, leading to inconsistent error formatting/exit behavior. Switch tofail()/CommandFailureso failures are normalized by the shared boundary. - In
packages/cli/src/commands/macro/delete.ts, the documented--forcebehavior and examples don’t match implementation, sobrowse macro delete <name>currently fails where docs imply it should proceed/confirm. Align the command logic or update help/examples before merge to avoid immediate user confusion and failed workflows.
Architecture diagram
sequenceDiagram
participant CLI as browse CLI
participant MacroCmd as macro subcommand
participant Recording as recording.ts
participant Store as store.ts
participant Replay as replay.ts
participant Driver as command-cli.ts
participant Runtime as driver/runtime.ts
participant Session as Active Session
Note over CLI,Session: Record Flow
CLI->>MacroCmd: macro record <name>
MacroCmd->>Recording: startMacroRecording(name)
Recording->>Store: readRecordingState()
Store-->>Recording: null (no active recording)
Recording->>Store: writeRecordingState({name, steps:[]})
Recording-->>MacroCmd: started
MacroCmd-->>CLI: {recording: true}
CLI->>Driver: run driver command (e.g., open, click)
Driver->>Runtime: runDriverCommandWithTarget()
Runtime-->>Driver: result
Driver->>Recording: appendMacroStepIfRecording(command, params)
alt command is snapshot/refs/cursor/tab.list
Recording-->>Driver: skip (no append)
else command is recordable
Recording->>Store: readRecordingState()
Store-->>Recording: active state
Recording->>Recording: push step {command, params}
Recording->>Store: writeRecordingState(updated)
end
Driver-->>CLI: output result
CLI->>MacroCmd: macro stop
MacroCmd->>Recording: stopMacroRecording()
Recording->>Store: readRecordingState()
Store-->>Recording: active state
Recording->>Recording: build BrowseMacro from state
Recording->>Store: saveMacro(macro) -> ~/.browse/macros/<name>.json
Recording->>Store: clearRecordingState()
Recording-->>MacroCmd: {name, steps}
MacroCmd-->>CLI: {message: "Saved..."}
Note over CLI,Session: Replay Flow
CLI->>MacroCmd: macro run <name> [--delay N]
MacroCmd->>Replay: replayMacro({name, session, target, delayMs})
Replay->>Store: loadMacro(name)
Store-->>Replay: BrowseMacro with steps array
loop each step in macro.steps
Replay->>Runtime: runDriverCommandWithTarget(session, target, command, params)
Runtime-->>Replay: result
Replay->>Replay: push result to results array
opt delayMs > 0
Replay->>Replay: sleep(delayMs)
end
end
Replay-->>MacroCmd: {macro, results}
MacroCmd-->>CLI: {name, results, steps}
Note over CLI,Session: List/Show/Delete Flow
CLI->>MacroCmd: macro list
MacroCmd->>Store: listMacroNames() -> scan ~/.browse/macros/*.json
Store-->>MacroCmd: sorted macro names
MacroCmd->>Recording: getActiveRecordingName()
Recording-->>MacroCmd: active name or null
MacroCmd-->>CLI: {macros, recording}
CLI->>MacroCmd: macro show <name>
MacroCmd->>Store: loadMacro(name) -> read JSON file
Store-->>MacroCmd: BrowseMacro object
MacroCmd-->>CLI: output macro JSON
CLI->>MacroCmd: macro delete <name> --force
MacroCmd->>Store: macroFilePath(name)
MacroCmd->>MacroCmd: fs.unlink(file)
MacroCmd-->>CLI: {deleted: true, name}
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Emit driver results before best-effort recording, remove misleading --force on delete, and use fail() for not-found errors. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
1 issue found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/cli/src/lib/macro/recording.ts">
<violation number="1" location="packages/cli/src/lib/macro/recording.ts:92">
P2: Silent suppression of macro recording errors can hide dropped steps and produce a misleadingly successful recording UX.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| } catch { | ||
| // Best-effort recording must not mask successful driver commands. | ||
| } |
There was a problem hiding this comment.
P2: Silent suppression of macro recording errors can hide dropped steps and produce a misleadingly successful recording UX.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/lib/macro/recording.ts, line 92:
<comment>Silent suppression of macro recording errors can hide dropped steps and produce a misleadingly successful recording UX.</comment>
<file context>
@@ -82,3 +82,14 @@ export async function getActiveRecordingName(): Promise<string | null> {
+): Promise<void> {
+ try {
+ await appendMacroStepIfRecording(command, params);
+ } catch {
+ // Best-effort recording must not mask successful driver commands.
+ }
</file context>
| } catch { | |
| // Best-effort recording must not mask successful driver commands. | |
| } | |
| } catch (error) { | |
| // Best-effort recording must not mask successful driver commands. | |
| // Surface the failure so users can diagnose missing macro steps. | |
| console.error("Failed to record macro step:", error); | |
| } |
why
Users running repeatable browse workflows need a lightweight way to capture and replay driver command sequences without scripting each step manually.
what changed
browse macrocommands:record,stop,run,list,show, anddeleterunDriverCommandFromFlagspath after execution~/.browse/macros/<name>.json(override withBROWSE_MACRO_DIR)--delaybetween stepssnapshot,refs,cursor,tab.list) from recordingstest plan
pnpm --filter browse buildpnpm --filter browse exec vitest run tests/macro.test.tsbrowse macro record login-flowopen,click,fill)browse macro stopandbrowse macro show login-flowbrowse macro run login-flowin an active sessionMade with Cursor
Summary by cubic
Adds
browse macrocommands to record and replay driver command sequences so users can save repeatable flows and run them later in an active session. Macros are stored on disk and can be listed, inspected, and deleted.New Features
browse macrosubcommands:record,stop,run,list,show,delete; macros persist to~/.browse/macros/<name>.json(override withBROWSE_MACRO_DIR) and can replay with--delay.runDriverCommandFromFlagspath, skipping inspection-only commands (snapshot,refs,cursor,tab.list); includes unit tests.Bug Fixes
macro deletenow fails cleanly when a name is not found (no--force).Written for commit 6aa7337. Summary will update on new commits.