diff --git a/frontend/src/components/terminal/Terminal.tsx b/frontend/src/components/terminal/Terminal.tsx index 04d94d4e7e1..c7eb532109a 100644 --- a/frontend/src/components/terminal/Terminal.tsx +++ b/frontend/src/components/terminal/Terminal.tsx @@ -14,7 +14,7 @@ function Terminal() {
- Terminal (read-only) + Terminal
diff --git a/frontend/src/hooks/useTerminal.ts b/frontend/src/hooks/useTerminal.ts index cf198c34879..d2665d33b54 100644 --- a/frontend/src/hooks/useTerminal.ts +++ b/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(() => { diff --git a/frontend/src/services/terminalService.ts b/frontend/src/services/terminalService.ts new file mode 100644 index 00000000000..7f3608245be --- /dev/null +++ b/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); +} diff --git a/opendevin/events/stream.py b/opendevin/events/stream.py index 365b62afb82..21c563a0534 100644 --- a/opendevin/events/stream.py +++ b/opendevin/events/stream.py @@ -95,6 +95,7 @@ def unsubscribe(self, id: EventStreamSubscriber): # 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 @@ async def add_event(self, event: Event, source: EventSource): 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) diff --git a/opendevin/server/session/session.py b/opendevin/server/session/session.py index e49edb5931c..b8bccbf4e2d 100644 --- a/opendevin/server/session/session.py +++ b/opendevin/server/session/session.py @@ -9,7 +9,11 @@ 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 @@ async def on_event(self, event: Event): 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))