diff --git a/frontend/src/components/terminal/Terminal.test.tsx b/frontend/src/components/terminal/Terminal.test.tsx index c8a583dca4..e71afd0f6c 100644 --- a/frontend/src/components/terminal/Terminal.test.tsx +++ b/frontend/src/components/terminal/Terminal.test.tsx @@ -102,6 +102,23 @@ describe("Terminal", () => { expect(mockTerminal.write).toHaveBeenCalledWith("$ "); }); + it("should display a custom symbol if output contains a custom symbol", () => { + renderTerminal([ + { type: "input", content: "echo Hello" }, + { + type: "output", + content: + "Hello\r\n\r\n[Python Interpreter: /openhands/poetry/openhands-5O4_aCHf-py3.11/bin/python]\nopenhands@659478cb008c:/workspace $ ", + }, + ]); + + expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo Hello"); + expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "Hello"); + expect(mockTerminal.write).toHaveBeenCalledWith( + "\nopenhands@659478cb008c:/workspace $ ", + ); + }); + // This test fails because it expects `disposeMock` to have been called before the component is unmounted. it.skip("should dispose the terminal on unmount", () => { const { unmount } = renderWithProviders(); diff --git a/frontend/src/hooks/useTerminal.ts b/frontend/src/hooks/useTerminal.ts index 7919553328..fcd7b440dc 100644 --- a/frontend/src/hooks/useTerminal.ts +++ b/frontend/src/hooks/useTerminal.ts @@ -3,6 +3,7 @@ import { Terminal } from "@xterm/xterm"; import React from "react"; import { Command } from "#/state/commandSlice"; import { sendTerminalCommand } from "#/services/terminalService"; +import { parseTerminalOutput } from "#/utils/parseTerminalOutput"; /* NOTE: Tests for this hook are indirectly covered by the tests for the XTermTerminal component. @@ -101,14 +102,16 @@ export const useTerminal = (commands: Command[] = []) => { // Start writing commands from the last command index for (let i = lastCommandIndex.current; i < commands.length; i += 1) { const command = commands[i]; - const lines = command.content.split("\n"); + const lines = parseTerminalOutput(command.content).output.split("\n"); lines.forEach((line: string) => { - terminal.current?.writeln(line); + terminal.current?.writeln(parseTerminalOutput(line).output); }); if (command.type === "output") { - terminal.current.write("\n$ "); + terminal.current.write( + `\n${parseTerminalOutput(command.content).symbol} `, + ); } } diff --git a/frontend/src/utils/parseTerminalOutput.test.ts b/frontend/src/utils/parseTerminalOutput.test.ts new file mode 100644 index 0000000000..7d09f1c96b --- /dev/null +++ b/frontend/src/utils/parseTerminalOutput.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from "vitest"; +import { parseTerminalOutput } from "./parseTerminalOutput"; + +describe("parseTerminalOutput", () => { + it("should parse the command, env, and symbol", () => { + const raw = + "web_scraper.py\r\n\r\n[Python Interpreter: /openhands/poetry/openhands-5O4_aCHf-py3.11/bin/python]\nopenhands@659478cb008c:/workspace $ "; + + const parsed = parseTerminalOutput(raw); + + expect(parsed.output).toBe("web_scraper.py"); + expect(parsed.symbol).toBe("openhands@659478cb008c:/workspace $"); + }); + + it("should return raw output if unable to parse", () => { + const raw = "web_scraper.py"; + + const parsed = parseTerminalOutput(raw); + expect(parsed.output).toBe("web_scraper.py"); + }); +}); diff --git a/frontend/src/utils/parseTerminalOutput.ts b/frontend/src/utils/parseTerminalOutput.ts new file mode 100644 index 0000000000..d0fc71c781 --- /dev/null +++ b/frontend/src/utils/parseTerminalOutput.ts @@ -0,0 +1,27 @@ +/** + * Parses the raw output from the terminal into the command and symbol + * @param raw The raw output to be displayed in the terminal + * @returns The parsed output + * + * @example + * const raw = + * "web_scraper.py\r\n\r\n[Python Interpreter: /openhands/poetry/openhands-5O4_aCHf-py3.11/bin/python]\nopenhands@659478cb008c:/workspace $ "; + * + * const parsed = parseTerminalOutput(raw); + * + * console.log(parsed.output); // web_scraper.py + * console.log(parsed.symbol); // openhands@659478cb008c:/workspace $ + */ +export const parseTerminalOutput = (raw: string) => { + const envRegex = /\[Python Interpreter: (.*)\]/; + const env = raw.match(envRegex); + let fullOutput = raw; + if (env && env[0]) fullOutput = fullOutput.replace(`${env[0]}\n`, ""); + const [output, s] = fullOutput.split("\r\n\r\n"); + const symbol = s || "$"; + + return { + output: output.trim(), + symbol: symbol.trim(), + }; +};