Skip to content
Merged
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
13 changes: 13 additions & 0 deletions packages/types/src/__tests__/global-settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,16 @@ describe("alwaysDenyUnapprovedCommands global setting", () => {
expect(() => globalSettingsSchema.parse({ alwaysDenyUnapprovedCommands: "true" })).toThrow()
})
})

describe("collapseCommandOutputByDefault global setting", () => {
it.each([true, false, undefined])("round-trips %s through the export/import schema", (value) => {
const settings = globalSettingsSchema.parse({ collapseCommandOutputByDefault: value })
expect(globalSettingsSchema.parse(JSON.parse(JSON.stringify(settings))).collapseCommandOutputByDefault).toBe(
value,
)
expect(GLOBAL_SETTINGS_KEYS).toContain("collapseCommandOutputByDefault")
})
it("rejects non-boolean values", () => {
expect(globalSettingsSchema.safeParse({ collapseCommandOutputByDefault: "true" }).success).toBe(false)
})
})
4 changes: 4 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
*/
export const DEFAULT_DIFF_FUZZY_THRESHOLD = 1.0

// Preserve the existing terminal-dependent output display until users opt in.
export const DEFAULT_COLLAPSE_COMMAND_OUTPUT_BY_DEFAULT = false

Check warning on line 53 in packages/types/src/global-settings.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/global-settings.ts:53: Survived BooleanLiteral mutant (replacement: true). See the job summary for the complete list and resolution guidance.

export const DEFAULT_DESTRUCTIVE_COMMAND_GUARD_ENABLED = false

