From 2108d4bfda61ef6fd8b9be66a2b6daaa11d68621 Mon Sep 17 00:00:00 2001 From: null-runner Date: Sun, 12 Jul 2026 21:06:10 +0200 Subject: [PATCH] fix(dashboard): paste on Ctrl+V and word-delete on Ctrl+Backspace/Delete The dashboard chat is an xterm.js terminal in a browser tab, so several editing keys never reached the input: - Bare Ctrl+V fell through to the TUI, whose server-side clipboard read can't see the browser/OS clipboard, so it reported "No image found in clipboard". Route Ctrl+V through the same navigator.clipboard path as Ctrl+Shift+V (image-or-text). Fixes #24860. - Ctrl+Backspace / Ctrl+Delete never sent a word-delete: xterm.js emits bare DEL regardless of modifier. Send ^W (0x17) and Alt+d (ESC d) to the PTY so readline / prompt_toolkit delete the previous / next word. Ctrl+W itself stays unavailable in a browser tab: it is a reserved shortcut (close tab) that preventDefault cannot suppress. Ctrl+Backspace covers word-delete there; the Electron desktop app can bind Ctrl+W. --- web/src/pages/ChatPage.tsx | 41 +++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/web/src/pages/ChatPage.tsx b/web/src/pages/ChatPage.tsx index 46ae92bb41..3fd98bbd22 100644 --- a/web/src/pages/ChatPage.tsx +++ b/web/src/pages/ChatPage.tsx @@ -670,7 +670,12 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { // react to the keypress. // Paste: Cmd+Shift+V on macOS, Ctrl+Shift+V on others. const copyModifier = isMac ? ev.metaKey : ev.ctrlKey && ev.shiftKey; - const pasteModifier = isMac ? ev.metaKey : ev.ctrlKey && ev.shiftKey; + // Paste on BARE Ctrl+V too (not only Ctrl+Shift+V). Bare Ctrl+V otherwise + // falls through to the TUI, whose server-side clipboard read can't see the + // browser/OS clipboard → "No image found in clipboard". Routing Ctrl+V + // through the same navigator.clipboard path below makes it paste + // image-or-text correctly, like Ctrl+Shift+V. + const pasteModifier = isMac ? ev.metaKey : ev.ctrlKey; if (copyModifier && ev.key.toLowerCase() === "c") { const sel = term.getSelection(); @@ -690,6 +695,40 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { // (or the bare ev if the user used a different modifier). } + // Ctrl+Backspace → delete previous word. xterm.js sends bare DEL + // regardless of modifier, so word-delete never reaches the TUI on its + // own. Send ^W (0x17), which readline / prompt_toolkit treat as + // delete-word-backward. (Ctrl+W can't be used in a browser tab — it's a + // reserved shortcut that closes the tab and preventDefault has no effect; + // for Ctrl+W muscle memory use the Electron desktop app.) + if ( + ev.ctrlKey && + !ev.shiftKey && + !ev.altKey && + !ev.metaKey && + ev.key === "Backspace" + ) { + ev.preventDefault(); + const ws = wsRef.current; + if (ws && ws.readyState === WebSocket.OPEN) ws.send("\x17"); + return false; + } + + // Ctrl+Delete → delete next word. Mirror of Ctrl+Backspace; sends Alt+d + // (ESC d), the readline / prompt_toolkit kill-word-forward binding. + if ( + ev.ctrlKey && + !ev.shiftKey && + !ev.altKey && + !ev.metaKey && + ev.key === "Delete" + ) { + ev.preventDefault(); + const ws = wsRef.current; + if (ws && ws.readyState === WebSocket.OPEN) ws.send("\x1bd"); + return false; + } + if (pasteModifier && ev.key.toLowerCase() === "v") { // preventDefault suppresses the DOM paste event, so image paste must // be handled here via clipboard.read() — readText() alone misses