Skip to content
Open
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
28 changes: 23 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 41 additions & 14 deletions src/integrations/terminal/TerminalProcess.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import stripAnsi from "strip-ansi"
import * as vscode from "vscode"
import { inspect } from "util"

Expand Down Expand Up @@ -245,7 +244,7 @@ export class TerminalProcess extends BaseTerminalProcess {
// command is finished, we still want to consider it 'hot' in case
// so that api request stalls to let diagnostics catch up").
this.stopHotTimer()
this.emit("completed", this.removeEscapeSequences(this.fullOutput))
this.emit("completed", this.stripCursorSequences(this.removeVSCodeShellIntegration(this.fullOutput)))
this.emit("continue")
}

Expand Down Expand Up @@ -311,7 +310,7 @@ export class TerminalProcess extends BaseTerminalProcess {
outputToProcess = outputToProcess.slice(0, endIndex)

// Clean and return output
return this.removeEscapeSequences(outputToProcess)
return this.stripCursorSequences(this.removeVSCodeShellIntegration(outputToProcess))
}

private emitRemainingBufferIfListening() {
Expand Down Expand Up @@ -375,17 +374,45 @@ export class TerminalProcess extends BaseTerminalProcess {
return data.slice(contentStart, endIndex)
}

// Removes ANSI escape sequences and VSCode-specific terminal control codes from output.
// While stripAnsi handles most ANSI codes, VSCode's shell integration adds custom
// escape sequences (OSC 633) that need special handling. These sequences control
// terminal features like marking command start/end and setting prompts.
//
// This method could be extended to handle other escape sequences, but any additions
// should be carefully considered to ensure they only remove control codes and don't
// alter the actual content or behavior of the output stream.
private removeEscapeSequences(str: string): string {
// eslint-disable-next-line no-control-regex
return stripAnsi(str.replace(/\x1b\]633;[^\x07]+\x07/gs, "").replace(/\x1b\]133;[^\x07]+\x07/gs, ""))
/**
* Remove only VSCode shell integration sequences (OSC 633/133) while
* preserving standard ANSI SGR escape codes for color/formatting.
*
* VSCode shell integration uses OSC 633 and OSC 133 sequences to mark
* prompt boundaries, command starts/ends, etc. These are not useful
* for inline display and should be stripped.
*
* Standard ANSI SGR sequences (e.g., \x1B[32m for green) are preserved
* so the frontend can render them as styled HTML.
*/
private removeVSCodeShellIntegration(text: string): string {
// Remove OSC 633 sequences: \x1B]633;....\x07 or \x1B]633;....\x1B\\
// Remove OSC 133 sequences: \x1B]133;....\x07 or \x1B]133;....\x1B\\
return (
text
// eslint-disable-next-line no-control-regex
.replace(/\x1B\]633;[^\x07\x1B]*(?:\x07|\x1B\\)/g, "")
// eslint-disable-next-line no-control-regex
.replace(/\x1B\]133;[^\x07\x1B]*(?:\x07|\x1B\\)/g, "")
// eslint-disable-next-line no-control-regex
.replace(/\x1B\][0-9]+;[^\x07\x1B]*(?:\x07|\x1B\\)/g, "")
) // Also remove other common OSC sequences that aren't color-related
}

