From a196881ab0202439f2a2ff50f927b5403bba6d18 Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Wed, 29 Oct 2025 21:30:10 +0400 Subject: [PATCH] chore(frontend): Make terminal read-only by removing user input handlers (#11546) --- .../components/terminal/terminal.test.tsx | 3 +- .../__tests__/hooks/use-terminal.test.tsx | 3 +- frontend/src/hooks/use-terminal.ts | 127 ++---------------- frontend/src/i18n/translation.json | 28 ++-- 4 files changed, 26 insertions(+), 135 deletions(-) diff --git a/frontend/__tests__/components/terminal/terminal.test.tsx b/frontend/__tests__/components/terminal/terminal.test.tsx index 8224bd6251..15fb6357b2 100644 --- a/frontend/__tests__/components/terminal/terminal.test.tsx +++ b/frontend/__tests__/components/terminal/terminal.test.tsx @@ -11,6 +11,7 @@ const renderTerminal = (commands: Command[] = []) => { }; describe.skip("Terminal", () => { + // Terminal is now read-only - no user input functionality global.ResizeObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), disconnect: vi.fn(), @@ -21,8 +22,6 @@ describe.skip("Terminal", () => { write: vi.fn(), writeln: vi.fn(), dispose: vi.fn(), - onKey: vi.fn(), - attachCustomKeyEventHandler: vi.fn(), loadAddon: vi.fn(), }; diff --git a/frontend/__tests__/hooks/use-terminal.test.tsx b/frontend/__tests__/hooks/use-terminal.test.tsx index 3988c43102..4f110df171 100644 --- a/frontend/__tests__/hooks/use-terminal.test.tsx +++ b/frontend/__tests__/hooks/use-terminal.test.tsx @@ -35,13 +35,12 @@ function TestTerminalComponent() { } describe("useTerminal", () => { + // Terminal is read-only - no longer tests user input functionality const mockTerminal = vi.hoisted(() => ({ loadAddon: vi.fn(), open: vi.fn(), write: vi.fn(), writeln: vi.fn(), - onKey: vi.fn(), - attachCustomKeyEventHandler: vi.fn(), dispose: vi.fn(), })); diff --git a/frontend/src/hooks/use-terminal.ts b/frontend/src/hooks/use-terminal.ts index ccc53e5a01..224feac1bf 100644 --- a/frontend/src/hooks/use-terminal.ts +++ b/frontend/src/hooks/use-terminal.ts @@ -2,11 +2,7 @@ import { FitAddon } from "@xterm/addon-fit"; import { Terminal } from "@xterm/xterm"; import React from "react"; import { Command, useCommandStore } from "#/state/command-store"; -import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state"; -import { getTerminalCommand } from "#/services/terminal-service"; import { parseTerminalOutput } from "#/utils/parse-terminal-output"; -import { useSendMessage } from "#/hooks/use-send-message"; -import { useAgentState } from "#/hooks/use-agent-state"; /* NOTE: Tests for this hook are indirectly covered by the tests for the XTermTerminal component. @@ -38,15 +34,11 @@ const renderCommand = ( const persistentLastCommandIndex = { current: 0 }; export const useTerminal = () => { - const { send } = useSendMessage(); - const { curAgentState } = useAgentState(); const commands = useCommandStore((state) => state.commands); const terminal = React.useRef(null); const fitAddon = React.useRef(null); const ref = React.useRef(null); const lastCommandIndex = persistentLastCommandIndex; // Use the persistent reference - const keyEventDisposable = React.useRef<{ dispose: () => void } | null>(null); - const disabled = RUNTIME_INACTIVE_STATES.includes(curAgentState); const createTerminal = () => new Terminal({ @@ -57,6 +49,7 @@ export const useTerminal = () => { fastScrollModifier: "alt", fastScrollSensitivity: 5, allowTransparency: true, + disableStdin: true, // Make terminal read-only theme: { background: "transparent", }, @@ -65,55 +58,12 @@ export const useTerminal = () => { const initializeTerminal = () => { if (terminal.current) { if (fitAddon.current) terminal.current.loadAddon(fitAddon.current); - if (ref.current) terminal.current.open(ref.current); - } - }; - - const copySelection = (selection: string) => { - const clipboardItem = new ClipboardItem({ - "text/plain": new Blob([selection], { type: "text/plain" }), - }); - - navigator.clipboard.write([clipboardItem]); - }; - - const pasteSelection = (callback: (text: string) => void) => { - navigator.clipboard.readText().then(callback); - }; - - const pasteHandler = (event: KeyboardEvent, cb: (text: string) => void) => { - const isControlOrMetaPressed = - event.type === "keydown" && (event.ctrlKey || event.metaKey); - - if (isControlOrMetaPressed) { - if (event.code === "KeyV") { - pasteSelection((text: string) => { - terminal.current?.write(text); - cb(text); - }); - } - - if (event.code === "KeyC") { - const selection = terminal.current?.getSelection(); - if (selection) copySelection(selection); + if (ref.current) { + terminal.current.open(ref.current); + // Hide cursor for read-only terminal using ANSI escape sequence + terminal.current.write("\x1b[?25l"); } } - - return true; - }; - - const handleEnter = (command: string) => { - terminal.current?.write("\r\n"); - // Don't write the command again as it will be added to the commands array - // and rendered by the useEffect that watches commands - send(getTerminalCommand(command)); - // Don't add the prompt here as it will be added when the command is processed - // and the commands array is updated - }; - - const handleBackspace = (command: string) => { - terminal.current?.write("\b \b"); - return command.slice(0, -1); }; // Initialize terminal and handle cleanup @@ -136,7 +86,7 @@ export const useTerminal = () => { } lastCommandIndex.current = commands.length; } - terminal.current.write("$ "); + // Don't show prompt in read-only terminal } return () => { @@ -150,19 +100,17 @@ export const useTerminal = () => { commands.length > 0 && lastCommandIndex.current < commands.length ) { - let lastCommandType = ""; for (let i = lastCommandIndex.current; i < commands.length; i += 1) { - lastCommandType = commands[i].type; + if (commands[i].type === "input") { + terminal.current.write("$ "); + } // Pass true for isUserInput to skip rendering user input commands // that have already been displayed as the user typed renderCommand(commands[i], terminal.current, false); } lastCommandIndex.current = commands.length; - if (lastCommandType === "output") { - terminal.current.write("$ "); - } } - }, [commands, disabled]); + }, [commands]); React.useEffect(() => { let resizeObserver: ResizeObserver | null = null; @@ -180,60 +128,5 @@ export const useTerminal = () => { }; }, []); - React.useEffect(() => { - if (terminal.current) { - // Dispose of existing listeners if they exist - if (keyEventDisposable.current) { - keyEventDisposable.current.dispose(); - keyEventDisposable.current = null; - } - - let commandBuffer = ""; - - if (!disabled) { - // Add new key event listener and store the disposable - keyEventDisposable.current = terminal.current.onKey( - ({ key, domEvent }) => { - if (domEvent.key === "Enter") { - handleEnter(commandBuffer); - commandBuffer = ""; - } else if (domEvent.key === "Backspace") { - if (commandBuffer.length > 0) { - commandBuffer = handleBackspace(commandBuffer); - } - } else { - // Ignore paste event - if (key.charCodeAt(0) === 22) { - return; - } - commandBuffer += key; - terminal.current?.write(key); - } - }, - ); - - // Add custom key handler and store the disposable - terminal.current.attachCustomKeyEventHandler((event) => - pasteHandler(event, (text) => { - commandBuffer += text; - }), - ); - } else { - // Add a noop handler when disabled - keyEventDisposable.current = terminal.current.onKey((e) => { - e.domEvent.preventDefault(); - e.domEvent.stopPropagation(); - }); - } - } - - return () => { - if (keyEventDisposable.current) { - keyEventDisposable.current.dispose(); - keyEventDisposable.current = null; - } - }; - }, [disabled]); - return ref; }; diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index ff51ba06f0..c8b36276f7 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -14320,20 +14320,20 @@ "uk": "Зупинити сервер" }, "COMMON$TERMINAL": { - "en": "Terminal", - "ja": "ターミナル", - "zh-CN": "终端", - "zh-TW": "終端機", - "ko-KR": "터미널", - "no": "Terminal", - "it": "Terminale", - "pt": "Terminal", - "es": "Terminal", - "ar": "الطرفية", - "fr": "Terminal", - "tr": "Terminal", - "de": "Terminal", - "uk": "Термінал" + "en": "Terminal (read-only)", + "ja": "ターミナル (読み取り専用)", + "zh-CN": "终端(只读)", + "zh-TW": "終端機(唯讀)", + "ko-KR": "터미널 (읽기 전용)", + "no": "Terminal (skrivebeskyttet)", + "it": "Terminale (sola lettura)", + "pt": "Terminal (somente leitura)", + "es": "Terminal (solo lectura)", + "ar": "الطرفية (للقراءة فقط)", + "fr": "Terminal (lecture seule)", + "tr": "Terminal (salt okunur)", + "de": "Terminal (schreibgeschützt)", + "uk": "Термінал (тільки читання)" }, "COMMON$UNKNOWN": { "en": "Unknown",