mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
Interactive Terminal (#2493)
* Interactive Terminal * linted * fixed tests * fixed tests * refactored logic * remove console logs
This commit is contained in:
committed by
GitHub
parent
0845d475b8
commit
c743320201
@@ -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" />
|
||||
|
||||
@@ -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
frontend/src/services/terminalService.ts
Normal file
8
frontend/src/services/terminalService.ts
Normal 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);
|
||||
}
|
||||
@@ -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,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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user