浏览代码

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>
Jim Su 2 年之前
父节点
当前提交
cf97b66ff9
共有 5 个文件被更改,包括 78 次插入27 次删除
  1. 1 0
      frontend/.env
  2. 8 0
      frontend/README.md
  3. 25 1
      frontend/package-lock.json
  4. 4 1
      frontend/package.json
  5. 40 25
      frontend/src/components/Terminal.tsx

+ 1 - 0
frontend/.env

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

+ 8 - 0
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.

+ 25 - 1
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",

+ 4 - 1
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",

+ 40 - 25
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
-
-husky - Git hooks installed
-
-added 1455 packages, and audited 1456 packages in 1m
-
-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
-...`;
-
-  return (
-    <div className="terminal">
-      <SyntaxHighlighter language="bash" style={atomDark}>
-        {terminalOutput}
-      </SyntaxHighlighter>
-    </div>
-  );
+  const terminalRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    const terminal = new XtermTerminal({
+      fontFamily: "Menlo, Monaco, 'Courier New', monospace",
+      fontSize: 14,
+    });
+
+    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);
+
+    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;