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
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export const globalSettingsSchema = z.object({
maxOpenTabsContext: z.number().optional(),
maxWorkspaceFiles: z.number().optional(),
showRooIgnoredFiles: z.boolean().optional(),
compactToolUI: z.boolean().optional(),
enableSubfolderRules: z.boolean().optional(),
maxImageFileSize: z.number().optional(),
maxTotalImageSize: z.number().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 @@ -320,6 +320,7 @@ export type ExtensionState = Pick<
maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500)
maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500)
showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings
compactToolUI?: boolean // Whether to collapse executed tool blocks to a single expandable line (#322)
enableSubfolderRules: boolean // Whether to load rules from subdirectories
maxReadFileLine?: number // Maximum line limit for read_file tool (-1 for default)
maxImageFileSize: number // Maximum size of image files to process in MB
Expand Down
3 changes: 3 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2065,6 +2065,7 @@ export class ClineProvider
disabledTools,
telemetrySetting,
showRooIgnoredFiles,
compactToolUI,
enableSubfolderRules,
language,
maxImageFileSize,
Expand Down Expand Up @@ -2221,6 +2222,7 @@ export class ClineProvider
telemetryKey,
machineId,
showRooIgnoredFiles: showRooIgnoredFiles ?? false,
compactToolUI: compactToolUI ?? false,
enableSubfolderRules: enableSubfolderRules ?? false,
language: language ?? formatLanguage(vscode.env.language),
renderContext: this.renderContext,
Expand Down Expand Up @@ -2423,6 +2425,7 @@ export class ClineProvider
disabledTools: stateValues.disabledTools,
telemetrySetting: stateValues.telemetrySetting || "unset",
showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? false,
compactToolUI: stateValues.compactToolUI ?? false,
enableSubfolderRules: stateValues.enableSubfolderRules ?? false,
maxImageFileSize: stateValues.maxImageFileSize ?? 5,
maxTotalImageSize: stateValues.maxTotalImageSize ?? 20,
Expand Down
35 changes: 33 additions & 2 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import {
Split,
ArrowRight,
Check,
ChevronRight,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { PathTooltip } from "../ui/PathTooltip"
Expand Down Expand Up @@ -182,8 +183,16 @@ export const ChatRowContent = ({
}: ChatRowContentProps) => {
const { t, i18n } = useTranslation()

const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, clineMessages, currentTaskItem } =
useExtensionState()
const {
mcpServers,
alwaysAllowMcp,
currentCheckpoint,
mode,
apiConfiguration,
clineMessages,
currentTaskItem,
compactToolUI,
} = useExtensionState()
const { info: model } = useSelectedModel(apiConfiguration)
const [isEditing, setIsEditing] = useState(false)
const [editedContent, setEditedContent] = useState("")
Expand Down Expand Up @@ -1405,6 +1414,28 @@ export const ChatRowContent = ({
const sayTool = safeJsonParse<ClineSayTool>(message.text)
if (!sayTool) return null

// Compact mode (#322): collapse executed (history) tool blocks to a single
// clickable line, hiding verbose params/payloads until expanded. Only applies
// to `say` tool rows here — approval prompts (`ask`) are never compacted.
if (compactToolUI && !isExpanded) {
Comment thread
edelauna marked this conversation as resolved.
const compactLabel = sayTool.path ? `${sayTool.tool}: ${sayTool.path}` : sayTool.tool
return (
<button
type="button"
onClick={handleToggleExpand}
aria-expanded={false}
className="flex items-center gap-2 py-0.5 cursor-pointer text-vscode-descriptionForeground hover:text-vscode-foreground bg-transparent border-none text-inherit w-full text-left"
data-testid="compact-tool-row"
title={t("chat:compactTool.expandHint")}>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would adding aria-expanded={false} here round out the a11y fix? Since this button only renders in the collapsed state, the value is always false, but it signals to screen readers that this is an expandable control.

Suggested change
title={t("chat:compactTool.expandHint")}>
title={t("chat:compactTool.expandHint")}
aria-expanded={false}>

<ChevronRight className="w-3.5 h-3.5 shrink-0" />
<PocketKnife className="w-3.5 h-3.5 shrink-0" />
<span className="truncate text-sm">
{t("chat:compactTool.label", { tool: compactLabel })}
</span>
</button>
)
}

switch (sayTool.tool) {
case "runSlashCommand": {
const slashCommandInfo = sayTool
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React from "react"
import { fireEvent, render, screen } from "@/utils/test-utils"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import type { ClineMessage } from "@roo-code/types"
import { ChatRowContent } from "../ChatRow"

const mockPostMessage = vi.fn()
const mockOnToggleExpand = vi.fn()

vi.mock("@src/utils/vscode", () => ({
vscode: {
postMessage: (...args: unknown[]) => mockPostMessage(...args),
},
}))

// Mock i18n
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, opts?: Record<string, string>) => {
const map: Record<string, string> = {
"chat:compactTool.expandHint": "Click to expand",
"chat:compactTool.label": opts?.tool ? `tool: ${opts.tool}` : "tool",
}
return map[key] || key
},
}),
Trans: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
initReactI18next: { type: "3rdParty", init: () => {} },
}))

