From 07bba99eb5e3e8a71db8e8d723bf2aea1089bd4e Mon Sep 17 00:00:00 2001 From: megaproxy Date: Thu, 28 May 2026 21:31:59 +0100 Subject: [PATCH] Use canvas renderer to fix stuck/ghost cursor in panes The DOM renderer draws the cursor as a separate layered element; under the Claude TUI's rapid cursor hide/show plus cursorBlink it leaves a stale white block frozen where the cursor used to be. Load @xterm/addon-canvas (composites the cursor into the text surface) with a try/catch that falls back to the DOM renderer on init failure. Canvas over WebGL because tiletopia runs many panes and WebView2 caps live WebGL contexts (~16). Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 1 + src/components/XtermPane.tsx | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/package.json b/package.json index 26121e1..393ccb7 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@tauri-apps/api": "^2.0.0", "@tauri-apps/plugin-clipboard-manager": "^2.0.0", "@tauri-apps/plugin-opener": "^2.0.0", + "@xterm/addon-canvas": "^0.7.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^5.5.0", diff --git a/src/components/XtermPane.tsx b/src/components/XtermPane.tsx index de4309a..1e76493 100644 --- a/src/components/XtermPane.tsx +++ b/src/components/XtermPane.tsx @@ -2,6 +2,7 @@ import { useRef, useEffect } from "react"; import { Terminal } from "@xterm/xterm"; import { FitAddon } from "@xterm/addon-fit"; import { WebLinksAddon } from "@xterm/addon-web-links"; +import { CanvasAddon } from "@xterm/addon-canvas"; import type { UnlistenFn } from "@tauri-apps/api/event"; import { readText as clipboardReadText, @@ -149,6 +150,23 @@ export default function XtermPane({ ); term.open(container); + // Use the canvas renderer instead of xterm's default DOM renderer. + // The DOM renderer draws the cursor as a separate layered element and, + // under the Claude TUI's rapid hide/show (\x1b[?25l/h) + cursorBlink, + // leaves a stale cursor block frozen where the cursor used to be (the + // "stuck white marker"). The canvas renderer composites the cursor into + // the same surface as the text, so hide/show transitions clear cleanly. + // Chosen over the WebGL addon because tiletopia runs many panes at once + // and Chromium/WebView2 caps live WebGL contexts (~16) — canvas has no + // such hard limit. Loaded after open() so the core renderer exists. + try { + term.loadAddon(new CanvasAddon()); + } catch (e) { + // If canvas init fails for any reason, xterm falls back to the DOM + // renderer on its own — degrade gracefully rather than blank the pane. + console.warn("CanvasAddon load failed; using DOM renderer:", e); + } + // Initial size — fit before asking the PTY for its dimensions. fit.fit();