فهرست منبع

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

James Long 1 ماه پیش
والد
کامیت
e1d72fa338

+ 3 - 0
bun.lock

@@ -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=="],

+ 1 - 0
packages/simulation/package.json

@@ -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:*",

+ 17 - 5
packages/simulation/src/frontend/actions.ts

@@ -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
 }

+ 12 - 5
packages/simulation/src/frontend/png.ts

@@ -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({

+ 1 - 1
packages/simulation/src/frontend/server.ts

@@ -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)
     }

+ 3 - 3
packages/simulation/src/protocol/index.ts

@@ -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,