diff --git a/bun.lock b/bun.lock index bd1981f6e5..df33000ba5 100644 --- a/bun.lock +++ b/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=="], diff --git a/packages/simulation/package.json b/packages/simulation/package.json index f9be3bfc33..624cf32ef7 100644 --- a/packages/simulation/package.json +++ b/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:*", diff --git a/packages/simulation/src/frontend/actions.ts b/packages/simulation/src/frontend/actions.ts index 068949459b..5819d94035 100644 --- a/packages/simulation/src/frontend/actions.ts +++ b/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 } diff --git a/packages/simulation/src/frontend/png.ts b/packages/simulation/src/frontend/png.ts index 137a09e1be..4f7b11c1a0 100644 --- a/packages/simulation/src/frontend/png.ts +++ b/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({ diff --git a/packages/simulation/src/frontend/server.ts b/packages/simulation/src/frontend/server.ts index e3149c39f8..b878bdd761 100644 --- a/packages/simulation/src/frontend/server.ts +++ b/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) } diff --git a/packages/simulation/src/protocol/index.ts b/packages/simulation/src/protocol/index.ts index eda31c30bf..a2b9fa3c29 100644 --- a/packages/simulation/src/protocol/index.ts +++ b/packages/simulation/src/protocol/index.ts @@ -97,8 +97,8 @@ export namespace Frontend { export const RecordingFinish = Schema.String export type RecordingFinish = Schema.Schema.Type - export const ArtifactParams = Schema.Struct({ path: Schema.optional(Schema.String) }) - export interface ArtifactParams extends Schema.Schema.Type {} + export const ScreenshotParams = Schema.Struct({ name: Schema.optional(Schema.String) }) + export interface ScreenshotParams extends Schema.Schema.Type {} export const TypeParams = Schema.Struct({ text: Schema.String }) export interface TypeParams extends Schema.Schema.Type {} @@ -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,