From daa1c15cde2e37b2bd4f0ca9127821f44f4f9407 Mon Sep 17 00:00:00 2001 From: fahd04 Date: Thu, 19 Feb 2026 12:38:13 +0000 Subject: [PATCH 1/3] feat: shell_screenshot returns png_url and svg_url Add png_url and svg_url to shell_screenshot response alongside download_url for backward compatibility. Users can now choose SVG for scalability or PNG for compatibility. Also fix TypeScript error in buffer-to-svg.ts (showCursor not in IModes type) that prevented local build. Closes #45 --- src/index.ts | 2 +- src/lib/buffer-to-svg.ts | 2 +- src/tools/shell-screenshot.ts | 12 ++++++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 57f0b59..2516fee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -202,7 +202,7 @@ Tips: server.tool( "shell_screenshot", - "Capture terminal screenshot as PNG. Returns a download_url - use curl to save the file locally (e.g., curl -o screenshot.png )", + "Capture terminal screenshot as PNG and SVG. Returns png_url and svg_url - use curl to save (e.g., curl -o screenshot.png )", shellScreenshotSchema, async (params) => shellScreenshot(params, toolContext) ); diff --git a/src/lib/buffer-to-svg.ts b/src/lib/buffer-to-svg.ts index 1891dcb..4d72c4a 100644 --- a/src/lib/buffer-to-svg.ts +++ b/src/lib/buffer-to-svg.ts @@ -159,7 +159,7 @@ export function bufferToSvg( } // Render cursor if visible - if (terminal.modes.showCursor) { + if ((terminal.modes as { showCursor?: boolean }).showCursor) { const cursorX = buffer.cursorX; const cursorY = buffer.cursorY; diff --git a/src/tools/shell-screenshot.ts b/src/tools/shell-screenshot.ts index 7c658fa..ca924ef 100644 --- a/src/tools/shell-screenshot.ts +++ b/src/tools/shell-screenshot.ts @@ -53,8 +53,16 @@ export async function shellScreenshot( context.log(`[shellwright] Screenshot saved: ${screenshotDir}/${baseName}.{png,svg,ansi,txt}`); - const downloadUrl = context.getDownloadUrl(context.getMcpSessionId(), session_id, "screenshots", filename); - const output = { filename, download_url: downloadUrl, hint: "Use curl -o to save the file" }; + const mcpSessionId = context.getMcpSessionId(); + const pngUrl = context.getDownloadUrl(mcpSessionId, session_id, "screenshots", `${baseName}.png`); + const svgUrl = context.getDownloadUrl(mcpSessionId, session_id, "screenshots", `${baseName}.svg`); + const output = { + filename, + download_url: pngUrl, + png_url: pngUrl, + svg_url: svgUrl, + hint: "Use curl -o to save the file", + }; context.logToolCall("shell_screenshot", { session_id, name }, output); return { From 02e5ea9236c0e85059b897ab9337752967dfece6 Mon Sep 17 00:00:00 2001 From: Dave Kerr Date: Tue, 10 Mar 2026 11:18:04 +0000 Subject: [PATCH 2/3] refactor: replace generic download_url with format-specific URLs Remove the ambiguous download_url field from tool responses and replace with explicit download_png_url, download_svg_url, and download_gif_url so models know exactly which format they're downloading. --- CLAUDE.md | 2 +- README.md | 17 +++++++++-------- docs/architecture.md | 6 +++--- evaluations/run.ts | 23 +++++++++++++++++------ src/index.ts | 2 +- src/tools/shell-record-stop.ts | 4 ++-- src/tools/shell-screenshot.ts | 7 +++---- 7 files changed, 36 insertions(+), 25 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ff7987e..cb5db8f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,7 @@ npm run dev # Development with hot-reload - **TypeScript + ESM** - Modern Node.js with ES modules - **MCP SDK** - Uses `@modelcontextprotocol/sdk` for MCP protocol - **PTY** - Uses `node-pty` for pseudo-terminal management -- **File Server** - HTTP file server runs on port 7498 regardless of MCP transport mode (stdio or HTTP). Tool results include `download_url` which is always valid. +- **File Server** - HTTP file server runs on port 7498 regardless of MCP transport mode (stdio or HTTP). Tool results include format-specific download URLs (`download_png_url`, `download_svg_url`, `download_gif_url`) which are always valid. ## MCP Tools diff --git a/README.md b/README.md index 25f23c3..b384897 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ Some configuration can also be provided by the LLM, simply prompt for it: | [`shell_start`](#shell_start) | Start a new PTY session | | [`shell_send`](#shell_send) | Send input to a session | | [`shell_read`](#shell_read) | Read the terminal buffer | -| [`shell_screenshot`](#shell_screenshot) | Capture terminal as PNG | +| [`shell_screenshot`](#shell_screenshot) | Capture terminal as PNG and SVG | | [`shell_record_start`](#shell_record_start) | Start recording for GIF export | | [`shell_record_stop`](#shell_record_stop) | Stop recording and save GIF | | [`shell_stop`](#shell_stop) | Stop a PTY session | @@ -286,7 +286,7 @@ drwxr-xr-x 10 user staff 320 Dec 18 09:00 .. ### **shell_screenshot** -Capture terminal as PNG. Also saves SVG, ANSI, and plain text versions. Pass `name` without extension (`.png` is added automatically). Optionally add a macOS-style window border (off by default): +Capture terminal as PNG and SVG. Also saves ANSI and plain text versions. Pass `name` without extension (`.png` is added automatically). Optionally add a macOS-style window border (off by default): ```json { @@ -296,13 +296,14 @@ Capture terminal as PNG. Also saves SVG, ANSI, and plain text versions. Pass `na } ``` -The response contains a `download_url` for curl to save the file locally: +The response contains `download_png_url` and `download_svg_url` for curl to save the files locally: ```json { "filename": "my-screenshot.png", - "download_url": "http://localhost:7498/files/mcp-.../screenshots/my-screenshot.png", - "hint": "Use curl -o to save the file" + "download_png_url": "http://localhost:7498/files/mcp-.../screenshots/my-screenshot.png", + "download_svg_url": "http://localhost:7498/files/mcp-.../screenshots/my-screenshot.svg", + "hint": "Use curl -o to save the file" } ``` @@ -339,13 +340,13 @@ Stop recording and render frames to GIF: } ``` -The response contains a `download_url` for curl to save the file locally: +The response contains a `download_gif_url` for curl to save the file locally: ```json { "filename": "my-recording.gif", - "download_url": "http://localhost:7498/files/mcp-.../recordings/my-recording.gif", - "hint": "Use curl -o to save the file", + "download_gif_url": "http://localhost:7498/files/mcp-.../recordings/my-recording.gif", + "hint": "Use curl -o to save the file", "frame_count": 42, "duration_ms": 4200 } diff --git a/docs/architecture.md b/docs/architecture.md index cafa9b8..1643bd7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -35,13 +35,13 @@ ## File Download Flow -Screenshots and recordings return a `download_url` instead of base64 data: +Screenshots and recordings return format-specific download URLs instead of base64 data: ``` 1. LLM calls shell_screenshot() or shell_record_stop() 2. Server saves file to temp directory -3. Server returns: { filename, download_url, ... } -4. LLM uses curl to download: curl -o file.png +3. Server returns: { filename, download_png_url, download_svg_url, ... } or { filename, download_gif_url, ... } +4. LLM uses curl to download: curl -o file.png ``` This avoids token overflow from large base64 payloads. diff --git a/evaluations/run.ts b/evaluations/run.ts index 9386182..1e298d9 100644 --- a/evaluations/run.ts +++ b/evaluations/run.ts @@ -88,7 +88,7 @@ ${prompt}`, } } } else if (message.type === "user") { - // Capture tool results to extract download_url + // Capture tool results to extract download URLs for (const block of message.message.content) { if (block.type === "tool_result") { const content = typeof block.content === "string" @@ -102,11 +102,22 @@ ${prompt}`, } catch { continue; // Not JSON, skip } - if (parsed.download_url && parsed.filename) { - const dest = path.join(scenarioPath, parsed.filename); - console.log(` Downloading: ${parsed.download_url} → ${parsed.filename}`); - await downloadArtifact(parsed.download_url, dest); - artifacts.push({ filename: parsed.filename, localPath: dest }); + const downloadUrls: { url: string; filename: string }[] = []; + if (parsed.download_png_url && parsed.filename) { + downloadUrls.push({ url: parsed.download_png_url, filename: parsed.filename }); + } + if (parsed.download_svg_url && parsed.filename) { + const svgFilename = parsed.filename.replace(/\.png$/i, ".svg"); + downloadUrls.push({ url: parsed.download_svg_url, filename: svgFilename }); + } + if (parsed.download_gif_url && parsed.filename) { + downloadUrls.push({ url: parsed.download_gif_url, filename: parsed.filename }); + } + for (const { url, filename } of downloadUrls) { + const dest = path.join(scenarioPath, filename); + console.log(` Downloading: ${url} → ${filename}`); + await downloadArtifact(url, dest); + artifacts.push({ filename, localPath: dest }); console.log(` ✓ Artifact saved: ${dest}`); } } diff --git a/src/index.ts b/src/index.ts index 626a970..b6c5123 100644 --- a/src/index.ts +++ b/src/index.ts @@ -223,7 +223,7 @@ Tips: server.tool( "shell_record_stop", - "Stop recording and save GIF. Returns a download_url - use curl to save the file locally (e.g., curl -o recording.gif )", + "Stop recording and save GIF. Returns download_gif_url - use curl to save the file locally (e.g., curl -o recording.gif )", shellRecordStopSchema, async (params) => shellRecordStop(params, toolContext) ); diff --git a/src/tools/shell-record-stop.ts b/src/tools/shell-record-stop.ts index e1f817a..b681148 100644 --- a/src/tools/shell-record-stop.ts +++ b/src/tools/shell-record-stop.ts @@ -47,10 +47,10 @@ export async function shellRecordStop( const downloadUrl = context.getDownloadUrl(context.getMcpSessionId(), session_id, "recordings", filename); const output = { filename, - download_url: downloadUrl, + download_gif_url: downloadUrl, frame_count: result.frameCount, duration_ms: durationMs, - hint: "Use curl -o to save the file" + hint: "Use curl -o to save the file" }; context.logToolCall("shell_record_stop", { session_id, name }, output); diff --git a/src/tools/shell-screenshot.ts b/src/tools/shell-screenshot.ts index afcb4a3..da30629 100644 --- a/src/tools/shell-screenshot.ts +++ b/src/tools/shell-screenshot.ts @@ -63,10 +63,9 @@ export async function shellScreenshot( const svgUrl = context.getDownloadUrl(mcpSessionId, session_id, "screenshots", `${baseName}.svg`); const output = { filename, - download_url: pngUrl, - png_url: pngUrl, - svg_url: svgUrl, - hint: "Use curl -o to save the file", + download_png_url: pngUrl, + download_svg_url: svgUrl, + hint: "Use curl -o to save the file", }; context.logToolCall("shell_screenshot", { session_id, name }, output); From dfbb23be7ef3cba912d832c274f697eb44df8c10 Mon Sep 17 00:00:00 2001 From: Dave Kerr Date: Tue, 10 Mar 2026 15:05:49 +0000 Subject: [PATCH 3/3] chore: remove task.md --- task.md | 71 --------------------------------------------------------- 1 file changed, 71 deletions(-) delete mode 100644 task.md diff --git a/task.md b/task.md deleted file mode 100644 index 7896e28..0000000 --- a/task.md +++ /dev/null @@ -1,71 +0,0 @@ -# Task: Add macOS window border to screenshots - -Add an optional `border` parameter to `shell_screenshot` that wraps terminal content in macOS-style window chrome (traffic lights, title bar, rounded corners, drop shadow). - -## Context - -Screenshots are rendered via: terminal buffer → SVG (`src/lib/buffer-to-svg.ts`) → PNG (resvg). The SVG generation already supports themes, font size, and font family options. This feature adds window decorations as SVG elements wrapping the existing content. - -## Changes - -### 1. Extend `SvgOptions` in `src/lib/buffer-to-svg.ts` - -Add border config to the options interface: - -```typescript -interface SvgOptions { - fontSize?: number; - fontFamily?: string; - theme?: Theme; - border?: { - style?: 'macos'; - title?: string; - }; -} -``` - -### 2. Update SVG generation in `src/lib/buffer-to-svg.ts` - -When `border.style === 'macos'`: - -- Add 28px to total height for title bar -- Add 12px horizontal padding each side -- Wrap existing content in `` -- Add before content: - - `` with `` filter (dx=0, dy=4, stdDeviation=6, opacity=0.3) - - Outer `` - - Background `` with rx=8, ry=8, full dimensions - - Title bar `` (top 28px, slightly darker fill) - - Traffic lights: 3 `` at x=12,32,52 y=14 r=6 fills #ff5f56, #ffbd2e, #27c93f - - Optional title `` centered in title bar -- Close wrapper groups after content - -### 3. Pass `border` through tool params in `src/index.ts` - -Update the `shell_screenshot` tool definition (~line 380): - -- Add `border` to the zod schema (optional object with `style` enum and optional `title` string) -- Pass `border` to `bufferToSvg()` call (~line 400) - -### 4. Update `shell_record_start` similarly (optional) - -If recording GIFs should also support borders, pass the option through frame capture. Can skip for v1. - -## Usage - -``` -# Without border (unchanged) -shell_screenshot session_id="..." name="screenshot" - -# With macOS border -shell_screenshot session_id="..." name="screenshot" border={"style": "macos", "title": "Terminal"} -``` - -## Design notes - -- No new dependencies — pure SVG elements, resvg handles them natively -- Non-breaking — border is optional, defaults to no border -- Traffic light colours: red #ff5f56, yellow #ffbd2e, green #27c93f -- Title bar background should be slightly darker than terminal background (e.g. #21252b for one-dark theme) -- Drop shadow gives depth, but keep it subtle -- 8px border radius for rounded corners matches macOS