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 b3cf749a7f
commit e1d72fa338
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 37 additions and 14 deletions

View file

@ -873,6 +873,7 @@
"name": "@opencode-ai/simulation",
"version": "1.17.13",
"dependencies": {
"@fontsource/adwaita-mono": "5.2.1",
"@napi-rs/canvas": "1.0.2",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/llm": "workspace:*",
@ -1724,6 +1725,8 @@
"@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/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],

View file

@ -17,6 +17,7 @@
"typecheck": "tsgo --noEmit"
},
"dependencies": {
"@fontsource/adwaita-mono": "5.2.1",
"@napi-rs/canvas": "1.0.2",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/llm": "workspace:*",

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 { dirname, join } from "node:path"
import { extname, join, resolve } from "node:path"
import type { CliRenderer, Renderable } from "@opentui/core"
import { createMockKeys, createMockMouse, type MockInput, type MockMouse } from "@opentui/core/testing"
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()
const image = SimulationPng.screenshot(harness.renderer)
const path = output ?? join(await mkdtemp(join(tmpdir(), "opencode-drive-")), "screenshot.png")
if (output) await mkdir(dirname(output), { recursive: true })
const filename = name ?? `screenshot-${crypto.randomUUID()}`
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)
return path
}

View file

@ -5,12 +5,19 @@ import { TextAttributes, type CapturedFrame, type CliRenderer, type RGBA } from
const CellWidth = 10
const CellHeight = 20
const FontSize = 16
const FontFamily = "OpenCode Screenshot"
const FontFamily = "OpenCode Mono"
GlobalFonts.registerFromPath(
fileURLToPath(new URL("../../../ui/src/assets/fonts/JetBrainsMonoNerdFontMono-Regular.woff2", import.meta.url)),
FontFamily,
)
for (const file of [
"adwaita-mono-latin-400-normal.woff2",
"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) {
return screenshotFrame({

View file

@ -17,7 +17,7 @@ async function handle(
) {
switch (request.method) {
case "ui.screenshot":
return SimulationActions.screenshot(harness, request.params?.path)
return SimulationActions.screenshot(harness, request.params?.name)
case "ui.state": {
return SimulationActions.state(harness)
}

View file

@ -97,8 +97,8 @@ export namespace Frontend {
export const RecordingFinish = Schema.String
export type RecordingFinish = Schema.Schema.Type<typeof RecordingFinish>
export const ArtifactParams = Schema.Struct({ path: Schema.optional(Schema.String) })
export interface ArtifactParams extends Schema.Schema.Type<typeof ArtifactParams> {}
export const ScreenshotParams = Schema.Struct({ name: Schema.optional(Schema.String) })
export interface ScreenshotParams extends Schema.Schema.Type<typeof ScreenshotParams> {}
export const TypeParams = Schema.Struct({ text: Schema.String })
export interface TypeParams extends Schema.Schema.Type<typeof TypeParams> {}
@ -124,7 +124,7 @@ export namespace Frontend {
Schema.Struct({
...JsonRpc.RequestFields,
method: Schema.Literal("ui.screenshot"),
params: Schema.optional(ArtifactParams),
params: Schema.optional(ScreenshotParams),
}),
Schema.Struct({
...JsonRpc.RequestFields,