// Mock CodeBlock (avoid ESM/highlighter costs)
vi.mock("@src/components/common/CodeBlock", () => ({
default: () => null,
}))

// Mock useExtensionState to enable compactToolUI
vi.mock("@src/context/ExtensionStateContext", () => ({
useExtensionState: () => ({
mcpServers: [],
alwaysAllowMcp: false,
currentCheckpoint: undefined,
mode: "code",
apiConfiguration: {},
clineMessages: [],
currentTaskItem: undefined,
compactToolUI: true,
}),
ExtensionStateContextProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}))

const queryClient = new QueryClient()

function createSayToolMessage(toolPayload: Record<string, unknown>): ClineMessage {
return {
type: "say",
say: "tool" as any,
ts: Date.now(),
text: JSON.stringify(toolPayload),
}
}

function renderChatRow(message: ClineMessage, isExpanded = false) {
return render(
<QueryClientProvider client={queryClient}>
<ChatRowContent
message={message}
isExpanded={isExpanded}
isLast={false}
isStreaming={false}
onToggleExpand={mockOnToggleExpand}
onSuggestionClick={() => {}}
onBatchFileResponse={() => {}}
onFollowUpUnmount={() => {}}
isFollowUpAnswered={false}
/>
</QueryClientProvider>,
)
}