private stripCursorSequences(text: string): string {
return (
text
// eslint-disable-next-line no-control-regex
.replace(/\x1B\[\d*[ABCDEFGHJ]/g, "") // Remove cursor movement: up, down, forward, back
// eslint-disable-next-line no-control-regex
.replace(/\x1B\[su/g, "") // Remove cursor position save/restore
// eslint-disable-next-line no-control-regex
.replace(/\x1B\[\d*[KJ]/g, "") // Remove erase in line/display
// eslint-disable-next-line no-control-regex
.replace(/\x1B\[\?25[hl]/g, "") // Remove cursor show/hide
// eslint-disable-next-line no-control-regex
.replace(/\x1B\[\d*;\d*r/g, "") // Remove scroll region
)
}

/**
Expand Down
87 changes: 87 additions & 0 deletions src/integrations/terminal/__tests__/TerminalProcess.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as vscode from "vscode"
import { TerminalProcess } from "../TerminalProcess"
import { Terminal } from "../Terminal"

// Mock dependencies
vi.mock("vscode", () => ({
window: {
createTerminal: vi.fn(),
},
workspace: {
getConfiguration: vi.fn().mockReturnValue({
get: vi.fn(),
}),
},
ThemeIcon: vi.fn(),
}))

describe("TerminalProcess ANSI Handling", () => {
let terminalProcess: any // Using any to access private methods
let mockTerminal: any

beforeEach(() => {
mockTerminal = {
shellIntegration: {
executeCommand: vi.fn(),
},
name: "Test Terminal",
processId: Promise.resolve(123),
creationOptions: {},
exitStatus: undefined,
state: { isInteractedWith: true },
dispose: vi.fn(),
hide: vi.fn(),
show: vi.fn(),
sendText: vi.fn(),
}

const terminalInfo = new Terminal(1, mockTerminal, "/tmp")
terminalProcess = new TerminalProcess(terminalInfo)
})

describe("removeVSCodeShellIntegration", () => {
it("should preserve standard ANSI SGR sequences", () => {
const input = "\x1B[32mgreen text\x1B[0m"
const result = terminalProcess.removeVSCodeShellIntegration(input)
expect(result).toBe("\x1B[32mgreen text\x1B[0m")
})

it("should remove OSC 633 sequences", () => {
const input = "\x1B]633;A\x07some text"
const result = terminalProcess.removeVSCodeShellIntegration(input)
expect(result).toBe("some text")
})

it("should remove OSC 133 sequences", () => {
const input = "\x1B]133;A\x07some text"
const result = terminalProcess.removeVSCodeShellIntegration(input)
expect(result).toBe("some text")
})

it("should handle mixed sequences", () => {
const input = "\x1B]633;C\x07\x1B[1m\x1B[32m✓\x1B[39m\x1B[22m test passed"
const result = terminalProcess.removeVSCodeShellIntegration(input)
expect(result).toBe("\x1B[1m\x1B[32m✓\x1B[39m\x1B[22m test passed")
})

it("should remove other OSC sequences", () => {
const input = "\x1B]0;Console Title\x07Content"
const result = terminalProcess.removeVSCodeShellIntegration(input)
expect(result).toBe("Content")
})
})

describe("stripCursorSequences", () => {
it("should remove cursor movement codes", () => {
const input = "text\x1B[1Aup\x1B[2Kclear"
const result = terminalProcess.stripCursorSequences(input)
expect(result).toBe("textupclear")
})

it("should preserve colors while removing cursor codes", () => {
const input = "\x1B[31mred\x1B[1B\x1B[32mgreen"
const result = terminalProcess.stripCursorSequences(input)
expect(result).toBe("\x1B[31mred\x1B[32mgreen")
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -354,9 +354,12 @@ describe("TerminalProcess with Bash Command Output", () => {
expect(capturedOutput).toBe("Red Text\r\n")
} else {
// Use printf instead of echo -e for more consistent behavior across platforms
// Note: ANSI escape sequences are stripped in the output processing
const { capturedOutput } = await testTerminalCommand('printf "\\033[31mRed Text\\033[0m\\n"', "Red Text\n")
expect(capturedOutput).toBe("Red Text\n")
// Note: ANSI escape sequences are now preserved in the output processing
const { capturedOutput } = await testTerminalCommand(
'printf "\\033[31mRed Text\\033[0m\\n"',
"\x1B[31mRed Text\x1B[0m\n",
)
expect(capturedOutput).toBe("\x1B[31mRed Text\x1B[0m\n")
}
})

Expand Down
5 changes: 3 additions & 2 deletions webview-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@types/qrcode": "^1.5.5",
"@vscode/codicons": "^0.0.36",
"@vscode/webview-ui-toolkit": "^1.4.0",
"ansi-to-html": "^0.7.2",
"axios": "^1.12.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand All @@ -57,8 +58,8 @@
"pretty-bytes": "^7.0.0",
"qrcode": "^1.5.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-compiler-runtime": "^1.0.0",
"react-dom": "^18.3.1",
"react-i18next": "^15.4.1",
"react-icons": "^5.5.0",
"react-markdown": "^9.0.3",
Expand Down Expand Up @@ -86,7 +87,6 @@
"zod": "^3.25.61"
},
"devDependencies": {
"babel-plugin-react-compiler": "^1.0.0",
"@roo-code/config-eslint": "workspace:^",
"@roo-code/config-typescript": "workspace:^",
"@testing-library/jest-dom": "^6.6.3",
Expand All @@ -103,6 +103,7 @@
"@types/vscode-webview": "^1.57.5",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/ui": "^3.2.3",
"babel-plugin-react-compiler": "^1.0.0",
"identity-obj-proxy": "^3.0.0",
"jsdom": "^26.0.0",
"vite": "6.3.6",
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/components/chat/CommandExecution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Button, StandardTooltip } from "@src/components/ui"
import CodeBlock from "@src/components/common/CodeBlock"

import { CommandPatternSelector } from "./CommandPatternSelector"
import { TerminalOutput } from "./TerminalOutput"

interface CommandPattern {
pattern: string
Expand Down Expand Up @@ -225,7 +226,7 @@ const OutputContainerInternal = ({ isExpanded, output }: { isExpanded: boolean;
"max-h-0": !isExpanded,
"max-h-[100%] mt-1 pt-1 border-t border-border/25": isExpanded,
})}>
{output.length > 0 && <CodeBlock source={output} language="log" />}
{output.length > 0 && <TerminalOutput content={output} />}
</div>
)

Expand Down
Loading
Loading