opencode-drive 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,9 @@ If you installed the skill file, OpenCode will be able to see and interact with
22
22
 
23
23
  ## Using with agents
24
24
 
25
- Install the skill file above and ask the agent to test various flows with the app. You also ask it to make a recording and it will generate a video file for you to watch anything it did.
25
+ Install the skill file above and ask the agent to test various flows with the app. Start with `--record` when you want a video; `opencode-drive stop` then exports the complete session and prints its path.
26
+
27
+ Screenshots and videos are written to `<system temp>/opencode-drive/output` with unique filenames. Set `OPENCODE_DRIVE_MEDIA_DIR` to use a different directory.
26
28
 
27
29
  ## UI development
28
30
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-drive",
4
- "version": "0.1.7",
4
+ "version": "0.1.9",
5
5
  "description": "Drive real and simulated OpenCode instances",
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -12,26 +12,32 @@
12
12
  "exports": {
13
13
  ".": "./src/index.ts",
14
14
  "./client": "./src/client/index.ts",
15
- "./experimental": "./src/experimental/index.ts"
15
+ "./experimental": "./src/experimental/index.ts",
16
+ "./recording": "./src/recording/index.ts"
16
17
  },
17
18
  "files": [
18
19
  "bin/opencode-drive",
19
20
  "src/cli",
20
21
  "src/client",
21
22
  "src/experimental",
23
+ "src/recording",
22
24
  "src/index.ts",
23
25
  "README.md"
24
26
  ],
25
27
  "scripts": {
26
28
  "drive": "bun run src/cli/index.ts",
27
29
  "lint": "oxlint src",
28
- "test": "bun test test/cli/parse.test.ts test/cli/integration.test.ts",
30
+ "test": "bun test test",
29
31
  "typecheck": "tsgo --noEmit",
30
32
  "check": "bun run lint && bun run typecheck"
31
33
  },
32
34
  "dependencies": {
33
35
  "@effect/platform-node": "4.0.0-beta.93",
34
36
  "@effect/platform-node-shared": "4.0.0-beta.93",
37
+ "@fontsource/adwaita-mono": "5.2.1",
38
+ "@napi-rs/canvas": "1.0.2",
39
+ "@wterm/core": "0.3.0",
40
+ "@wterm/ghostty": "0.3.0",
35
41
  "effect": "4.0.0-beta.93"
36
42
  },
37
43
  "devDependencies": {
@@ -22,10 +22,9 @@ export const commandInfo = {
22
22
  value: false,
23
23
  description: "Return focus, elements, and available UI actions",
24
24
  },
25
- "ui.start-record": { value: false, description: "Start recording the UI" },
26
- "ui.end-record": {
25
+ "ui.recording.finish": {
27
26
  value: false,
28
- description: "Stop recording and return the recording path",
27
+ description: "Finish recording and return the timeline path",
29
28
  },
30
29
  } as const
31
30
 
@@ -38,8 +37,7 @@ export function commandAcceptsValue(operation: string) {
38
37
  if (operation === "ui.click") return commandInfo[operation].value
39
38
  if (operation === "ui.screenshot") return commandInfo[operation].value
40
39
  if (operation === "ui.state") return commandInfo[operation].value
41
- if (operation === "ui.start-record") return commandInfo[operation].value
42
- if (operation === "ui.end-record") return commandInfo[operation].value
40
+ if (operation === "ui.recording.finish") return commandInfo[operation].value
43
41
  throw new Error(`unknown drive command "${operation}"`)
44
42
  }
45
43
 
@@ -142,10 +140,8 @@ async function execute(
142
140
  return ui.screenshot()
143
141
  case "ui.state":
144
142
  return ui.state()
145
- case "ui.start-record":
146
- return ui.startRecord()
147
- case "ui.end-record":
148
- return ui.endRecord()
143
+ case "ui.recording.finish":
144
+ return ui.finishRecording()
149
145
  }
150
146
  throw new Error(`unknown drive command "${command.operation}"`)
151
147
  }
@@ -5,11 +5,16 @@ import type {
5
5
  ResponseUpdate,
6
6
  } from "./response-generator.js"
7
7
 