/**
Expand Down Expand Up @@ -236,6 +239,7 @@
terminalOutputPreviewSize: z.enum(["small", "medium", "large"]).optional(),
terminalShellIntegrationTimeout: z.number().optional(),
terminalShellIntegrationDisabled: z.boolean().optional(),
collapseCommandOutputByDefault: z.boolean().optional(),
terminalCommandDelay: z.number().optional(),
terminalPowershellCounter: z.boolean().optional(),
terminalZshClearEolMark: z.boolean().optional(),
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ export type ExtensionState = Pick<
| "terminalOutputPreviewSize"
| "terminalShellIntegrationTimeout"
| "terminalShellIntegrationDisabled"
| "collapseCommandOutputByDefault"
| "terminalCommandDelay"
| "terminalPowershellCounter"
| "terminalZshClearEolMark"
Expand Down
6 changes: 6 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
DEFAULT_DIFF_FUZZY_THRESHOLD,
DEFAULT_DESTRUCTIVE_COMMAND_GUARD_ENABLED,
DEFAULT_ALWAYS_DENY_UNAPPROVED_COMMANDS,
DEFAULT_COLLAPSE_COMMAND_OUTPUT_BY_DEFAULT,
DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES,
DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED,
DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES,
Expand Down Expand Up @@ -2751,6 +2752,7 @@ export class ClineProvider
diffFuzzyThreshold,
terminalShellIntegrationTimeout,
terminalShellIntegrationDisabled,
collapseCommandOutputByDefault,
terminalCommandDelay,
terminalPowershellCounter,
terminalZshClearEolMark,
Expand Down Expand Up @@ -2923,6 +2925,8 @@ export class ClineProvider
diffFuzzyThreshold: diffFuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD,
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,
terminalShellIntegrationDisabled: terminalShellIntegrationDisabled ?? true,
collapseCommandOutputByDefault:
collapseCommandOutputByDefault ?? DEFAULT_COLLAPSE_COMMAND_OUTPUT_BY_DEFAULT,
terminalCommandDelay: terminalCommandDelay ?? 0,
terminalPowershellCounter: terminalPowershellCounter ?? false,
terminalZshClearEolMark: terminalZshClearEolMark ?? true,
Expand Down Expand Up @@ -3159,6 +3163,8 @@ export class ClineProvider
terminalShellIntegrationTimeout:
stateValues.terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,
terminalShellIntegrationDisabled: stateValues.terminalShellIntegrationDisabled ?? true,
collapseCommandOutputByDefault:
stateValues.collapseCommandOutputByDefault ?? DEFAULT_COLLAPSE_COMMAND_OUTPUT_BY_DEFAULT,
terminalCommandDelay: stateValues.terminalCommandDelay ?? 0,
terminalPowershellCounter: stateValues.terminalPowershellCounter ?? false,
terminalZshClearEolMark: stateValues.terminalZshClearEolMark ?? true,
Expand Down
10 changes: 10 additions & 0 deletions src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2226,6 +2226,16 @@ describe("ClineProvider", () => {
expect(state.writeDelayMs).toBe(DEFAULT_WRITE_DELAY_MS)
})

test.each([true, false, undefined])(
"returns the saved output collapse preference (%s) to the webview",
async (value) => {
await provider.resolveWebviewView(mockWebviewView)
await provider.contextProxy.setValue("collapseCommandOutputByDefault", value)
expect((await provider.getState()).collapseCommandOutputByDefault).toBe(value ?? false)
expect((await provider.getStateToPostToWebview()).collapseCommandOutputByDefault).toBe(value ?? false)
},
)

test("getState applies fallback defaults for write, diff, and terminal settings", async () => {
// Mock globalState.get to return undefined for the fallback settings
// (typed reassignment — Memento.get has no mock type, same pattern as the
Expand Down
25 changes: 25 additions & 0 deletions src/core/webview/__tests__/webviewMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2971,6 +2971,31 @@ describe("webviewMessageHandler - telemetrySetting", () => {
})
})

describe("webviewMessageHandler - collapseCommandOutputByDefault", () => {
beforeEach(() => {
vi.clearAllMocks()
})

it.each([true, false, undefined])(
"persists the output collapse preference (%s) without changing terminal mode",
async (value) => {
await webviewMessageHandler(mockClineProvider, {
type: "updateSettings",
updatedSettings: { collapseCommandOutputByDefault: value },
})
expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith(
"collapseCommandOutputByDefault",
value,
)
expect(mockClineProvider.contextProxy.setValue).not.toHaveBeenCalledWith(
"terminalShellIntegrationDisabled",
expect.anything(),
)
expect(mockClineProvider.postStateToWebview).toHaveBeenCalledTimes(1)
},
)
})

describe("updateProfileModel", () => {
let updateProfileModel: Mock<ClineProvider["updateProfileModel"]>

Expand Down
27 changes: 27 additions & 0 deletions webview-ui/playwright/gallery/stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,33 @@ const mermaidGantt = `gantt
Ship release :active, release, after scope, 3d`

export const stories: Record<string, Story> = {
"terminal-settings": async () => {
const [{ AppProviders }, { TerminalSettings }] = await Promise.all([
import("../AppProviders"),
import("@/components/settings/TerminalSettings"),
])
function Settings() {
const [collapsed, setCollapsed] = useState(false)
return (
<div
className="w-[420px] p-4 bg-vscode-editor-background text-vscode-foreground"
data-testid="terminal-settings-story">
<TerminalSettings
terminalShellIntegrationDisabled={true}
collapseCommandOutputByDefault={collapsed}
setCachedStateField={(field, value) => {
if (field === "collapseCommandOutputByDefault") setCollapsed(Boolean(value))
}}
/>
</div>
)
}
return (
<AppProviders>
<Settings />
</AppProviders>
)
},
"bedrock-output-budget": async ({ model = "anthropic.claude-opus-5" }) => {
const [{ AppProviders }, { ThinkingBudget }, { bedrockModels, providerIdentifiers }] = await Promise.all([
import("../AppProviders"),
Expand Down
19 changes: 13 additions & 6 deletions webview-ui/src/components/chat/CommandExecution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import { t } from "i18next"
import { ChevronDown, OctagonX } from "lucide-react"

import { type ExtensionMessage, type CommandExecutionStatus, commandExecutionStatusSchema } from "@roo-code/types"
import {
type ExtensionMessage,
type CommandExecutionStatus,
commandExecutionStatusSchema,
DEFAULT_COLLAPSE_COMMAND_OUTPUT_BY_DEFAULT,
} from "@roo-code/types"

import { safeJsonParse } from "@roo/core"
import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences"
Expand Down Expand Up @@ -42,6 +47,7 @@
export const CommandExecution = ({ executionId, text, icon, title, isDenied = false }: CommandExecutionProps) => {
const {
terminalShellIntegrationDisabled = false,
collapseCommandOutputByDefault = DEFAULT_COLLAPSE_COMMAND_OUTPUT_BY_DEFAULT,
destructiveCommandGuardEnabled = false,
allowedCommands = [],
deniedCommands = [],
Expand All @@ -51,9 +57,8 @@

const { command, output: parsedOutput } = useMemo(() => parseCommandAndOutput(text), [text])

// If we aren't opening the VSCode terminal for this command then we default
// to expanding the command execution output.
const [isExpanded, setIsExpanded] = useState(terminalShellIntegrationDisabled)
// Apply the preference on mount. Manual toggles remain in control afterwards.
const [isExpanded, setIsExpanded] = useState(!collapseCommandOutputByDefault && terminalShellIntegrationDisabled)
const [streamingOutput, setStreamingOutput] = useState("")
// Initialize from the module-level cache so that components mounting after
// the "started" event was delivered still show the running indicator.
Expand Down Expand Up @@ -159,7 +164,9 @@
// Not a terminal state -- signals a mid-execution retry
// via execa after a shell integration failure. A new
// "started" event will follow, so leave the cache intact.
setIsExpanded(true)
if (!collapseCommandOutputByDefault) {
setIsExpanded(true)
}
break
case "output":
setStreamingOutput(data.output)
Expand All @@ -171,7 +178,7 @@
}
}
},
[executionId],
[executionId, collapseCommandOutputByDefault],

Check warning on line 181 in webview-ui/src/components/chat/CommandExecution.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/chat/CommandExecution.tsx:181: Survived ArrayDeclaration mutant (replacement: []). See the job summary for the complete list and resolution guidance.
)

useEvent("message", onMessage)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { act, fireEvent, screen, renderWithExtensionState } from "@/utils/test-utils"
import { CommandExecution } from "../CommandExecution"

vi.mock("@/utils/vscode", () => ({ vscode: { postMessage: vi.fn() } }))
vi.mock("../../common/CodeBlock", () => ({
default: ({ source }: { source: string }) => <div>{source}</div>,
}))
vi.mock("../TerminalOutput", () => ({
TerminalOutput: ({ content }: { content: string }) => <div data-testid="terminal-output">{content}</div>,
}))
vi.mock("../CommandPatternSelector", () => ({ CommandPatternSelector: () => null }))

const sendStatus = (data: Record<string, unknown>) => {
act(() =>
window.dispatchEvent(
new MessageEvent("message", {
data: {
type: "commandExecutionStatus",
text: JSON.stringify({ executionId: "collapse-test", ...data }),
},
}),
),
)
}
const setup = (collapseCommandOutputByDefault?: boolean, terminalShellIntegrationDisabled = true) =>
renderWithExtensionState(
<CommandExecution executionId="collapse-test" text="npm test\nOutput:\nInitial output" />,
{
state: { collapseCommandOutputByDefault, terminalShellIntegrationDisabled },
},
)
const outputContainer = () => screen.getByTestId("terminal-output").parentElement

describe("command output collapse preference", () => {
it.each([
[true, true, true],
[true, false, true],
[false, true, false],
[false, false, true],
[undefined, true, false],
[undefined, false, true],
])("applies preference %s with inline mode %s", (preference, inline, collapsed) => {
setup(preference, inline)
expect(outputContainer()).toHaveClass(collapsed ? "max-h-0" : "max-h-[100%]")
})
it("keeps streaming and completed output collapsed until manually expanded", () => {
setup(true)
sendStatus({ status: "started", pid: 123 })
sendStatus({ status: "output", output: "Streaming output" })
expect(screen.getByTestId("terminal-output")).toHaveTextContent("Streaming output")
expect(outputContainer()).toHaveClass("max-h-0")
fireEvent.click(screen.getAllByRole("button").at(-1)!)
expect(outputContainer()).not.toHaveClass("max-h-0")
sendStatus({ status: "output", output: "More output" })
sendStatus({ status: "exited", exitCode: 0 })
expect(outputContainer()).not.toHaveClass("max-h-0")
fireEvent.click(screen.getAllByRole("button").at(-1)!)
expect(outputContainer()).toHaveClass("max-h-0")
})

it("preserves a manual expansion through fallback and new output", () => {
setup(true, false)
fireEvent.click(screen.getAllByRole("button").at(-1)!)
sendStatus({ status: "fallback" })
sendStatus({ status: "output", output: "Fallback output" })
expect(outputContainer()).not.toHaveClass("max-h-0")
})
it.each([true, false, undefined])("respects preference %s during shell integration fallback", (preference) => {
setup(preference, false)
sendStatus({ status: "fallback" })
expect(outputContainer()).toHaveClass(preference ? "max-h-0" : "max-h-[100%]")
})
})
3 changes: 3 additions & 0 deletions webview-ui/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
terminalOutputPreviewSize,
terminalShellIntegrationTimeout,
terminalShellIntegrationDisabled, // Added from upstream
collapseCommandOutputByDefault,
terminalCommandDelay,
terminalPowershellCounter,
terminalZshClearEolMark,
Expand Down Expand Up @@ -416,6 +417,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
diffFuzzyThreshold,
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? 30_000,
terminalShellIntegrationDisabled,
collapseCommandOutputByDefault,
terminalCommandDelay,
terminalPowershellCounter,
terminalZshClearEolMark,
Expand Down Expand Up @@ -897,6 +899,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
terminalOutputPreviewSize={terminalOutputPreviewSize}
terminalShellIntegrationTimeout={terminalShellIntegrationTimeout}
terminalShellIntegrationDisabled={terminalShellIntegrationDisabled}
collapseCommandOutputByDefault={collapseCommandOutputByDefault}
terminalCommandDelay={terminalCommandDelay}
terminalPowershellCounter={terminalPowershellCounter}
terminalZshClearEolMark={terminalZshClearEolMark}
Expand Down
29 changes: 28 additions & 1 deletion webview-ui/src/components/settings/TerminalSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
import { useEvent, useMount } from "react-use"
import { Terminal } from "lucide-react"

import { type ExtensionMessage, type TerminalOutputPreviewSize } from "@roo-code/types"
import {
type ExtensionMessage,
type TerminalOutputPreviewSize,
DEFAULT_COLLAPSE_COMMAND_OUTPUT_BY_DEFAULT,
} from "@roo-code/types"

import { cn } from "@/lib/utils"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Slider, Button } from "@/components/ui"
Expand All @@ -21,6 +25,7 @@
terminalOutputPreviewSize?: TerminalOutputPreviewSize
terminalShellIntegrationTimeout?: number
terminalShellIntegrationDisabled?: boolean
collapseCommandOutputByDefault?: boolean
terminalCommandDelay?: number
terminalPowershellCounter?: boolean
terminalZshClearEolMark?: boolean
Expand All @@ -33,6 +38,7 @@
| "terminalOutputPreviewSize"
| "terminalShellIntegrationTimeout"
| "terminalShellIntegrationDisabled"
| "collapseCommandOutputByDefault"
| "terminalCommandDelay"
| "terminalPowershellCounter"
| "terminalZshClearEolMark"
Expand All @@ -51,6 +57,7 @@
terminalOutputPreviewSize,
terminalShellIntegrationTimeout,
terminalShellIntegrationDisabled,
collapseCommandOutputByDefault,
terminalCommandDelay,
terminalPowershellCounter,
terminalZshClearEolMark,
Expand Down Expand Up @@ -151,6 +158,26 @@
</div>
</div>

<SearchableSetting
settingId="terminal-collapse-command-output"
section="terminal"
label={t("settings:terminal.collapseCommandOutput.label")}>

Check warning on line 164 in webview-ui/src/components/settings/TerminalSettings.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/settings/TerminalSettings.tsx:164: Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.
<VSCodeCheckbox
checked={collapseCommandOutputByDefault ?? DEFAULT_COLLAPSE_COMMAND_OUTPUT_BY_DEFAULT}
onChange={(event) =>
setCachedStateField(
"collapseCommandOutputByDefault",
(event.target as HTMLInputElement).checked,
)
}
data-testid="terminal-collapse-command-output-checkbox">
<span className="font-medium">{t("settings:terminal.collapseCommandOutput.label")}</span>

Check warning on line 174 in webview-ui/src/components/settings/TerminalSettings.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/settings/TerminalSettings.tsx:174: Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:terminal.collapseCommandOutput.description")}

Check warning on line 177 in webview-ui/src/components/settings/TerminalSettings.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/settings/TerminalSettings.tsx:177: Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.
</div>
</SearchableSetting>

{/* Advanced Settings */}
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
Expand Down
Loading
Loading