Эх сурвалжийг харах

Interactive Terminal (#2493)

* Interactive Terminal

* linted

* fixed tests

* fixed tests

* refactored logic

* remove console logs
மனோஜ்குமார் பழனிச்சாமி 1 жил өмнө
parent
commit
c743320201

+ 1 - 1
frontend/src/components/terminal/Terminal.tsx

@@ -14,7 +14,7 @@ function Terminal() {
     <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)
+        Terminal
       </div>
       <div className="grow p-2 flex min-h-0">
         <div ref={ref} className="h-full w-full" />

+ 40 - 0
frontend/src/hooks/useTerminal.ts

@@ -2,6 +2,7 @@ import { FitAddon } from "@xterm/addon-fit";
 import { Terminal } from "@xterm/xterm";
 import React from "react";
 import { Command } from "#/state/commandSlice";
+import { sendTerminalCommand } from "#/services/terminalService";
 
 /*
   NOTE: Tests for this hook are indirectly covered by the tests for the XTermTerminal component.
@@ -26,6 +27,7 @@ export const useTerminal = (commands: Command[] = []) => {
     fitAddon.current = new FitAddon();
 
     let resizeObserver: ResizeObserver;
+    let commandBuffer = "";
 
     if (ref.current) {
       /* Initialize the terminal in the DOM */
@@ -33,6 +35,44 @@ export const useTerminal = (commands: Command[] = []) => {
       terminal.current.open(ref.current);
 
       terminal.current.write("$ ");
+      terminal.current.onKey(({ key, domEvent }) => {
+        if (domEvent.key === "Enter") {
+          terminal.current?.write("\r\n");
+          sendTerminalCommand(commandBuffer);
+          commandBuffer = "";
+        } else if (domEvent.key === "Backspace") {
+          if (commandBuffer.length > 0) {
+            commandBuffer = commandBuffer.slice(0, -1);
+            terminal.current?.write("\b \b");
+          }
+        } else {
+          // Ignore paste event
+          if (key.charCodeAt(0) === 22) {
+            return;
+          }
+          commandBuffer += key;
+          terminal.current?.write(key);
+        }
+      });
+      terminal.current.attachCustomKeyEventHandler((arg) => {
+        if (arg.ctrlKey && arg.code === "KeyV" && arg.type === "keydown") {
+          navigator.clipboard.readText().then((text) => {
+            terminal.current?.write(text);
+            commandBuffer += text;
+          });
+        }
+        if (arg.ctrlKey && arg.code === "KeyC" && arg.type === "keydown") {
+          const selection = terminal.current?.getSelection();
+          if (selection) {
+            const clipboardItem = new ClipboardItem({
+              "text/plain": new Blob([selection], { type: "text/plain" }),
+            });
+
+            navigator.clipboard.write([clipboardItem]);
+          }
+        }
+        return true;
+      });
 
       /* Listen for resize events */
       resizeObserver = new ResizeObserver(() => {

+ 8 - 0
frontend/src/services/terminalService.ts

@@ -0,0 +1,8 @@
+import ActionType from "#/types/ActionType";
+import Session from "./session";
+
+export function sendTerminalCommand(command: string): void {
+  const event = { action: ActionType.RUN, args: { command } };
+  const eventString = JSON.stringify(event);
+  Session.send(eventString);
+}

+ 3 - 1
opendevin/events/stream.py

@@ -95,6 +95,7 @@ class EventStream:
 
     # TODO: make this not async
     async def add_event(self, event: Event, source: EventSource):
+        logger.debug(f'Adding event {event} from {source}')
         async with self._lock:
             event._id = self._cur_id  # type: ignore [attr-defined]
             self._cur_id += 1
@@ -105,6 +106,7 @@ class EventStream:
             self._file_store.write(
                 self._get_filename_for_id(event.id), json.dumps(data)
             )
-        for key, stack in self._subscribers.items():
+        for stack in self._subscribers.values():
             callback = stack[-1]
+            logger.debug(f'Notifying subscriber {callback} of event {event}')
             await callback(event)

+ 9 - 3
opendevin/server/session/session.py

@@ -9,7 +9,11 @@ from opendevin.core.schema import AgentState
 from opendevin.core.schema.action import ActionType
 from opendevin.events.action import ChangeAgentStateAction, NullAction
 from opendevin.events.event import Event, EventSource
-from opendevin.events.observation import AgentStateChangedObservation, NullObservation
+from opendevin.events.observation import (
+    AgentStateChangedObservation,
+    CmdOutputObservation,
+    NullObservation,
+)
 from opendevin.events.serialization import event_from_dict, event_to_dict
 from opendevin.events.stream import EventStreamSubscriber
 
@@ -85,8 +89,10 @@ class Session:
             return
         if isinstance(event, NullObservation):
             return
-        if event.source == EventSource.AGENT and not isinstance(
-            event, (NullAction, NullObservation)
+        if event.source == EventSource.AGENT:
+            await self.send(event_to_dict(event))
+        elif event.source == EventSource.USER and isinstance(
+            event, CmdOutputObservation
         ):
             await self.send(event_to_dict(event))