8
+ export interface StopResult {
9
+ readonly recording?: string
10
+ readonly screenshots: ReadonlyArray<string>
11
+ }
12
+
8
13
  export async function listenControl(
9
14
  path: string,
10
15
  handlers: {
11
- readonly restart: () => Promise<void>
12
- readonly stop: () => Promise<void>
16
+ readonly restart: () => Promise<string | undefined>
17
+ readonly stop: (onProgress: (percent: number) => void) => Promise<StopResult>
13
18
  readonly responses: (
14
19
  input: ResponseUpdate,
15
20
  ) => Promise<ResponseConfiguration>
@@ -27,7 +32,8 @@ export async function listenControl(
27
32
  }
28
33
  if (!buffer.includes("\n")) return
29
34
  socket.removeAllListeners("data")
30
- void handle(buffer.slice(0, buffer.indexOf("\n"))).then(
35
+ const progress = (percent: number) => socket.write(`progress ${percent}\n`)
36
+ void handle(buffer.slice(0, buffer.indexOf("\n")), progress).then(
31
37
  (result) =>
32
38
  socket.end(
33
39
  `success${result === undefined ? "" : ` ${JSON.stringify(result)}`}\n`,
@@ -39,9 +45,9 @@ export async function listenControl(
39
45
  )
40
46
  })
41
47
  })
42
- const handle = async (input: string) => {
48
+ const handle = async (input: string, onProgress: (percent: number) => void) => {
43
49
  if (input === "restart") return handlers.restart()
44
- if (input === "stop") return handlers.stop()
50
+ if (input === "stop") return handlers.stop(onProgress)
45
51
  if (input === "responses") return handlers.responses({})
46
52
  if (input.startsWith("responses "))
47
53
  return handlers.responses(parseResponseUpdate(input.slice("responses ".length)))
@@ -54,9 +60,28 @@ export async function listenControl(
54
60
  }
55
61
  }
56
62
 
57
- export async function request(path: string, command: "restart" | "stop") {
63
+ export async function request(
64
+ path: string,
65
+ command: "restart",
66
+ ) {
58
67
  const response = await send(path, command)
59
- if (response !== "success") throw responseError(response)
68
+ if (response === "success") return undefined
69
+ if (!response.startsWith("success ")) throw responseError(response)
70
+ const value: unknown = JSON.parse(response.slice("success ".length))
71
+ if (typeof value !== "string")
72
+ throw new Error("instance returned an invalid recording path")
73
+ return value
74
+ }
75
+
76
+ export async function requestStop(
77
+ path: string,
78
+ onProgress?: (percent: number) => void,
79
+ ) {
80
+ const response = await send(path, "stop", onProgress)
81
+ if (!response.startsWith("success ")) throw responseError(response)
82
+ const value: unknown = JSON.parse(response.slice("success ".length))
83
+ if (!isStopResult(value)) throw new Error("instance returned an invalid stop result")
84
+ return value
60
85
  }
61
86
 
62
87
  export async function requestResponses(path: string, input: ResponseUpdate) {
@@ -73,22 +98,34 @@ export async function requestResponses(path: string, input: ResponseUpdate) {
73
98
  return value
74
99
  }
75
100
 
76
- function send(path: string, command: string) {
101
+ function send(path: string, command: string, onProgress?: (percent: number) => void) {
77
102
  return new Promise<string>((resolve, reject) => {
78
103
  const socket = connect(path)
79
104
  let response = ""
105
+ let buffer = ""
80
106
  const timer = setTimeout(() => {
81
107
  socket.destroy()
82
108
  reject(new Error("instance control request timed out"))
83
- }, 10_000)
109
+ }, 5 * 60_000)
84
110
  socket.setEncoding("utf8")
85
111
  socket.on("connect", () => socket.write(`${command}\n`))
86
112
  socket.on("data", (data) => {
87
- response += data
113
+ buffer += data
114
+ while (buffer.includes("\n")) {
115
+ const index = buffer.indexOf("\n")
116
+ const line = buffer.slice(0, index)
117
+ buffer = buffer.slice(index + 1)
118
+ if (line.startsWith("progress ")) {
119
+ const percent = Number(line.slice("progress ".length))
120
+ if (Number.isInteger(percent)) onProgress?.(percent)
121
+ continue
122
+ }
123
+ response += `${line}\n`
124
+ }
88
125
  })
89
126
  socket.on("end", () => {
90
127
  clearTimeout(timer)
91
- resolve(response.trim())
128
+ resolve(`${response}${buffer}`.trim())
92
129
  })
93
130
  socket.on("error", () => {
94
131
  clearTimeout(timer)
@@ -123,6 +160,13 @@ function isResponseConfiguration(
123
160
  return "tools" in value && stringArrayValue(value.tools)
124
161
  }
125
162
 
163
+ function isStopResult(value: unknown): value is StopResult {
164
+ if (typeof value !== "object" || value === null) return false
165
+ if (!("screenshots" in value) || !stringArrayValue(value.screenshots))
166
+ return false
167
+ return !("recording" in value) || typeof value.recording === "string"
168
+ }
169
+
126
170
  function stringArrayValue(value: unknown) {
127
171
  return Array.isArray(value) && value.every((item) => typeof item === "string")
128
172
  }
package/src/cli/index.ts CHANGED
@@ -38,14 +38,13 @@ const startCommand = Command.make(
38
38
  visible: Flag.boolean("visible").pipe(
39
39
  Flag.withDescription("Show OpenCode in the terminal"),
40
40
  ),
41
+ record: Flag.boolean("record").pipe(
42
+ Flag.withDescription("Record the complete headless session and export it on stop"),
43
+ ),
41
44
  dev: Flag.string("dev").pipe(
42
45
  Flag.optional,
43
46
  Flag.withDescription("Path to an OpenCode development checkout"),
44
47
  ),
45
- state: Flag.string("state").pipe(
46
- Flag.optional,
47
- Flag.withDescription("Simulation snapshot containing files/"),
48
- ),
49
48
  },
50
49
  (config) =>
51
50
  execute(() =>
@@ -100,32 +99,6 @@ const screenshotCommand = Command.make("screenshot", { name }, (config) =>
100
99
  ),
101
100
  ).pipe(Command.withDescription("Take a screenshot and print its path"))
102
101
 
103
- const startRecordingCommand = Command.make(
104
- "record-start",
105
- { name },
106
- (config) =>
107
- execute(() =>
108
- send({
109
- kind: "send",
110
- name: Option.getOrUndefined(config.name),
111
- commands: [{ operation: "ui.start-record" }],
112
- }),
113
- ),
114
- ).pipe(Command.withDescription("Start recording the UI"))
115
-
116
- const endRecordingCommand = Command.make(
117
- "record-end",
118
- { name },
119
- (config) =>
120
- execute(() =>
121
- send({
122
- kind: "send",
123
- name: Option.getOrUndefined(config.name),
124
- commands: [{ operation: "ui.end-record" }],
125
- }),
126
- ),
127
- ).pipe(Command.withDescription("Stop recording and print its path"))
128
-
129
102
  const restartCommand = Command.make("restart", { name }, (config) =>
130
103
  execute(() => restart(Option.getOrUndefined(config.name))),
131
104
  ).pipe(
@@ -175,8 +148,6 @@ const root = Command.make("opencode-drive").pipe(
175
148
  startCommand,
176
149
  sendCommand,
177
150
  screenshotCommand,
178
- startRecordingCommand,
179
- endRecordingCommand,
180
151
  listCommand,
181
152
  responsesCommand,
182
153
  logsCommand,
@@ -196,8 +167,8 @@ function toStartOptions(
196
167
  readonly name: string
197
168
  readonly daemon: boolean
198
169
  readonly visible: boolean
170
+ readonly record: boolean
199
171
  readonly dev: Option.Option<string>
200
- readonly state: Option.Option<string>
201
172
  },
202
173
  commands: ReadonlyArray<DriveCommand>,
203
174
  app: ReadonlyArray<string>,
@@ -210,8 +181,8 @@ function toStartOptions(
210
181
  daemon: config.daemon,
211
182
  script: Option.getOrUndefined(config.script),
212
183
  visible: config.visible,
184
+ record: config.record,
213
185
  dev: Option.getOrUndefined(config.dev),
214
- state: Option.getOrUndefined(config.state),
215
186
  command: app,
216
187
  }
217
188
  if (options.dev !== undefined && app.length > 0)
@@ -1,15 +1,18 @@
1
- import { mkdir, rm } from "node:fs/promises"
1
+ import { mkdir } from "node:fs/promises"
2
2
  import { tmpdir } from "node:os"
3
3
  import { join, resolve } from "node:path"
4
+ import { ensureMediaDirectory } from "./media.js"
5
+ import type { DriveScriptSetup } from "./script.js"
4
6
 
5
7
  export interface LaunchOptions {
6
8
  readonly name: string
7
9
  readonly command?: ReadonlyArray<string>
8
10
  readonly dev?: string
9
- readonly state?: string
10
11
  readonly scripted?: boolean
11
12
  readonly visible?: boolean
13
+ readonly record?: boolean
12
14
  readonly env?: Readonly<Record<string, string>>
15
+ readonly setup?: DriveScriptSetup
13
16
  }
14
17
 
15
18
  export async function launchInstance(options: LaunchOptions) {
@@ -22,6 +25,7 @@ export async function launchInstance(options: LaunchOptions) {
22
25
  backend: `ws://127.0.0.1:${await freePort()}`,
23
26
  }
24
27
  const drive = join(artifacts, "drive")
28
+ const media = await ensureMediaDirectory()
25
29
  await Promise.all([
26
30
  mkdir(logs, { recursive: true }),
27
31
  mkdir(drive, { recursive: true }),
@@ -30,66 +34,71 @@ export async function launchInstance(options: LaunchOptions) {
30
34
  mkdir(join(artifacts, "home", ".local", "share"), { recursive: true }),
31
35
  mkdir(join(artifacts, "home", ".local", "state"), { recursive: true }),
32
36
  ])
33
- await Bun.write(
34
- join(drive, `${options.name}.json`),
35
- `${JSON.stringify({ endpoints }, undefined, 2)}\n`,
36
- )
37
- const state = options.state
38
- ? resolve(options.state)
39
- : join(artifacts, "state")
40
- if (!options.state) {
41
- await rm(state, { recursive: true, force: true })
42
- await Promise.all([
43
- mkdir(join(state, "files", ".git"), { recursive: true }),
44
- mkdir(join(state, "files", ".opencode"), { recursive: true }),
45
- mkdir(join(state, "files", "src"), { recursive: true }),
46
- ])
47
- await Promise.all([
48
- Bun.write(
49
- join(state, "files", ".opencode", "opencode.jsonc"),
50
- `${JSON.stringify(
51
- {
52
- model: "simulation/gpt-sim-model",
53
- permissions: [{ action: "*", resource: "*", effect: "allow" }],
54
- providers: {
55
- simulation: {
56
- name: "Simulation",
57
- package: "aisdk:@ai-sdk/openai-compatible",
58
- settings: { baseURL: "https://api.openai.com/v1" },
59
- request: { body: { apiKey: "sim-key" } },
60
- models: {
61
- "gpt-sim-model": {
62
- name: "Simulated Model",
63
- capabilities: {
64
- tools: true,
65
- input: ["text"],
66
- output: ["text"],
67
- },
68
- limit: { context: 128000, output: 16000 },
37
+ let recording = options.record ? recordingPaths(media) : undefined
38
+ const writeDriveManifest = () =>
39
+ Bun.write(
40
+ join(drive, `${options.name}.json`),
41
+ `${JSON.stringify(
42
+ {
43
+ endpoints,
44
+ ...(recording ? { recording: { timeline: recording.timeline } } : {}),
45
+ },
46
+ undefined,
47
+ 2,
48
+ )}\n`,
49
+ )
50
+ await writeDriveManifest()
51
+ const files = join(artifacts, "files")
52
+ await Promise.all([
53
+ mkdir(join(files, ".git"), { recursive: true }),
54
+ mkdir(join(files, ".opencode"), { recursive: true }),
55
+ mkdir(join(files, "src"), { recursive: true }),
56
+ ])
57
+ await Promise.all([
58
+ Bun.write(
59
+ join(files, ".opencode", "opencode.jsonc"),
60
+ `${JSON.stringify(
61
+ {
62
+ model: "simulation/gpt-sim-model",
63
+ permissions: [{ action: "*", resource: "*", effect: "allow" }],
64
+ providers: {
65
+ simulation: {
66
+ name: "Simulation",
67
+ package: "aisdk:@ai-sdk/openai-compatible",
68
+ settings: { baseURL: "https://api.openai.com/v1" },
69
+ request: { body: { apiKey: "sim-key" } },
70
+ models: {
71
+ "gpt-sim-model": {
72
+ name: "Simulated Model",
73
+ capabilities: {
74
+ tools: true,
75
+ input: ["text"],
76
+ output: ["text"],
69
77
  },
78
+ limit: { context: 128000, output: 16000 },
70
79
  },
71
80
  },
72
81
  },
73
82
  },
74
- undefined,
75
- 2,
76
- )}\n`,
77
- ),
78
- Bun.write(
79
- join(state, "files", "src", "garden.js"),
80
- 'export function greet(name) {\n return `Hello, ${name}.`\n}\n',
81
- ),
82
- ])
83
- }
83
+ },
84
+ undefined,
85
+ 2,
86
+ )}\n`,
87
+ ),
88
+ Bun.write(
89
+ join(files, "src", "garden.js"),
90
+ 'export function greet(name) {\n return `Hello, ${name}.`\n}\n',
91
+ ),
92
+ ])
93
+ await options.setup?.({ directory: files })
84
94
  const environment = cleanEnv({
85
95
  ...process.env,
86
96
  ...options.env,
87
97
  OPENCODE_SIMULATE: "1",
88
- OPENCODE_SIMULATE_STATE: state,
89
98
  DRIVE_REGISTRY_DIR: drive,
90
99
  OPENCODE_DRIVE: options.name,
91
100
  OPENCODE_DRIVE_RENDERER: options.visible ? "visible" : "headless",
92
- OPENCODE_CONFIG_DIR: join(artifacts, ".opencode"),
101
+ OPENCODE_CONFIG_DIR: join(files, ".opencode"),
93
102
  OPENCODE_DB: ":memory:",
94
103
  OPENCODE_LOG_LEVEL: !options.visible
95
104
  ? "INFO"
@@ -101,13 +110,13 @@ export async function launchInstance(options: LaunchOptions) {
101
110
  XDG_STATE_HOME: join(artifacts, "home", ".local", "state"),
102
111
  })
103
112
  const command = options.dev
104
- ? await prepareDev(artifacts, options.dev)
113
+ ? await prepareDev(files, options.dev)
105
114
  : options.command?.length
106
115
  ? [...options.command]
107
116
  : ["opencode2"]
108
117
  const spawn = () =>
109
118
  Bun.spawn(command, {
110
- cwd: artifacts,
119
+ cwd: files,
111
120
  env: environment,
112
121
  stdin: options.visible ? "inherit" : "ignore",
113
122
  stdout: !options.visible
@@ -124,6 +133,9 @@ export async function launchInstance(options: LaunchOptions) {
124
133
  artifacts,
125
134
  logs,
126
135
  endpoints,
136
+ get recording() {
137
+ return recording
138
+ },
127
139
  get child() {
128
140
  return child
129
141
  },
@@ -143,6 +155,8 @@ export async function launchInstance(options: LaunchOptions) {
143
155
  if (restarting) return restarting
144
156
  restarting = (async () => {
145
157
  await terminate(child)
158
+ recording = options.record ? recordingPaths(media) : undefined
159
+ await writeDriveManifest()
146
160
  child = spawn()
147
161
  await Promise.all([
148
162
  waitForWebSocket(endpoints.ui, child.exited, 60_000),
@@ -177,6 +191,14 @@ export async function launchInstance(options: LaunchOptions) {
177
191
  }
178
192
  }
179
193
 
194
+ function recordingPaths(directory: string) {
195
+ const id = crypto.randomUUID()
196
+ return {
197
+ timeline: join(directory, `recording-${id}.jsonl`),
198
+ video: join(directory, `recording-${id}.mp4`),
199
+ }
200
+ }
201
+
180
202
  async function terminate(child: Bun.Subprocess) {
181
203
  if (child.exitCode !== null) return
182
204
  child.kill("SIGTERM")
@@ -0,0 +1,16 @@
1
+ import { mkdir } from "node:fs/promises"
2
+ import { tmpdir } from "node:os"
3
+ import { join, resolve } from "node:path"
4
+
5
+ export function mediaDirectory() {
6
+ return resolve(
7
+ process.env.OPENCODE_DRIVE_MEDIA_DIR ??
8
+ join(tmpdir(), "opencode-drive", "output"),
9
+ )
10
+ }
11
+
12
+ export async function ensureMediaDirectory() {
13
+ const directory = mediaDirectory()
14
+ await mkdir(directory, { recursive: true })
15
+ return directory
16
+ }
@@ -2,6 +2,6 @@ import { request } from "./control.js"
2
2
  import { resolveInstance } from "./registry.js"
3
3
 
4
4
  export async function restart(name?: string) {
5
- await request((await resolveInstance(name)).control, "restart")
6
- console.log("success")
5
+ const recording = await request((await resolveInstance(name)).control, "restart")
6
+ console.log(recording ?? "success")
7
7
  }
package/src/cli/script.ts CHANGED
@@ -15,23 +15,44 @@ export interface ScriptContext {
15
15
 
16
16
  export type DriveScript = (context: ScriptContext) => void | Promise<void>
17
17
 
18
+ export interface ScriptSetupContext {
19
+ readonly directory: string
20
+ }
21
+
22
+ export type DriveScriptSetup = (
23
+ context: ScriptSetupContext,
24
+ ) => void | Promise<void>
25
+
26
+ export interface LoadedDriveScript {
27
+ readonly run: DriveScript
28
+ readonly setup?: DriveScriptSetup
29
+ }
30
+
18
31
  export function defineScript(script: DriveScript) {
19
32
  return script
20
33
  }
21
34
 
35
+ export async function loadScript(file: string): Promise<LoadedDriveScript> {
36
+ const module: { readonly default?: unknown; readonly setup?: unknown } =
37
+ await import(pathToFileURL(resolve(file)).href)
38
+ if (!isDriveScript(module.default))
39
+ throw new Error("script must default-export a function")
40
+ if (module.setup !== undefined && !isDriveScriptSetup(module.setup))
41
+ throw new Error("script setup export must be a function")
42
+ return {
43
+ run: module.default,
44
+ ...(module.setup === undefined ? {} : { setup: module.setup }),
45
+ }
46
+ }
47
+
22
48
  export async function runScript(
23
- file: string,
49
+ script: DriveScript,
24
50
  artifacts: string,
25
51
  endpoints: { readonly ui: string; readonly backend: string },
26
52
  signal: AbortSignal,
53
+ onScreenshot?: (path: string) => void,
27
54
  ) {
28
- const module: { readonly default?: unknown } = await import(
29
- pathToFileURL(resolve(file)).href
30
- )
31
- const script = module.default
32
- if (!isDriveScript(script))
33
- throw new Error("script must default-export a function")
34
- const ui = await connectSimulation({ url: endpoints.ui })
55
+ const ui = await connectSimulation({ url: endpoints.ui, onScreenshot })
35
56
  const backend = await connectBackendSimulation({
36
57
  url: endpoints.backend,
37
58
  }).catch((error) => {
@@ -75,3 +96,7 @@ async function waitForEditor(ui: SimulationClient, signal: AbortSignal) {
75
96
  function isDriveScript(value: unknown): value is DriveScript {
76
97
  return typeof value === "function"
77
98
  }
99
+
100
+ function isDriveScriptSetup(value: unknown): value is DriveScriptSetup {
101
+ return typeof value === "function"
102
+ }
package/src/cli/send.ts CHANGED
@@ -5,13 +5,11 @@ import { resolveInstance } from "./registry.js"
5
5
  export async function send(options: SendOptions) {
6
6
  if (options.commands.length === 0)
7
7
  throw new Error("send requires at least one --command.ui.* flag")
8
- const result = await executeCommands(
9
- (await resolveInstance(options.name)).endpoints.ui,
10
- options.commands,
11
- )
8
+ const instance = await resolveInstance(options.name)
9
+ const result = await executeCommands(instance.endpoints.ui, options.commands)
12
10
  if (
13
11
  options.commands.length === 1 &&
14
- ["ui.screenshot", "ui.end-record"].includes(
12
+ ["ui.screenshot", "ui.recording.finish"].includes(
15
13
  options.commands[0]?.operation ?? "",
16
14
  )
17
15
  ) {
@@ -20,7 +18,7 @@ export async function send(options: SendOptions) {
20
18
  }
21
19
  if (
22
20
  options.commands.length === 1 &&
23
- ["ui.state", "ui.start-record"].includes(
21
+ ["ui.state"].includes(
24
22
  options.commands[0]?.operation ?? "",
25
23
  )
26
24
  ) {