fix(simulation): stabilize screenshot artifacts (#35933)

This commit is contained in:
James Long
2026-07-08 11:48:53 -04:00
committed by GitHub
parent 6045824878
commit c57f516f7e
6 changed files with 37 additions and 14 deletions
+3
View File
@@ -873,6 +873,7 @@
"name": "@opencode-ai/simulation", "name": "@opencode-ai/simulation",
"version": "1.17.13", "version": "1.17.13",
"dependencies": { "dependencies": {
"@fontsource/adwaita-mono": "5.2.1",
"@napi-rs/canvas": "1.0.2", "@napi-rs/canvas": "1.0.2",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/llm": "workspace:*", "@opencode-ai/llm": "workspace:*",
@@ -1724,6 +1725,8 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
"@fontsource/adwaita-mono": ["@fontsource/adwaita-mono@5.2.1", "", {}, "sha512-6+Q1UIvklJ9REijs6kv7YlRNt6yktRj0iW8H69YIugdD9P2h3eIX1AB8/9ICMfpVyVeywlsrCXg82y/LfRrjyg=="],
"@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.5", "", {}, "sha512-G09N3GfuT9qj3Ax2FDZvKqZttzM3v+cco2l8uXamhKyXLdmlaUDH5o88/C3vtTHj2oT7yRKsvxz9F+BXbWKMYA=="], "@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.5", "", {}, "sha512-G09N3GfuT9qj3Ax2FDZvKqZttzM3v+cco2l8uXamhKyXLdmlaUDH5o88/C3vtTHj2oT7yRKsvxz9F+BXbWKMYA=="],
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
+1
View File
@@ -17,6 +17,7 @@
"typecheck": "tsgo --noEmit" "typecheck": "tsgo --noEmit"
}, },
"dependencies": { "dependencies": {
"@fontsource/adwaita-mono": "5.2.1",
"@napi-rs/canvas": "1.0.2", "@napi-rs/canvas": "1.0.2",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/llm": "workspace:*", "@opencode-ai/llm": "workspace:*",
+17 -5
View File
@@ -1,6 +1,6 @@
import { mkdir, mkdtemp } from "node:fs/promises" import { mkdir } from "node:fs/promises"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
import { dirname, join } from "node:path" import { extname, join, resolve } from "node:path"
import type { CliRenderer, Renderable } from "@opentui/core" import type { CliRenderer, Renderable } from "@opentui/core"
import { createMockKeys, createMockMouse, type MockInput, type MockMouse } from "@opentui/core/testing" import { createMockKeys, createMockMouse, type MockInput, type MockMouse } from "@opentui/core/testing"
import type { SimulationProtocol } from "../protocol" import type { SimulationProtocol } from "../protocol"
@@ -104,11 +104,23 @@ export function state(harness: Harness) {
} }
} }
export async function screenshot(harness: Harness, output?: string) { export async function screenshot(harness: Harness, name?: string) {
await harness.renderOnce() await harness.renderOnce()
const image = SimulationPng.screenshot(harness.renderer) const image = SimulationPng.screenshot(harness.renderer)
const path = output ?? join(await mkdtemp(join(tmpdir(), "opencode-drive-")), "screenshot.png") const filename = name ?? `screenshot-${crypto.randomUUID()}`
if (output) await mkdir(dirname(output), { recursive: true }) if (
!filename ||
filename.includes("/") ||
filename.includes("\\") ||
extname(filename)
)
throw new Error("screenshot name must not contain a path or extension")
const directory = resolve(
process.env.OPENCODE_DRIVE_MEDIA_DIR ??
join(tmpdir(), "opencode-drive", "output"),
)
await mkdir(directory, { recursive: true })
const path = join(directory, `${filename}.png`)
await Bun.write(path, image.data) await Bun.write(path, image.data)
return path return path
} }
+12 -5
View File
@@ -5,12 +5,19 @@ import { TextAttributes, type CapturedFrame, type CliRenderer, type RGBA } from
const CellWidth = 10 const CellWidth = 10
const CellHeight = 20 const CellHeight = 20
const FontSize = 16 const FontSize = 16
const FontFamily = "OpenCode Screenshot" const FontFamily = "OpenCode Mono"
GlobalFonts.registerFromPath( for (const file of [
fileURLToPath(new URL("../../../ui/src/assets/fonts/JetBrainsMonoNerdFontMono-Regular.woff2", import.meta.url)), "adwaita-mono-latin-400-normal.woff2",
FontFamily, "adwaita-mono-latin-700-normal.woff2",
) "adwaita-mono-latin-400-italic.woff2",
"adwaita-mono-latin-700-italic.woff2",
]) {
GlobalFonts.registerFromPath(
fileURLToPath(import.meta.resolve(`@fontsource/adwaita-mono/files/${file}`)),
FontFamily,
)
}
export function screenshot(renderer: CliRenderer) { export function screenshot(renderer: CliRenderer) {
return screenshotFrame({ return screenshotFrame({
+1 -1
View File
@@ -17,7 +17,7 @@ async function handle(
) { ) {
switch (request.method) { switch (request.method) {
case "ui.screenshot": case "ui.screenshot":
return SimulationActions.screenshot(harness, request.params?.path) return SimulationActions.screenshot(harness, request.params?.name)
case "ui.state": { case "ui.state": {
return SimulationActions.state(harness) return SimulationActions.state(harness)
} }
+3 -3
View File
@@ -97,8 +97,8 @@ export namespace Frontend {
export const RecordingFinish = Schema.String export const RecordingFinish = Schema.String
export type RecordingFinish = Schema.Schema.Type<typeof RecordingFinish> export type RecordingFinish = Schema.Schema.Type<typeof RecordingFinish>
export const ArtifactParams = Schema.Struct({ path: Schema.optional(Schema.String) }) export const ScreenshotParams = Schema.Struct({ name: Schema.optional(Schema.String) })
export interface ArtifactParams extends Schema.Schema.Type<typeof ArtifactParams> {} export interface ScreenshotParams extends Schema.Schema.Type<typeof ScreenshotParams> {}
export const TypeParams = Schema.Struct({ text: Schema.String }) export const TypeParams = Schema.Struct({ text: Schema.String })
export interface TypeParams extends Schema.Schema.Type<typeof TypeParams> {} export interface TypeParams extends Schema.Schema.Type<typeof TypeParams> {}
@@ -124,7 +124,7 @@ export namespace Frontend {
Schema.Struct({ Schema.Struct({
...JsonRpc.RequestFields, ...JsonRpc.RequestFields,
method: Schema.Literal("ui.screenshot"), method: Schema.Literal("ui.screenshot"),
params: Schema.optional(ArtifactParams), params: Schema.optional(ScreenshotParams),
}), }),
Schema.Struct({ Schema.Struct({
...JsonRpc.RequestFields, ...JsonRpc.RequestFields,