Просмотр исходного кода

fix: webview terminal output processing error (#3028)

KJ7LNW 10 месяцев назад
Родитель
Сommit
7d75536741

+ 5 - 2
src/core/tools/executeCommandTool.ts

@@ -149,9 +149,12 @@ export async function executeCommand(
 	const terminalProvider = terminalShellIntegrationDisabled ? "execa" : "vscode"
 	const clineProvider = await cline.providerRef.deref()
 
+	let accumulatedOutput = ""
 	const callbacks: RooTerminalCallbacks = {
-		onLine: async (output: string, process: RooTerminalProcess) => {
-			const status: CommandExecutionStatus = { executionId, status: "output", output }
+		onLine: async (lines: string, process: RooTerminalProcess) => {
+			accumulatedOutput += lines
+			const compressedOutput = Terminal.compressTerminalOutput(accumulatedOutput, terminalOutputLineLimit)
+			const status: CommandExecutionStatus = { executionId, status: "output", output: compressedOutput }
 			clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
 
 			if (runInBackground) {

+ 33 - 48
webview-ui/src/components/chat/CommandExecution.tsx

@@ -1,6 +1,5 @@
-import { HTMLAttributes, useCallback, useEffect, useMemo, useState } from "react"
+import { useCallback, useState, memo } from "react"
 import { useEvent } from "react-use"
-import { Virtuoso } from "react-virtuoso"
 import { ChevronDown, Skull } from "lucide-react"
 
 import { CommandExecutionStatus, commandExecutionStatusSchema } from "@roo/schemas"
@@ -19,6 +18,17 @@ interface CommandExecutionProps {
 	text?: string
 }
 
+const parseCommandAndOutput = (text: string) => {
+	const index = text.indexOf(COMMAND_OUTPUT_STRING)
+	if (index === -1) {
+		return { command: text, output: "" }
+	}
+	return {
+		command: text.slice(0, index),
+		output: text.slice(index + COMMAND_OUTPUT_STRING.length),
+	}
+}
+
 export const CommandExecution = ({ executionId, text }: CommandExecutionProps) => {
 	const { terminalShellIntegrationDisabled = false } = useExtensionState()
 
@@ -27,13 +37,11 @@ export const CommandExecution = ({ executionId, text }: CommandExecutionProps) =
 	const [isExpanded, setIsExpanded] = useState(terminalShellIntegrationDisabled)
 
 	const [status, setStatus] = useState<CommandExecutionStatus | null>(null)
-	const [output, setOutput] = useState("")
-	const [command, setCommand] = useState(text)
-
-	const lines = useMemo(
-		() => [`$ ${command}`, ...output.split("\n").filter((line) => line.trim() !== "")],
-		[output, command],
-	)
+	const { command: initialCommand, output: initialOutput } = text
+		? parseCommandAndOutput(text)
+		: { command: "", output: "" }
+	const [output, setOutput] = useState(initialOutput)
+	const [command, setCommand] = useState(initialCommand)
 
 	const onMessage = useCallback(
 		(event: MessageEvent) => {
@@ -55,7 +63,7 @@ export const CommandExecution = ({ executionId, text }: CommandExecutionProps) =
 							setStatus(data)
 							break
 						case "output":
-							setOutput((output) => output + data.output)
+							setOutput(data.output)
 							break
 						case "fallback":
 							setIsExpanded(true)
@@ -72,22 +80,9 @@ export const CommandExecution = ({ executionId, text }: CommandExecutionProps) =
 
 	useEvent("message", onMessage)
 
-	useEffect(() => {
-		if (!status && text) {
-			const index = text.indexOf(COMMAND_OUTPUT_STRING)
-
-			if (index === -1) {
-				setCommand(text)
-			} else {
-				setCommand(text.slice(0, index))
-				setOutput(text.slice(index + COMMAND_OUTPUT_STRING.length))
-			}
-		}
-	}, [status, text])
-
 	return (
 		<div className="w-full bg-vscode-editor-background border border-vscode-border rounded-xs p-2">
-			<CodeBlock source={command} language="shell" />
+			<CodeBlock source={text ? parseCommandAndOutput(text).command : command} language="shell" />
 			<div className="flex flex-row items-center justify-between gap-2 px-1">
 				<div className="flex flex-row items-center gap-1">
 					{status?.status === "started" && (
@@ -116,7 +111,7 @@ export const CommandExecution = ({ executionId, text }: CommandExecutionProps) =
 							<div className="whitespace-nowrap">Exited ({status.exitCode})</div>
 						</div>
 					)}
-					{lines.length > 0 && (
+					{output.length > 0 && (
 						<Button variant="ghost" size="icon" onClick={() => setIsExpanded(!isExpanded)}>
 							<ChevronDown
 								className={cn("size-4 transition-transform duration-300", {
@@ -127,31 +122,21 @@ export const CommandExecution = ({ executionId, text }: CommandExecutionProps) =
 					)}
 				</div>
 			</div>
-			<div
-				className={cn("mt-1 pt-1 border-t border-border/25", { hidden: !isExpanded })}
-				style={{ height: Math.min((lines.length + 1) * 16, 200) }}>
-				{lines.length > 0 && (
-					<Virtuoso
-						className="h-full"
-						totalCount={lines.length}
-						itemContent={(i) => <Line className="text-sm">{lines[i]}</Line>}
-						followOutput="auto"
-					/>
-				)}
-			</div>
+			<MemoizedOutputContainer isExpanded={isExpanded} output={output} />
 		</div>
 	)
 }
 
-type LineProps = HTMLAttributes<HTMLDivElement>
-
-const Line = ({ className, ...props }: LineProps) => {
-	return (
-		<div
-			className={cn("font-mono text-vscode-editor-foreground whitespace-pre-wrap break-words", className)}
-			{...props}
-		/>
-	)
-}
-
 CommandExecution.displayName = "CommandExecution"
+
+const OutputContainer = ({ isExpanded, output }: { isExpanded: boolean; output: string }) => (
+	<div
+		className={cn("mt-1 pt-1 border-t border-border/25 overflow-hidden transition-[max-height] duration-300", {
+			"max-h-0": !isExpanded,
+			"max-h-[100%]": isExpanded,
+		})}>
+		{output.length > 0 && <CodeBlock source={output} language="log" />}
+	</div>
+)
+
+const MemoizedOutputContainer = memo(OutputContainer)