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.
This commit is contained in:
null-runner
2026-07-12 21:06:10 +02:00
committed by Teknium
parent c21dc294b2
commit 2108d4bfda

View File

@@ -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