diff --git a/webview-ui/src/__tests__/components/common/CommandOutputViewer.test.tsx b/webview-ui/src/__tests__/components/common/CommandOutputViewer.test.tsx
new file mode 100644
index 00000000000..03562c51b68
--- /dev/null
+++ b/webview-ui/src/__tests__/components/common/CommandOutputViewer.test.tsx
@@ -0,0 +1,63 @@
+import React from "react"
+import { render, screen } from "@testing-library/react"
+import CommandOutputViewer from "../../../components/common/CommandOutputViewer"
+
+// Mock the cn utility function
+jest.mock("../../../lib/utils", () => ({
+ cn: (...inputs: any[]) => inputs.filter(Boolean).join(" "),
+}))
+
+// Mock the Virtuoso component
+jest.mock("react-virtuoso", () => ({
+ Virtuoso: React.forwardRef(({ totalCount, itemContent }: any, ref: any) => (
+
+ {Array.from({ length: totalCount }).map((_, index) => (
+
+ {itemContent(index)}
+
+ ))}
+
+ )),
+ VirtuosoHandle: jest.fn(),
+}))
+
+describe("CommandOutputViewer", () => {
+ it("renders command output with virtualized list", () => {
+ const testOutput = "Line 1\nLine 2\nLine 3"
+
+ render()
+
+ // Check if Virtuoso container is rendered
+ expect(screen.getByTestId("virtuoso-container")).toBeInTheDocument()
+
+ // Check if all lines are rendered
+ expect(screen.getByText("Line 1")).toBeInTheDocument()
+ expect(screen.getByText("Line 2")).toBeInTheDocument()
+ expect(screen.getByText("Line 3")).toBeInTheDocument()
+ })
+
+ it("handles empty output", () => {
+ render()
+
+ // Should still render the container but with no items
+ expect(screen.getByTestId("virtuoso-container")).toBeInTheDocument()
+
+ // No virtuoso items should be rendered for empty string (which creates one empty line)
+ expect(screen.getByTestId("virtuoso-item-0")).toBeInTheDocument()
+ expect(screen.queryByTestId("virtuoso-item-1")).not.toBeInTheDocument()
+ })
+
+ it("handles large output", () => {
+ // Create a large output with 1000 lines
+ const largeOutput = Array.from({ length: 1000 }, (_, i) => `Line ${i + 1}`).join("\n")
+
+ render()
+
+ // Check if Virtuoso container is rendered
+ expect(screen.getByTestId("virtuoso-container")).toBeInTheDocument()
+
+ // Check if first and last lines are rendered
+ expect(screen.getByText("Line 1")).toBeInTheDocument()
+ expect(screen.getByText("Line 1000")).toBeInTheDocument()
+ })
+})
diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx
index c09ae237834..4c950915ba7 100644
--- a/webview-ui/src/components/chat/ChatRow.tsx
+++ b/webview-ui/src/components/chat/ChatRow.tsx
@@ -16,6 +16,7 @@ import { findMatchingResourceOrTemplate } from "../../utils/mcp"
import { vscode } from "../../utils/vscode"
import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
+import CommandOutputViewer from "../common/CommandOutputViewer"
import MarkdownBlock from "../common/MarkdownBlock"
import { ReasoningBlock } from "./ReasoningBlock"
import Thumbnails from "../common/Thumbnails"
@@ -917,7 +918,7 @@ export const ChatRowContent = ({
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}>
{t("chat:commandOutput")}
- {isExpanded && }
+ {isExpanded && }
)}
diff --git a/webview-ui/src/components/common/CommandOutputViewer.tsx b/webview-ui/src/components/common/CommandOutputViewer.tsx
new file mode 100644
index 00000000000..443f3bc9e81
--- /dev/null
+++ b/webview-ui/src/components/common/CommandOutputViewer.tsx
@@ -0,0 +1,50 @@
+import { forwardRef, useEffect, useRef } from "react"
+import { Virtuoso, VirtuosoHandle } from "react-virtuoso"
+import { cn } from "../../lib/utils"
+
+interface CommandOutputViewerProps {
+ output: string
+}
+
+const CommandOutputViewer = forwardRef(({ output }, ref) => {
+ const virtuosoRef = useRef(null)
+ const lines = output.split("\n")
+
+ useEffect(() => {
+ // Scroll to the bottom when output changes
+ if (virtuosoRef.current && typeof virtuosoRef.current.scrollToIndex === "function") {
+ virtuosoRef.current.scrollToIndex({
+ index: lines.length - 1,
+ behavior: "auto",
+ })
+ }
+ }, [output, lines.length])
+
+ return (
+
+
(
+
+ {lines[index]}
+
+ )}
+ increaseViewportBy={{ top: 300, bottom: 300 }}
+ followOutput="auto"
+ />
+
+ )
+})
+
+CommandOutputViewer.displayName = "CommandOutputViewer"
+
+export default CommandOutputViewer