mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: Add CommandOutputViewer component and integrate it into ChatRow (#2326)
* feat: Add CommandOutputViewer component and integrate it into ChatRow * Remove unnecessary memo wrapper from CommandOutputViewer component
This commit is contained in:
parent
58149a083e
commit
e1f6eb625b
3 changed files with 115 additions and 1 deletions
|
|
@ -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) => (
|
||||
<div ref={ref} data-testid="virtuoso-container">
|
||||
{Array.from({ length: totalCount }).map((_, index) => (
|
||||
<div key={index} data-testid={`virtuoso-item-${index}`}>
|
||||
{itemContent(index)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)),
|
||||
VirtuosoHandle: jest.fn(),
|
||||
}))
|
||||
|
||||
describe("CommandOutputViewer", () => {
|
||||
it("renders command output with virtualized list", () => {
|
||||
const testOutput = "Line 1\nLine 2\nLine 3"
|
||||
|
||||
render(<CommandOutputViewer output={testOutput} />)
|
||||
|
||||
// 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(<CommandOutputViewer output="" />)
|
||||
|
||||
// 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(<CommandOutputViewer output={largeOutput} />)
|
||||
|
||||
// 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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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"}`}></span>
|
||||
<span style={{ fontSize: "0.8em" }}>{t("chat:commandOutput")}</span>
|
||||
</div>
|
||||
{isExpanded && <CodeBlock source={`${"```"}shell\n${output}\n${"```"}`} />}
|
||||
{isExpanded && <CommandOutputViewer output={output} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
50
webview-ui/src/components/common/CommandOutputViewer.tsx
Normal file
50
webview-ui/src/components/common/CommandOutputViewer.tsx
Normal file
|
|
@ -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<HTMLDivElement, CommandOutputViewerProps>(({ output }, ref) => {
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(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 (
|
||||
<div ref={ref} className="w-full rounded-b-md bg-[var(--vscode-editor-background)] h-[300px]">
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
className="h-full"
|
||||
totalCount={lines.length}
|
||||
itemContent={(index) => (
|
||||
<div
|
||||
className={cn(
|
||||
"px-3 py-0.5",
|
||||
"font-mono text-vscode-editor-foreground",
|
||||
"text-[var(--vscode-editor-font-size,var(--vscode-font-size,12px))]",
|
||||
"font-[var(--vscode-editor-font-family)]",
|
||||
"whitespace-pre-wrap break-all anywhere",
|
||||
)}>
|
||||
{lines[index]}
|
||||
</div>
|
||||
)}
|
||||
increaseViewportBy={{ top: 300, bottom: 300 }}
|
||||
followOutput="auto"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
CommandOutputViewer.displayName = "CommandOutputViewer"
|
||||
|
||||
export default CommandOutputViewer
|
||||
Loading…
Add table
Reference in a new issue