Skip to content

Commit

Permalink
Interactive Terminal (#2493)
Browse files Browse the repository at this point in the history
* Interactive Terminal

* linted

* fixed tests

* fixed tests

* refactored logic

* remove console logs
  • Loading branch information
SmartManoj authored Jun 22, 2024
1 parent 0845d47 commit c743320
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 5 deletions.
2 changes: 1 addition & 1 deletion frontend/src/components/terminal/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
40 changes: 40 additions & 0 deletions frontend/src/hooks/useTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -26,13 +27,52 @@ export const useTerminal = (commands: Command[] = []) => {
fitAddon.current = new FitAddon();

let resizeObserver: ResizeObserver;
let commandBuffer = "";

if (ref.current) {
/* Initialize the terminal in the DOM */
terminal.current.loadAddon(fitAddon.current);
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(() => {
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/services/terminalService.ts
Original file line number Diff line number Diff line change
@@ -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);
}
4 changes: 3 additions & 1 deletion opendevin/events/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
12 changes: 9 additions & 3 deletions opendevin/server/session/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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))

Expand Down

0 comments on commit c743320

Please sign in to comment.