diff --git a/webview-ui/src/components/chat/CommandExecution.tsx b/webview-ui/src/components/chat/CommandExecution.tsx index f2977849b71..ed6c90036d6 100644 --- a/webview-ui/src/components/chat/CommandExecution.tsx +++ b/webview-ui/src/components/chat/CommandExecution.tsx @@ -1,9 +1,9 @@ -import { HTMLAttributes, forwardRef, useMemo, useState } from "react" -import { Virtuoso } from "react-virtuoso" -import { ChevronDown } from "lucide-react" +import { forwardRef, useState } from "react" +import { useTranslation } from "react-i18next" import { useExtensionState } from "@src/context/ExtensionStateContext" -import { cn } from "@src/lib/utils" +import { BaseTerminal } from "../../../../src/integrations/terminal/BaseTerminal" +import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock" interface CommandExecutionProps { command: string @@ -11,45 +11,48 @@ interface CommandExecutionProps { } export const CommandExecution = forwardRef(({ command, output }, ref) => { - const { terminalShellIntegrationDisabled = false } = useExtensionState() - - // If we aren't opening the VSCode terminal for this command then we default - // to expanding the command execution output. + const { t } = useTranslation() + const { terminalShellIntegrationDisabled = false, terminalOutputLineLimit = 500 } = useExtensionState() const [isExpanded, setIsExpanded] = useState(terminalShellIntegrationDisabled) + const compressedOutput = BaseTerminal.compressTerminalOutput(output, terminalOutputLineLimit) - const lines = useMemo(() => output.split("\n"), [output]) + const onToggleExpand = () => { + setIsExpanded(!isExpanded) + } return ( -
+ <>
setIsExpanded(!isExpanded)}> - {command} - -
-
- {lines[i]}} - followOutput="auto" - /> + ref={ref} + style={{ + borderRadius: 3, + border: "1px solid var(--vscode-editorGroup-border)", + overflow: "hidden", + backgroundColor: CODE_BLOCK_BG_COLOR, + }}> + + {output.length > 0 && ( +
+
+ + {t("chat:commandOutput")} +
+ {isExpanded && } +
+ )}
-
+ ) }) -type LineProps = HTMLAttributes - -const Line = ({ className, ...props }: LineProps) => { - return ( -
- ) -} - CommandExecution.displayName = "CommandExecution" diff --git a/webview-ui/src/components/chat/__tests__/CommandExecution.test.tsx b/webview-ui/src/components/chat/__tests__/CommandExecution.test.tsx index 1fa5c831e33..0c0bd0733f2 100644 --- a/webview-ui/src/components/chat/__tests__/CommandExecution.test.tsx +++ b/webview-ui/src/components/chat/__tests__/CommandExecution.test.tsx @@ -1,33 +1,18 @@ // npx jest src/components/chat/__tests__/CommandExecution.test.tsx import React from "react" -import { render, screen } from "@testing-library/react" +import { render, screen, fireEvent } from "@testing-library/react" import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext" import { CommandExecution } from "../CommandExecution" +jest.mock("../../../components/common/CodeBlock") + jest.mock("@src/lib/utils", () => ({ cn: (...inputs: any[]) => inputs.filter(Boolean).join(" "), })) -jest.mock("lucide-react", () => ({ - ChevronDown: () =>
ChevronDown
, -})) - -jest.mock("react-virtuoso", () => ({ - Virtuoso: React.forwardRef(({ totalCount, itemContent }: any, ref: any) => ( -
- {Array.from({ length: totalCount }).map((_, index) => ( -
- {itemContent(index)} -
- ))} -
- )), - VirtuosoHandle: jest.fn(), -})) - describe("CommandExecution", () => { const renderComponent = (command: string, output: string) => { return render( @@ -40,24 +25,34 @@ describe("CommandExecution", () => { it("renders command output with virtualized list", () => { const testOutput = "Line 1\nLine 2\nLine 3" renderComponent("ls", testOutput) - expect(screen.getByTestId("virtuoso-container")).toBeInTheDocument() - expect(screen.getByText("Line 1")).toBeInTheDocument() - expect(screen.getByText("Line 2")).toBeInTheDocument() - expect(screen.getByText("Line 3")).toBeInTheDocument() + const codeBlock = screen.getByTestId("mock-code-block") + expect(codeBlock).toHaveTextContent("ls") + + fireEvent.click(screen.getByText("commandOutput")) + const outputBlock = screen.getAllByTestId("mock-code-block")[1] + + expect(outputBlock).toHaveTextContent("Line 1") + expect(outputBlock).toHaveTextContent("Line 2") + expect(outputBlock).toHaveTextContent("Line 3") }) it("handles empty output", () => { renderComponent("ls", "") - expect(screen.getByTestId("virtuoso-container")).toBeInTheDocument() - expect(screen.getByTestId("virtuoso-item-0")).toBeInTheDocument() - expect(screen.queryByTestId("virtuoso-item-1")).not.toBeInTheDocument() + const codeBlock = screen.getByTestId("mock-code-block") + expect(codeBlock).toHaveTextContent("ls") + expect(screen.queryByText("commandOutput")).not.toBeInTheDocument() + expect(screen.queryAllByTestId("mock-code-block")).toHaveLength(1) }) it("handles large output", () => { const largeOutput = Array.from({ length: 1000 }, (_, i) => `Line ${i + 1}`).join("\n") renderComponent("ls", largeOutput) - expect(screen.getByTestId("virtuoso-container")).toBeInTheDocument() - expect(screen.getByText("Line 1")).toBeInTheDocument() - expect(screen.getByText("Line 1000")).toBeInTheDocument() + const codeBlock = screen.getByTestId("mock-code-block") + expect(codeBlock).toHaveTextContent("ls") + + fireEvent.click(screen.getByText("commandOutput")) + const outputBlock = screen.getAllByTestId("mock-code-block")[1] + expect(outputBlock).toHaveTextContent("Line 1") + expect(outputBlock).toHaveTextContent("Line 1000") }) }) diff --git a/webview-ui/src/components/common/__mocks__/CodeBlock.tsx b/webview-ui/src/components/common/__mocks__/CodeBlock.tsx index b4d73633673..7eaa790ff22 100644 --- a/webview-ui/src/components/common/__mocks__/CodeBlock.tsx +++ b/webview-ui/src/components/common/__mocks__/CodeBlock.tsx @@ -1,10 +1,10 @@ import * as React from "react" interface CodeBlockProps { - children?: React.ReactNode + source?: string language?: string } -const CodeBlock: React.FC = () =>
Mocked Code Block
+const CodeBlock: React.FC = ({ source = "" }) =>
{source}
export default CodeBlock