From 22482bdc15982de17db1ca7156dc5ba5c617396e Mon Sep 17 00:00:00 2001 From: Dave Kerr Date: Fri, 13 Feb 2026 19:00:55 +0800 Subject: [PATCH 1/7] docs: cursor visibility design --- .../2026-02-13-cursor-visibility-design.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 docs/plans/2026-02-13-cursor-visibility-design.md diff --git a/docs/plans/2026-02-13-cursor-visibility-design.md b/docs/plans/2026-02-13-cursor-visibility-design.md new file mode 100644 index 0000000..0c37cae --- /dev/null +++ b/docs/plans/2026-02-13-cursor-visibility-design.md @@ -0,0 +1,36 @@ +# Cursor Visibility Design + +## Summary + +Render the terminal cursor in screenshots and recordings when xterm.js reports it as visible. + +## Behavior + +- Cursor renders automatically when visible in terminal state +- No API changes - no new parameters to tools +- Breaking change: existing outputs will now show cursors where terminal has them visible + +## Implementation + +Approach: Post-pass cursor rendering in `bufferToSvg`. + +After the main cell-rendering loop: + +1. Check cursor visibility (`terminal.modes.cursorHidden` is false, cursor within viewport) +2. Get position from `terminal.buffer.active.cursorX/Y` +3. Get cell at cursor position to determine current fg/bg colors +4. Render filled rectangle at cursor position using `theme.foreground` +5. Re-render character at cursor position with inverted colors (fg/bg swapped) + +SVG layering handles visual stacking. + +## Design Decisions + +- **Block cursor only** - no underline/bar styles for now +- **True color inversion** - text under cursor has fg/bg swapped, not semi-transparent overlay +- **Uses theme.foreground** - no cursor color added to Theme interface +- **Respects terminal state** - renders only when xterm.js cursor is visible + +## Follow-up + +Create GitHub issue `rfc: force show/hide cursor option` for users who need explicit override control. From 2f5676d6b23e4a9ad0ec68bf0cdb8cf31537fc43 Mon Sep 17 00:00:00 2001 From: Dave Kerr Date: Fri, 13 Feb 2026 19:11:37 +0800 Subject: [PATCH 2/7] docs: cursor visibility implementation plan --- docs/plans/2026-02-13-cursor-visibility.md | 142 +++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 docs/plans/2026-02-13-cursor-visibility.md diff --git a/docs/plans/2026-02-13-cursor-visibility.md b/docs/plans/2026-02-13-cursor-visibility.md new file mode 100644 index 0000000..b4ea96f --- /dev/null +++ b/docs/plans/2026-02-13-cursor-visibility.md @@ -0,0 +1,142 @@ +# Cursor Visibility Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Render terminal cursor in screenshots and recordings when visible. + +**Architecture:** Add cursor rendering as post-pass in `bufferToSvg`. Check `terminal.modes.showCursor`, render block cursor with inverted colors at cursor position. + +**Tech Stack:** TypeScript, xterm.js headless, SVG generation + +--- + +### Task 1: Add cursor rendering to bufferToSvg + +**Files:** +- Modify: `src/lib/buffer-to-svg.ts:159` (after main rendering loop) + +**Step 1: Write the cursor rendering code** + +Add after line 159 (after the main `for` loop ends, before the `return` statement): + +```typescript + // Render cursor if visible + if (terminal.modes.showCursor) { + const cursorX = buffer.cursorX; + const cursorY = buffer.cursorY; + + // Only render if cursor is within visible area + if (cursorX >= 0 && cursorX < cols && cursorY >= 0 && cursorY < rows) { + const cursorXPos = padding + cursorX * charWidth; + const cursorYPos = padding + cursorY * lineHeight; + + // Get the cell at cursor position for color inversion + const cursorLine = buffer.getLine(cursorY); + const cursorCell = cursorLine?.getCell(cursorX); + + // Cursor block uses foreground color + const cursorBg = theme.foreground; + // Text under cursor uses background color (inversion) + const cursorFg = theme.background; + + // Draw cursor block + lines.push( + `` + ); + + // Draw inverted character if present + if (cursorCell) { + const char = cursorCell.getChars(); + if (char && char.trim()) { + const textYPos = cursorYPos + opts.fontSize; + lines.push( + `${escapeXml(char)}` + ); + } + } + } + } +``` + +**Step 2: Build to verify no TypeScript errors** + +Run: `npm run build` +Expected: Build succeeds with no errors + +**Step 3: Manual test with a shell session** + +Run shellwright, start a session, take a screenshot. Verify cursor appears as a block. + +**Step 4: Commit** + +```bash +git add src/lib/buffer-to-svg.ts +git commit -m "feat: render cursor in screenshots and recordings" +``` + +--- + +### Task 2: Create GitHub RFC issue for force show/hide cursor + +**Step 1: Create the issue** + +```bash +gh issue create --title "rfc: force show/hide cursor option" --body "$(cat <<'EOF' +## Context + +Cursor rendering now respects terminal state automatically via `terminal.modes.showCursor`. + +## Potential Enhancement + +If users need explicit control to force cursor on/off regardless of terminal state, we could add a parameter: + +```typescript +shell_screenshot({ session_id, showCursor?: 'auto' | 'show' | 'hide' }) +``` + +- `auto` (default): respect terminal state +- `show`: always render cursor +- `hide`: never render cursor + +## Use Cases + +- Force show cursor in TUI apps that hide it +- Force hide cursor in shell sessions for cleaner screenshots + +Please comment if you have a use case for this. +EOF +)" +``` + +**Step 2: Note the issue URL** + +Record the issue URL for reference. + +**Step 3: Commit reference (optional)** + +If desired, add issue reference to design doc. + +--- + +### Task 3: Update design doc with implementation details + +**Files:** +- Modify: `docs/plans/2026-02-13-cursor-visibility-design.md` + +**Step 1: Add API details to design doc** + +Add under Implementation section: + +```markdown +## API Used + +- `terminal.modes.showCursor`: Boolean indicating cursor visibility (DECTCEM state) +- `terminal.buffer.active.cursorX/Y`: Cursor position +``` + +**Step 2: Commit** + +```bash +git add docs/plans/2026-02-13-cursor-visibility-design.md +git commit -m "docs: add API details to cursor visibility design" +``` From d7959fb45c9ae36e870a1b9534ca13e86ee32d71 Mon Sep 17 00:00:00 2001 From: Dave Kerr Date: Fri, 13 Feb 2026 19:26:02 +0800 Subject: [PATCH 3/7] chore: add .worktrees to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9250bac..046a97b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ examples/*/screenshots/ output/ .mcp.json .playwright-mcp/ +.worktrees/ From 83944d874bf21540effb88c3e13cc28a0aa71e3b Mon Sep 17 00:00:00 2001 From: Dave Kerr Date: Fri, 13 Feb 2026 19:33:10 +0800 Subject: [PATCH 4/7] feat: render cursor in screenshots and recordings Upgrade @xterm/headless to 6.1.0-beta.156 for showCursor mode support. --- package-lock.json | 15 +++++++++------ package.json | 2 +- src/lib/buffer-to-svg.ts | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index dbdff49..013c7f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,12 +11,12 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.24.3", "@resvg/resvg-js": "^2.6.2", - "@xterm/headless": "^5.5.0", + "@xterm/headless": "^6.1.0-beta.156", "commander": "^14.0.2", "express": "^5.2.1", "gifenc": "^1.0.3", "jimp": "^1.6.0", - "node-pty": "^1.0.0", + "node-pty": "1.0.0", "strip-ansi": "^7.1.2", "zod": "^3.25.0" }, @@ -3296,10 +3296,13 @@ ] }, "node_modules/@xterm/headless": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.5.0.tgz", - "integrity": "sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g==", - "license": "MIT" + "version": "6.1.0-beta.156", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.156.tgz", + "integrity": "sha512-dfdVGgvF4Jfr32Hb/LszxoefJL1C9JHLWV9e00ZytmiiqEB4cjMwtUdrqJf6gFUmygcQwvIL5Dj37XOsLoPNfA==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] }, "node_modules/abort-controller": { "version": "3.0.0", diff --git a/package.json b/package.json index cc22510..29ba8ec 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.24.3", "@resvg/resvg-js": "^2.6.2", - "@xterm/headless": "^5.5.0", + "@xterm/headless": "^6.1.0-beta.156", "commander": "^14.0.2", "express": "^5.2.1", "gifenc": "^1.0.3", diff --git a/src/lib/buffer-to-svg.ts b/src/lib/buffer-to-svg.ts index 88c11f9..1891dcb 100644 --- a/src/lib/buffer-to-svg.ts +++ b/src/lib/buffer-to-svg.ts @@ -158,6 +158,43 @@ export function bufferToSvg( } } + // Render cursor if visible + if (terminal.modes.showCursor) { + const cursorX = buffer.cursorX; + const cursorY = buffer.cursorY; + + // Only render if cursor is within visible area + if (cursorX >= 0 && cursorX < cols && cursorY >= 0 && cursorY < rows) { + const cursorXPos = padding + cursorX * charWidth; + const cursorYPos = padding + cursorY * lineHeight; + + // Get the cell at cursor position for color inversion + const cursorLine = buffer.getLine(cursorY); + const cursorCell = cursorLine?.getCell(cursorX); + + // Cursor block uses foreground color + const cursorBg = theme.foreground; + // Text under cursor uses background color (inversion) + const cursorFg = theme.background; + + // Draw cursor block + lines.push( + `` + ); + + // Draw inverted character if present + if (cursorCell) { + const char = cursorCell.getChars(); + if (char && char.trim()) { + const textYPos = cursorYPos + opts.fontSize; + lines.push( + `${escapeXml(char)}` + ); + } + } + } + } + return ` From 22cbf7c7510a4f62e05144dd1e87dab9e0745061 Mon Sep 17 00:00:00 2001 From: Dave Kerr Date: Fri, 13 Feb 2026 19:33:52 +0800 Subject: [PATCH 5/7] docs: add API details to cursor visibility design --- docs/plans/2026-02-13-cursor-visibility-design.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/plans/2026-02-13-cursor-visibility-design.md b/docs/plans/2026-02-13-cursor-visibility-design.md index 0c37cae..d722414 100644 --- a/docs/plans/2026-02-13-cursor-visibility-design.md +++ b/docs/plans/2026-02-13-cursor-visibility-design.md @@ -16,7 +16,7 @@ Approach: Post-pass cursor rendering in `bufferToSvg`. After the main cell-rendering loop: -1. Check cursor visibility (`terminal.modes.cursorHidden` is false, cursor within viewport) +1. Check cursor visibility (`terminal.modes.showCursor` is true, cursor within viewport) 2. Get position from `terminal.buffer.active.cursorX/Y` 3. Get cell at cursor position to determine current fg/bg colors 4. Render filled rectangle at cursor position using `theme.foreground` @@ -31,6 +31,13 @@ SVG layering handles visual stacking. - **Uses theme.foreground** - no cursor color added to Theme interface - **Respects terminal state** - renders only when xterm.js cursor is visible +## API Used + +- `terminal.modes.showCursor`: Boolean indicating cursor visibility (DECTCEM state) +- `terminal.buffer.active.cursorX/Y`: Cursor position + +**Note:** Requires `@xterm/headless@6.1.0-beta.156` or later for `showCursor` mode support. + ## Follow-up Create GitHub issue `rfc: force show/hide cursor option` for users who need explicit override control. From f44db43c514ebc8b3d2df58997834afdbce680e0 Mon Sep 17 00:00:00 2001 From: Dave Kerr Date: Fri, 13 Feb 2026 19:44:02 +0800 Subject: [PATCH 6/7] test: add cursor-visibility evaluation scenario --- .../scenarios/cursor-visibility/prompt.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 evaluations/scenarios/cursor-visibility/prompt.md diff --git a/evaluations/scenarios/cursor-visibility/prompt.md b/evaluations/scenarios/cursor-visibility/prompt.md new file mode 100644 index 0000000..a49fa70 --- /dev/null +++ b/evaluations/scenarios/cursor-visibility/prompt.md @@ -0,0 +1,21 @@ +# Cursor Visibility Test + +Verify that the terminal cursor renders correctly in recordings. + +## Instructions + +1. Start a shell session using `bash` with args `["--login", "-i"]` (80x24, one-dark theme) +2. Start recording at 10 FPS +3. Type `echo "testing cursor"` but DO NOT press Enter +4. Wait 500ms +5. Press Ctrl+A to move cursor to the beginning of the line +6. Wait 1 second so the cursor position is clearly visible +7. Stop recording and save as `recording.gif` +8. Stop the session + +## Expected Result + +A short recording showing: +- The text `echo "testing cursor"` being typed +- The cursor moving to the beginning of the line (on the 'e' of 'echo') +- The cursor rendered as a block with inverted colors (the 'e' should appear with swapped foreground/background) From 5fd8c9822cddeb3245d8328575b954c918962cfb Mon Sep 17 00:00:00 2001 From: Dave Kerr Date: Fri, 13 Feb 2026 19:53:20 +0800 Subject: [PATCH 7/7] test: add cursor-visibility baseline and eval filter support - Add scenario filter argument to evaluations/run.ts - Add baseline-local.gif for cursor-visibility scenario --- evaluations/run.ts | 12 ++++++++++-- .../cursor-visibility/baseline-local.gif | Bin 0 -> 36097 bytes 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 evaluations/scenarios/cursor-visibility/baseline-local.gif diff --git a/evaluations/run.ts b/evaluations/run.ts index ede43f0..ebd860e 100644 --- a/evaluations/run.ts +++ b/evaluations/run.ts @@ -144,8 +144,16 @@ async function main() { console.log("Building shellwright..."); execSync("npm run build", { stdio: "inherit", cwd: ROOT_DIR }); - // Find all scenarios - const scenarios = await fs.readdir(SCENARIOS_DIR); + // Find all scenarios, optionally filtered by command line argument + const filterArg = process.argv[2]; + let scenarios = await fs.readdir(SCENARIOS_DIR); + if (filterArg) { + scenarios = scenarios.filter((s) => s.includes(filterArg)); + if (scenarios.length === 0) { + console.error(`No scenarios matching "${filterArg}"`); + process.exit(1); + } + } const results: ScenarioResult[] = []; for (const scenario of scenarios) { diff --git a/evaluations/scenarios/cursor-visibility/baseline-local.gif b/evaluations/scenarios/cursor-visibility/baseline-local.gif new file mode 100644 index 0000000000000000000000000000000000000000..f747493c4c7ff2a7607956a35c6862f4ef4b2be7 GIT binary patch literal 36097 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 h?)ezq^D(;TV|35Q=$;Q6?D=RO;5{GFvl