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))