describe("ChatRow - compact tool UI", () => {
beforeEach(() => {
vi.clearAllMocks()
mockOnToggleExpand.mockClear()
})

it("renders the compact row when compactToolUI is true and not expanded", () => {
const message = createSayToolMessage({ tool: "readFile", path: "src/file.ts" })
renderChatRow(message, false)

expect(screen.getByTestId("compact-tool-row")).toBeInTheDocument()
})

it("calls onToggleExpand when the compact row is clicked", () => {
const message = createSayToolMessage({ tool: "readFile", path: "src/file.ts" })
renderChatRow(message, false)

fireEvent.click(screen.getByTestId("compact-tool-row"))

expect(mockOnToggleExpand).toHaveBeenCalledWith(message.ts)
})

it("has aria-expanded=false on the compact button", () => {
const message = createSayToolMessage({ tool: "readFile", path: "src/file.ts" })
renderChatRow(message, false)

expect(screen.getByTestId("compact-tool-row")).toHaveAttribute("aria-expanded", "false")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type ContextManagementSettingsProps = HTMLAttributes<HTMLDivElement> & {
maxOpenTabsContext: number
maxWorkspaceFiles: number
showRooIgnoredFiles?: boolean
compactToolUI?: boolean
enableSubfolderRules?: boolean
maxImageFileSize?: number
maxTotalImageSize?: number
Expand All @@ -50,6 +51,7 @@ type ContextManagementSettingsProps = HTMLAttributes<HTMLDivElement> & {
| "maxOpenTabsContext"
| "maxWorkspaceFiles"
| "showRooIgnoredFiles"
| "compactToolUI"
| "enableSubfolderRules"
| "maxImageFileSize"
| "maxTotalImageSize"
Expand All @@ -70,6 +72,7 @@ export const ContextManagementSettings = ({
maxOpenTabsContext,
maxWorkspaceFiles,
showRooIgnoredFiles,
compactToolUI,
enableSubfolderRules,
setCachedStateField,
maxImageFileSize,
Expand Down Expand Up @@ -229,6 +232,23 @@ export const ContextManagementSettings = ({
</div>
</SearchableSetting>

<SearchableSetting
settingId="context-compact-tool-ui"
section="contextManagement"
label={t("settings:contextManagement.compactToolUI.label")}>
<VSCodeCheckbox
checked={compactToolUI}
onChange={(e: any) => setCachedStateField("compactToolUI", e.target.checked)}
data-testid="compact-tool-ui-checkbox">
<label className="block font-medium mb-1">
{t("settings:contextManagement.compactToolUI.label")}
</label>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-3">
{t("settings:contextManagement.compactToolUI.description")}
</div>
</SearchableSetting>

<SearchableSetting
settingId="context-enable-subfolder-rules"
section="contextManagement"
Expand Down
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 @@ -185,6 +185,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
terminalZdotdir,
writeDelayMs,
showRooIgnoredFiles,
compactToolUI,
enableSubfolderRules,
maxImageFileSize,
maxTotalImageSize,
Expand Down Expand Up @@ -401,6 +402,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
maxOpenTabsContext: Math.min(Math.max(0, maxOpenTabsContext ?? 20), 500),
maxWorkspaceFiles: Math.min(Math.max(0, maxWorkspaceFiles ?? 200), 500),
showRooIgnoredFiles: showRooIgnoredFiles ?? true,
compactToolUI: compactToolUI ?? false,
enableSubfolderRules: enableSubfolderRules ?? false,
maxImageFileSize: maxImageFileSize ?? 5,
maxTotalImageSize: maxTotalImageSize ?? 20,
Expand Down Expand Up @@ -834,6 +836,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
maxOpenTabsContext={maxOpenTabsContext}
maxWorkspaceFiles={maxWorkspaceFiles ?? 200}
showRooIgnoredFiles={showRooIgnoredFiles}
compactToolUI={compactToolUI ?? false}
enableSubfolderRules={enableSubfolderRules}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
maxImageFileSize={maxImageFileSize}
maxTotalImageSize={maxTotalImageSize}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ describe("ContextManagementSettings", () => {
maxOpenTabsContext: 20,
maxWorkspaceFiles: 200,
showRooIgnoredFiles: false,
compactToolUI: false,
profileThresholds: {},
includeDiagnosticMessages: true,
maxDiagnosticMessages: 50,
Expand Down Expand Up @@ -150,6 +151,23 @@ describe("ContextManagementSettings", () => {
})
})

it("renders the compact tool UI toggle", () => {
render(<ContextManagementSettings {...defaultProps} />)
expect(screen.getByTestId("compact-tool-ui-checkbox")).toBeInTheDocument()
})

it("calls setCachedStateField when the compact tool UI checkbox is toggled", async () => {
const setCachedStateField = vi.fn()
render(<ContextManagementSettings {...defaultProps} setCachedStateField={setCachedStateField} />)

const checkbox = screen.getByTestId("compact-tool-ui-checkbox").querySelector("input")!
fireEvent.click(checkbox)

await waitFor(() => {
expect(setCachedStateField).toHaveBeenCalledWith("compactToolUI", true)
})
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These cover the settings toggle nicely. Would it be worth also adding a test that mounts ChatRowContent with compactToolUI=true and a say tool message, to verify the compact row renders (and that clicking it calls onToggleExpand)? That would give a regression guard on the actual chat-side behaviour.


it("calls setCachedStateField when max diagnostic messages slider is changed", async () => {
const setCachedStateField = vi.fn()
render(<ContextManagementSettings {...defaultProps} setCachedStateField={setCachedStateField} />)
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
cwd: "",
telemetrySetting: "unset",
showRooIgnoredFiles: true, // Default to showing .rooignore'd files with lock symbol (current behavior).
compactToolUI: false, // Default off — opt-in single-line collapsed tool blocks (#322).
Comment thread
edelauna marked this conversation as resolved.
enableSubfolderRules: false, // Default to disabled - must be enabled to load rules from subdirectories
renderContext: "sidebar",
maxReadFileLine: -1, // Default max line limit for read_file tool (-1 for default)
Expand Down
4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/ca/chat.json

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

4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/ca/settings.json

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

4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/de/chat.json

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

4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/de/settings.json

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

4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/en/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -476,5 +476,9 @@
"title": "Provider no longer supported",
"message": "This provider is no longer available. Select a supported provider to continue.",
"openSettings": "Open Settings"
},
"compactTool": {
"label": "Tool: {{tool}}",
"expandHint": "Click to expand tool details"
}
}
4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,10 @@
"enableSubfolderRules": {
"label": "Enable subfolder rules",
"description": "Recursively discover and load .roo/rules and AGENTS.md files from subdirectories. Useful for monorepos with per-package rules."
},
"compactToolUI": {
"label": "Compact tool display",
"description": "Collapse executed tool blocks to a single line in the chat; click a tool to expand its details. Approval prompts are always shown in full."
}
},
"terminal": {
Expand Down
4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/es/chat.json

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

Loading
Loading