From 84380a17ab73266bfc0b76c612a70c4c8062c232 Mon Sep 17 00:00:00 2001 From: Vinicios Rabaioli Date: Fri, 15 May 2026 01:37:54 -0300 Subject: [PATCH 1/3] fix(terminal): restore clipboard shortcuts and suppress ask popup --- src/app/App.tsx | 6 +- src/modules/terminal/lib/rendererPool.ts | 76 +++++++++++++++++++++++- 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index adfc0c72..06a320cf 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -429,6 +429,10 @@ export default function App() { }; const onUp = (e: MouseEvent) => { if (isInsideAi(e.target)) return; + if (activeTab?.kind !== "editor") { + setAskPopup(null); + return; + } // Defer one tick so xterm/CodeMirror finalize the selection. setTimeout(() => { const text = captureActiveSelection(); @@ -446,7 +450,7 @@ export default function App() { document.removeEventListener("mousedown", onDown); document.removeEventListener("mouseup", onUp); }; - }, [captureActiveSelection]); + }, [activeTab?.kind, captureActiveSelection]); const onAskFromSelection = useCallback(() => { askFromSelection(); diff --git a/src/modules/terminal/lib/rendererPool.ts b/src/modules/terminal/lib/rendererPool.ts index ecca0a18..62cbcc62 100644 --- a/src/modules/terminal/lib/rendererPool.ts +++ b/src/modules/terminal/lib/rendererPool.ts @@ -1,4 +1,5 @@ import { detectMonoFontFamily } from "@/lib/fonts"; +import { IS_LINUX, IS_MAC, IS_WINDOWS } from "@/lib/platform"; import { usePreferencesStore } from "@/modules/settings/preferences"; import { buildTerminalTheme } from "@/styles/terminalTheme"; import { openUrl } from "@tauri-apps/plugin-opener"; @@ -105,6 +106,7 @@ function createSlot(): Slot { host.setAttribute("data-terax-slot", String(slots.length)); getRecycler().appendChild(host); term.open(host); + attachTerminalPasteHandler(term); const slot: Slot = { id: slots.length, @@ -135,6 +137,9 @@ function createSlot(): Slot { if (leafId === null) return false; const bridge = adapter?.resolveLeaf(leafId); if (!bridge) return true; + if (handleClipboardShortcut(term, event)) { + return false; + } if (isCtrlBackspace(event)) { event.preventDefault(); if (event.type === "keydown") bridge.writeToPty("\x17"); @@ -555,9 +560,7 @@ export function getSlotForLeaf(leafId: number): Slot | null { } function isCtrlBackspace(e: KeyboardEvent): boolean { - const ua = typeof navigator !== "undefined" ? navigator.userAgent : ""; - const isMac = /Mac|iPhone|iPad/.test(ua); - const mod = isMac ? e.metaKey : e.ctrlKey; + const mod = IS_MAC ? e.metaKey : e.ctrlKey; return mod && (e.key === "Backspace" || e.code === "Backspace"); } @@ -566,3 +569,70 @@ function isShiftEnter(e: KeyboardEvent): boolean { e.key === "Enter" && e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey ); } + +function attachTerminalPasteHandler(term: Terminal): void { + const element = term.element; + if (!element) return; + element.addEventListener("paste", (event) => { + const text = event.clipboardData?.getData("text/plain") ?? ""; + if (!text) return; + event.preventDefault(); + term.paste(text); + }); +} + +function handleClipboardShortcut(term: Terminal, event: KeyboardEvent): boolean { + if (event.type !== "keydown") return false; + if (IS_WINDOWS && isCtrlV(event)) { + event.preventDefault(); + void pasteFromClipboard(term); + return true; + } + if (isCopyShortcut(event)) { + return copySelectionIfPresent(term, event); + } + return false; +} + +function isCopyShortcut(event: KeyboardEvent): boolean { + if (IS_WINDOWS) return isCtrlC(event); + if (IS_MAC) return isCmdC(event); + if (IS_LINUX) return isCtrlShiftC(event); + return false; +} + +function copySelectionIfPresent(term: Terminal, event: KeyboardEvent): boolean { + const selection = term.getSelection(); + if (!selection) return false; + event.preventDefault(); + void navigator.clipboard.writeText(selection).catch((error) => { + console.warn("[terax] terminal copy failed:", error); + }); + return true; +} + +async function pasteFromClipboard(term: Terminal): Promise { + try { + const text = await navigator.clipboard.readText(); + if (!text) return; + term.paste(text); + } catch (error) { + console.warn("[terax] terminal paste failed:", error); + } +} + +function isCtrlC(event: KeyboardEvent): boolean { + return event.key.toLowerCase() === "c" && event.ctrlKey && !event.altKey && !event.metaKey; +} + +function isCmdC(event: KeyboardEvent): boolean { + return event.key.toLowerCase() === "c" && event.metaKey && !event.altKey && !event.ctrlKey; +} + +function isCtrlV(event: KeyboardEvent): boolean { + return event.key.toLowerCase() === "v" && event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey; +} + +function isCtrlShiftC(event: KeyboardEvent): boolean { + return event.key.toLowerCase() === "c" && event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey; +} From 686bf3e6c276e18564e8bbdb8b2d4a3ead29a47f Mon Sep 17 00:00:00 2001 From: Vinicios Rabaioli Date: Fri, 15 May 2026 01:57:40 -0300 Subject: [PATCH 2/3] fix(terminal): address PR review feedback --- src/app/App.tsx | 6 +++-- src/modules/terminal/lib/rendererPool.ts | 29 +++++++++++++++++------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index 06a320cf..61b3813a 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -232,6 +232,8 @@ export default function App() { }, [hydrateSessions]); const activeTab = tabs.find((t) => t.id === activeId); + const activeTabKindRef = useRef(activeTab?.kind ?? null); + activeTabKindRef.current = activeTab?.kind ?? null; const isTerminalTab = activeTab?.kind === "terminal"; const isEditorTab = activeTab?.kind === "editor"; const isPreviewTab = activeTab?.kind === "preview"; @@ -429,7 +431,7 @@ export default function App() { }; const onUp = (e: MouseEvent) => { if (isInsideAi(e.target)) return; - if (activeTab?.kind !== "editor") { + if (activeTabKindRef.current !== "editor") { setAskPopup(null); return; } @@ -450,7 +452,7 @@ export default function App() { document.removeEventListener("mousedown", onDown); document.removeEventListener("mouseup", onUp); }; - }, [activeTab?.kind, captureActiveSelection]); + }, [captureActiveSelection]); const onAskFromSelection = useCallback(() => { askFromSelection(); diff --git a/src/modules/terminal/lib/rendererPool.ts b/src/modules/terminal/lib/rendererPool.ts index 62cbcc62..72776612 100644 --- a/src/modules/terminal/lib/rendererPool.ts +++ b/src/modules/terminal/lib/rendererPool.ts @@ -8,7 +8,7 @@ import { SearchAddon } from "@xterm/addon-search"; import { SerializeAddon } from "@xterm/addon-serialize"; import { WebLinksAddon } from "@xterm/addon-web-links"; import { WebglAddon } from "@xterm/addon-webgl"; -import { Terminal } from "@xterm/xterm"; +import { Terminal, type ITerminalAddon } from "@xterm/xterm"; export const POOL_MAX_SIZE = 4; const FIT_DEBOUNCE_MS = 8; @@ -106,7 +106,7 @@ function createSlot(): Slot { host.setAttribute("data-terax-slot", String(slots.length)); getRecycler().appendChild(host); term.open(host); - attachTerminalPasteHandler(term); + term.loadAddon(new TerminalPasteAddon()); const slot: Slot = { id: slots.length, @@ -570,15 +570,28 @@ function isShiftEnter(e: KeyboardEvent): boolean { ); } -function attachTerminalPasteHandler(term: Terminal): void { - const element = term.element; - if (!element) return; - element.addEventListener("paste", (event) => { +class TerminalPasteAddon implements ITerminalAddon { + private term: Terminal | null = null; + private element: HTMLElement | null = null; + + private readonly onPaste = (event: ClipboardEvent) => { const text = event.clipboardData?.getData("text/plain") ?? ""; if (!text) return; event.preventDefault(); - term.paste(text); - }); + this.term?.paste(text); + }; + + activate(term: Terminal): void { + this.term = term; + this.element = term.element ?? null; + this.element?.addEventListener("paste", this.onPaste); + } + + dispose(): void { + this.element?.removeEventListener("paste", this.onPaste); + this.element = null; + this.term = null; + } } function handleClipboardShortcut(term: Terminal, event: KeyboardEvent): boolean { From a9e56155363054a6b528e571875f14c16898e360 Mon Sep 17 00:00:00 2001 From: Vinicios Rabaioli Date: Fri, 15 May 2026 02:10:40 -0300 Subject: [PATCH 3/3] fix(terminal): harden clipboard copy handling --- src/modules/terminal/lib/rendererPool.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/modules/terminal/lib/rendererPool.ts b/src/modules/terminal/lib/rendererPool.ts index 72776612..16532804 100644 --- a/src/modules/terminal/lib/rendererPool.ts +++ b/src/modules/terminal/lib/rendererPool.ts @@ -618,12 +618,18 @@ function copySelectionIfPresent(term: Terminal, event: KeyboardEvent): boolean { const selection = term.getSelection(); if (!selection) return false; event.preventDefault(); - void navigator.clipboard.writeText(selection).catch((error) => { - console.warn("[terax] terminal copy failed:", error); - }); + void copySelectionToClipboard(selection); return true; } +async function copySelectionToClipboard(selection: string): Promise { + try { + await navigator.clipboard.writeText(selection); + } catch (error) { + console.warn("[terax] terminal copy failed:", error); + } +} + async function pasteFromClipboard(term: Terminal): Promise { try { const text = await navigator.clipboard.readText();