Interactive Terminal (#2493)

* Interactive Terminal

* linted

* fixed tests

* fixed tests

* refactored logic

* remove console logs
This commit is contained in:
மனோஜ்குமார் பழனிச்சாமி
2024-06-22 08:26:54 +05:30
committed by GitHub
parent 0845d475b8
commit c743320201
5 changed files with 61 additions and 5 deletions

View File

@@ -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" />

View File

@@ -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(() => {

View File

@@ -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);
}

View File

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

View File

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