mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
This reverts commit 27246aca7e0f3d399740db466f31026c891a5374.
This commit is contained in:
parent
e7b5ddfe06
commit
a1a0767681
28
frontend/package-lock.json
generated
28
frontend/package-lock.json
generated
@ -13,7 +13,6 @@
|
||||
"@react-types/shared": "^3.22.1",
|
||||
"@reduxjs/toolkit": "^2.2.2",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"clsx": "^2.1.0",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
@ -33,7 +32,8 @@
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"vite": "^5.1.6",
|
||||
"web-vitals": "^2.1.4"
|
||||
"web-vitals": "^2.1.4",
|
||||
"xterm-addon-fit": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
@ -5247,14 +5247,6 @@
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@xterm/addon-fit": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
|
||||
"integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
|
||||
"peerDependencies": {
|
||||
"@xterm/xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/xterm": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
@ -14268,6 +14260,22 @@
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/xterm": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
|
||||
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
|
||||
"deprecated": "This package is now deprecated. Move to @xterm/xterm instead.",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/xterm-addon-fit": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz",
|
||||
"integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==",
|
||||
"deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.",
|
||||
"peerDependencies": {
|
||||
"xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
"@react-types/shared": "^3.22.1",
|
||||
"@reduxjs/toolkit": "^2.2.2",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"clsx": "^2.1.0",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
@ -32,7 +31,8 @@
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"vite": "^5.1.6",
|
||||
"web-vitals": "^2.1.4"
|
||||
"web-vitals": "^2.1.4",
|
||||
"xterm-addon-fit": "^0.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
|
||||
@ -2,11 +2,8 @@ import { useDisclosure } from "@nextui-org/react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import CogTooth from "#/assets/cog-tooth";
|
||||
import AgentControlBar from "#/components/AgentControlBar";
|
||||
import AgentStatusBar from "#/components/AgentStatusBar";
|
||||
import ChatInterface from "#/components/ChatInterface";
|
||||
import Errors from "#/components/Errors";
|
||||
import Terminal from "#/components/terminal/Terminal";
|
||||
import { Container, Orientation } from "#/components/Resizable";
|
||||
import Workspace from "#/components/Workspace";
|
||||
import LoadPreviousSessionModal from "#/components/modals/load-previous-session/LoadPreviousSessionModal";
|
||||
@ -16,6 +13,9 @@ import { initializeAgent } from "#/services/settingsService";
|
||||
import Socket from "#/services/socket";
|
||||
import { ResFetchMsgTotal } from "#/types/ResponseType";
|
||||
import "./App.css";
|
||||
import AgentControlBar from "./components/AgentControlBar";
|
||||
import AgentStatusBar from "./components/AgentStatusBar";
|
||||
import Terminal from "./components/Terminal";
|
||||
|
||||
interface Props {
|
||||
setSettingOpen: (isOpen: boolean) => void;
|
||||
|
||||
115
frontend/src/components/Terminal.tsx
Normal file
115
frontend/src/components/Terminal.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import { IDisposable, Terminal as XtermTerminal } from "@xterm/xterm";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { VscTerminal } from "react-icons/vsc";
|
||||
import { useSelector } from "react-redux";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import Socket from "#/services/socket";
|
||||
import { RootState } from "#/store";
|
||||
import ActionType from "#/types/ActionType";
|
||||
import ObservationType from "#/types/ObservationType";
|
||||
|
||||
class JsonWebsocketAddon {
|
||||
_disposables: IDisposable[];
|
||||
|
||||
constructor() {
|
||||
this._disposables = [];
|
||||
}
|
||||
|
||||
activate(terminal: XtermTerminal) {
|
||||
this._disposables.push(
|
||||
terminal.onData((data) => {
|
||||
const payload = JSON.stringify({ action: "terminal", data });
|
||||
Socket.send(payload);
|
||||
}),
|
||||
);
|
||||
Socket.addEventListener("message", (event) => {
|
||||
const { action, args, observation, content } = JSON.parse(event.data);
|
||||
if (action === ActionType.RUN) {
|
||||
terminal.writeln(args.command);
|
||||
}
|
||||
if (observation === ObservationType.RUN) {
|
||||
content.split("\n").forEach((line: string) => {
|
||||
terminal.writeln(line);
|
||||
});
|
||||
terminal.write("\n$ ");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables.forEach((d) => d.dispose());
|
||||
Socket.removeEventListener("message", () => {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The terminal's content is set by write messages. To avoid complicated state logic,
|
||||
* we keep the terminal persistently open as a child of <App /> and hidden when not in use.
|
||||
*/
|
||||
|
||||
function Terminal(): JSX.Element {
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
const { commands } = useSelector((state: RootState) => state.cmd);
|
||||
|
||||
useEffect(() => {
|
||||
const terminal = new XtermTerminal({
|
||||
// This value is set to the appropriate value by the
|
||||
// `fitAddon.fit()` call below.
|
||||
// If not set here, the terminal does not respect the width
|
||||
// of its parent element. This causes a bug where the terminal
|
||||
// is too large and switching tabs causes a layout shift.
|
||||
cols: 0,
|
||||
fontFamily: "Menlo, Monaco, 'Courier New', monospace",
|
||||
fontSize: 14,
|
||||
theme: {
|
||||
background: "#262626",
|
||||
},
|
||||
});
|
||||
terminal.write("$ ");
|
||||
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
|
||||
terminal.open(terminalRef.current as HTMLDivElement);
|
||||
|
||||
// Without this timeout, `fitAddon.fit()` throws the error
|
||||
// "this._renderer.value is undefined"
|
||||
setTimeout(() => {
|
||||
fitAddon.fit();
|
||||
}, 1);
|
||||
|
||||
const jsonWebsocketAddon = new JsonWebsocketAddon();
|
||||
terminal.loadAddon(jsonWebsocketAddon);
|
||||
|
||||
// FIXME, temporary solution to display the terminal,
|
||||
// but it will rerender the terminal every time the commands change
|
||||
commands.forEach((command) => {
|
||||
if (command.type === "input") {
|
||||
terminal.writeln(command.content);
|
||||
} else {
|
||||
command.content.split("\n").forEach((line: string) => {
|
||||
terminal.writeln(line);
|
||||
});
|
||||
terminal.write("\n$ ");
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
terminal.dispose();
|
||||
};
|
||||
}, [commands]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center gap-2 px-4 py-2 text-sm border-b border-neutral-600">
|
||||
<VscTerminal />
|
||||
Terminal
|
||||
</div>
|
||||
<div className="grow p-2 flex min-h-0">
|
||||
<div ref={terminalRef} className="h-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Terminal;
|
||||
@ -1,3 +0,0 @@
|
||||
import { describe } from "vitest";
|
||||
|
||||
describe.todo("Terminal");
|
||||
@ -1,34 +0,0 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { VscTerminal } from "react-icons/vsc";
|
||||
import { RootState } from "#/store";
|
||||
import useXTerm from "../../hooks/useXTerm";
|
||||
|
||||
function Terminal() {
|
||||
const { commands } = useSelector((state: RootState) => state.cmd);
|
||||
|
||||
const xtermRef = useXTerm({
|
||||
commands,
|
||||
options: {
|
||||
fontFamily: "Menlo, Monaco, 'Courier New', monospace",
|
||||
fontSize: 14,
|
||||
theme: {
|
||||
background: "#262626",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center gap-2 px-4 py-2 text-sm border-b border-neutral-600">
|
||||
<VscTerminal />
|
||||
Terminal (read-only)
|
||||
</div>
|
||||
<div className="grow p-2 flex min-h-0">
|
||||
<div ref={xtermRef} className="h-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Terminal;
|
||||
@ -1,3 +0,0 @@
|
||||
import { describe } from "vitest";
|
||||
|
||||
describe.todo("useXTerm");
|
||||
@ -1,84 +0,0 @@
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import {
|
||||
ITerminalAddon,
|
||||
ITerminalInitOnlyOptions,
|
||||
ITerminalOptions,
|
||||
Terminal,
|
||||
} from "@xterm/xterm";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import React from "react";
|
||||
|
||||
type CommandType = "input" | "output";
|
||||
type Command = { type: CommandType; content: string };
|
||||
|
||||
interface XTermProps {
|
||||
options?: ITerminalOptions & ITerminalInitOnlyOptions;
|
||||
commands?: Command[];
|
||||
addons?: ITerminalAddon[];
|
||||
}
|
||||
|
||||
function useXTerm({ options, commands, addons }: XTermProps) {
|
||||
const [terminal, setTerminal] = React.useState<Terminal | null>(null);
|
||||
const [fitAddon] = React.useState<FitAddon>(new FitAddon());
|
||||
const xtermRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
/* Create a new terminal instance */
|
||||
const xterm = new Terminal(options);
|
||||
xterm.loadAddon(fitAddon);
|
||||
addons?.forEach((addon) => xterm.loadAddon(addon));
|
||||
|
||||
setTerminal(xterm);
|
||||
|
||||
return () => {
|
||||
xterm.dispose();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
/* Open the terminal in the DOM */
|
||||
if (terminal && xtermRef.current) {
|
||||
terminal.open(xtermRef.current);
|
||||
}
|
||||
}, [terminal, fitAddon]);
|
||||
|
||||
React.useEffect(() => {
|
||||
/* Write commands to the terminal */
|
||||
if (terminal && commands) {
|
||||
commands.forEach((command) => {
|
||||
if (command.type === "input") {
|
||||
terminal.write("$ ");
|
||||
terminal.write(`${command.content}\r`); // \r is needed to move the cursor to the beginning of the line to prevent tabbing the next line
|
||||
} else {
|
||||
terminal.writeln(command.content);
|
||||
}
|
||||
|
||||
terminal.write("\n");
|
||||
});
|
||||
|
||||
terminal.write("$ ");
|
||||
}
|
||||
}, [terminal, commands]);
|
||||
|
||||
React.useEffect(() => {
|
||||
/* Resize the terminal when the window is resized */
|
||||
let resizeObserver: ResizeObserver;
|
||||
|
||||
if (xtermRef.current) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
fitAddon.fit();
|
||||
});
|
||||
|
||||
resizeObserver.observe(xtermRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [fitAddon]);
|
||||
|
||||
return xtermRef;
|
||||
}
|
||||
|
||||
export default useXTerm;
|
||||
Loading…
x
Reference in New Issue
Block a user