Frontend: Implement real terminal with xterm.js (#39)

* Add xterm terminal

* Remove unused code blocks

* Update README.md with terminal documentation

* Update frontend/README.md

Co-authored-by: Graham Neubig <neubig@gmail.com>

---------

Co-authored-by: Graham Neubig <neubig@gmail.com>
This commit is contained in:
Jim Su 2024-03-17 23:33:48 -04:00 committed by GitHub
parent 346e992276
commit cf97b66ff9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 74 additions and 23 deletions

1
frontend/.env Normal file
View File

@ -0,0 +1 @@
REACT_APP_TERMINAL_WS_URL="ws://localhost:8080/ws"

View File

@ -42,3 +42,11 @@ You dont have to ever use `eject`. The curated feature set is suitable for sm
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
## Terminal
The OpenDevin terminal is powered by [Xterm.js](https://github.com/xtermjs/xterm.js).
The terminal listens for events over a WebSocket connection. The WebSocket URL is specified by the environment variable `REACT_APP_TERMINAL_WS_URL` (prepending `REACT_APP_` to environment variable names is necessary to expose them).
A simple websocket server can be found in the `/server` directory.

View File

@ -16,13 +16,16 @@
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/react-syntax-highlighter": "^15.5.11",
"@xterm/xterm": "^5.4.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"react-syntax-highlighter": "^15.5.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
"web-vitals": "^2.1.4",
"xterm-addon-attach": "^0.9.0",
"xterm-addon-fit": "^0.8.0"
},
"devDependencies": {
"@typescript-eslint/parser": "^5.62.0",
@ -4630,6 +4633,11 @@
"@xtuc/long": "4.2.2"
}
},
"node_modules/@xterm/xterm": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.4.0.tgz",
"integrity": "sha512-GlyzcZZ7LJjhFevthHtikhiDIl8lnTSgol6eTM4aoSNLcuXu3OEhnbqdCVIjtIil3jjabf3gDtb1S8FGahsuEw=="
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@ -19116,6 +19124,22 @@
"node": ">=0.4"
}
},
"node_modules/xterm-addon-attach": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/xterm-addon-attach/-/xterm-addon-attach-0.9.0.tgz",
"integrity": "sha512-NykWWOsobVZPPK3P9eFkItrnBK9Lw0f94uey5zhqIVB1bhswdVBfl+uziEzSOhe2h0rT9wD0wOeAYsdSXeavPw==",
"peerDependencies": {
"xterm": "^5.0.0"
}
},
"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==",
"peerDependencies": {
"xterm": "^5.0.0"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -11,13 +11,16 @@
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/react-syntax-highlighter": "^15.5.11",
"@xterm/xterm": "^5.4.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"react-syntax-highlighter": "^15.5.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
"web-vitals": "^2.1.4",
"xterm-addon-attach": "^0.9.0",
"xterm-addon-fit": "^0.8.0"
},
"scripts": {
"start": "react-scripts start",

View File

@ -1,30 +1,45 @@
import React from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { atomDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import React, { useEffect, useRef } from "react";
import { Terminal as XtermTerminal } from "@xterm/xterm";
import { AttachAddon } from "xterm-addon-attach";
import { FitAddon } from "xterm-addon-fit";
import "@xterm/xterm/css/xterm.css";
function Terminal(): JSX.Element {
const terminalOutput = `> chatbot-ui@2.0.0 prepare
> husky install
const terminalRef = useRef<HTMLDivElement>(null);
husky - Git hooks installed
useEffect(() => {
const terminal = new XtermTerminal({
fontFamily: "Menlo, Monaco, 'Courier New', monospace",
fontSize: 14,
});
added 1455 packages, and audited 1456 packages in 1m
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
295 packages are looking for funding
run \`npm fund\` for details
found 0 vulnerabilities
npm notice
npm notice New minor version of npm available! 10.7.3 -> 10.9.0
...`;
terminal.open(terminalRef.current as HTMLDivElement);
return (
<div className="terminal">
<SyntaxHighlighter language="bash" style={atomDark}>
{terminalOutput}
</SyntaxHighlighter>
</div>
);
// Without this timeout, `fitAddon.fit()` throws the error
// "this._renderer.value is undefined"
setTimeout(() => {
fitAddon.fit();
}, 1);
if (!process.env.REACT_APP_TERMINAL_WS_URL) {
throw new Error(
"The environment variable REACT_APP_TERMINAL_WS_URL is not set. Please set it to the WebSocket URL of the terminal server.",
);
}
const attachAddon = new AttachAddon(
new WebSocket(process.env.REACT_APP_TERMINAL_WS_URL as string),
);
terminal.loadAddon(attachAddon);
return () => {
terminal.dispose();
};
}, []);
return <div ref={terminalRef} style={{ width: "100%", height: "100%" }} />;
}
export default Terminal;