mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
chore(frontend): Make terminal read-only by removing user input handlers (#11546)
This commit is contained in:
parent
ca2c9546ad
commit
a196881ab0
@ -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(),
|
||||
};
|
||||
|
||||
|
||||
@ -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(),
|
||||
}));
|
||||
|
||||
|
||||
@ -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<Terminal | null>(null);
|
||||
const fitAddon = React.useRef<FitAddon | null>(null);
|
||||
const ref = React.useRef<HTMLDivElement>(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;
|
||||
};
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user