diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000000..36548828d0 --- /dev/null +++ b/frontend/.env @@ -0,0 +1 @@ +REACT_APP_TERMINAL_WS_URL="ws://localhost:8080/ws" \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md index 5e85d073c2..9d65735a62 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -42,3 +42,11 @@ You don’t 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. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 622dc2648d..6894f0c2db 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 4b75b9ebc6..5a2464e72a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/components/Terminal.tsx b/frontend/src/components/Terminal.tsx index 5eb1b0a977..4d09b87e2d 100644 --- a/frontend/src/components/Terminal.tsx +++ b/frontend/src/components/Terminal.tsx @@ -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(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 ( -
- - {terminalOutput} - -
- ); + // 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
; } export default Terminal;