Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions webview-ui/src/components/chat/CommandExecution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import CodeBlock from "../kilocode/common/CodeBlock" // kilocode_change
import { CommandPatternSelector } from "./CommandPatternSelector"
import { parseCommand } from "../../utils/command-validation"
import { extractPatternsFromCommand } from "../../utils/command-parser"
import { NEWLINE_PLACEHOLDER, CARRIAGE_RETURN_PLACEHOLDER } from "../../utils/command-validation-quote-protection"
import { t } from "i18next"

interface CommandPattern {
Expand All @@ -40,7 +41,14 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
setDeniedCommands,
} = useExtensionState()

const { command, output: parsedOutput } = useMemo(() => parseCommandAndOutput(text), [text])
const { command, output: parsedOutput } = useMemo(() => {
const parsed = parseCommandAndOutput(text)
// Restore newlines that were protected during parsing for display purposes
const displayCommand = parsed.command
.replace(new RegExp(NEWLINE_PLACEHOLDER, "g"), "\n")
.replace(new RegExp(CARRIAGE_RETURN_PLACEHOLDER, "g"), "\r")
return { command: displayCommand, output: parsed.output }
}, [text])

// If we aren't opening the VSCode terminal for this command then we default
// to expanding the command execution output.
Expand All @@ -61,17 +69,24 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
// Then extract patterns from each command using the existing pattern extraction logic
const allPatterns = new Set<string>()

// Add all individual commands first
// Add all individual commands first, but filter out commands with newlines
// (these are typically multi-line strings that shouldn't be shown as patterns)
allCommands.forEach((cmd) => {
if (cmd.trim()) {
allPatterns.add(cmd.trim())
const trimmed = cmd.trim()
if (trimmed && !trimmed.includes("\n") && !trimmed.includes("\r")) {
allPatterns.add(trimmed)
}
})

// Then add extracted patterns for each command
allCommands.forEach((cmd) => {
const patterns = extractPatternsFromCommand(cmd)
patterns.forEach((pattern) => allPatterns.add(pattern))
patterns.forEach((pattern) => {
// Also filter out patterns with newlines
if (!pattern.includes("\n") && !pattern.includes("\r")) {
allPatterns.add(pattern)
}
})
})

return Array.from(allPatterns).map((pattern) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, it, expect } from "vitest"
import { render } from "@testing-library/react"
import { CommandExecution } from "../CommandExecution"
import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext"
import { TooltipProvider } from "@src/components/ui/tooltip"

describe("CommandExecution - Display Fix", () => {
const mockExtensionState = {
terminalShellIntegrationDisabled: true,
allowedCommands: [],
deniedCommands: [],
setAllowedCommands: () => {},
setDeniedCommands: () => {},
}

const wrapper = ({ children }: { children: React.ReactNode }) => (
<TooltipProvider>
<ExtensionStateContextProvider {...(mockExtensionState as any)}>{children}</ExtensionStateContextProvider>
</TooltipProvider>
)

it("should display newlines correctly in git commit messages", () => {
const commandText = 'git commit -m "feat: title\n\n- point a\n- point b"'

const { container } = render(<CommandExecution executionId="test-1" text={commandText} />, { wrapper })

// The command should be displayed with actual newlines, not placeholders
const codeBlock = container.querySelector("code")
expect(codeBlock?.textContent).toContain("feat: title")
expect(codeBlock?.textContent).not.toContain("___NEWLINE___")
expect(codeBlock?.textContent).not.toContain("___CARRIAGE_RETURN___")
})

it("should not show multi-line commands in pattern selector", async () => {
const commandText =
'echo "ACT I Scene 1: A Digital Stage\n[The curtain rises on an empty terminal]\n[A lone cursor blinks in the darkness]"'

const { container } = render(<CommandExecution executionId="test-4" text={commandText} />, { wrapper })

// The command display should show the full command with newlines
const codeBlock = container.querySelector("code")
expect(codeBlock?.textContent).toContain("ACT I Scene 1")

// Expand the pattern selector
const expandButton =
container.querySelector('button[aria-label*="manage"]') || container.querySelector("button:has(svg)")
if (expandButton) {
;(expandButton as HTMLButtonElement).click()
}

// Wait a bit for the expansion
await new Promise((resolve) => setTimeout(resolve, 100))

// The pattern selector should only show "echo", not the full multi-line command
const patternElements = container.querySelectorAll(".font-mono.text-xs")
const patternTexts = Array.from(patternElements).map((el) => el.textContent?.trim())

// Should have "echo" pattern
expect(patternTexts.some((text) => text === "echo")).toBe(true)

// Should NOT have the full multi-line command
expect(patternTexts.some((text) => text && text.includes("ACT I Scene 1"))).toBe(false)
expect(patternTexts.some((text) => text && text.includes("\n"))).toBe(false)
})

it("should display CRLF correctly", () => {
const commandText = 'echo "hello\r\nworld"'

const { container } = render(<CommandExecution executionId="test-2" text={commandText} />, { wrapper })

// Just verify the component renders without errors
expect(container).toBeTruthy()
// Verify no placeholders are visible in the rendered output
expect(container.innerHTML).not.toContain("___NEWLINE___")
expect(container.innerHTML).not.toContain("___CARRIAGE_RETURN___")
})
})