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
25 changes: 24 additions & 1 deletion webview-ui/src/components/settings/CheckpointSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,25 @@ import {
MAX_CHECKPOINT_TIMEOUT_SECONDS,
MIN_CHECKPOINT_TIMEOUT_SECONDS,
DEFAULT_PER_WRITE_CHECKPOINTS,
DEFAULT_CHANGE_CARD_DETAIL,
type ChangeCardDetail,
} from "@roo-code/types"

type CheckpointSettingsProps = HTMLAttributes<HTMLDivElement> & {
enableCheckpoints?: boolean
checkpointTimeout?: number
perWriteCheckpoints?: boolean
setCachedStateField: SetCachedStateField<"enableCheckpoints" | "checkpointTimeout" | "perWriteCheckpoints">
changeCardDetail?: ChangeCardDetail
setCachedStateField: SetCachedStateField<
"enableCheckpoints" | "checkpointTimeout" | "perWriteCheckpoints" | "changeCardDetail"
>
}

export const CheckpointSettings = ({
enableCheckpoints,
checkpointTimeout,
perWriteCheckpoints,
changeCardDetail,
setCachedStateField,
...props
}: CheckpointSettingsProps) => {
Expand All @@ -52,6 +58,23 @@ export const CheckpointSettings = ({
</div>
</SearchableSetting>

<SearchableSetting
settingId="checkpoints-changeCardDetail"
section="checkpoints"
label={t("settings:checkpoints.changeCardDetail.label")}>
<VSCodeCheckbox
checked={(changeCardDetail ?? DEFAULT_CHANGE_CARD_DETAIL) === "full"}
onChange={(e: any) => {
setCachedStateField("changeCardDetail", e.target.checked ? "full" : "summary")
}}
data-testid="change-card-detail-checkbox">
<span className="font-medium">{t("settings:checkpoints.changeCardDetail.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:checkpoints.changeCardDetail.description")}
</div>
</SearchableSetting>

<SearchableSetting
settingId="checkpoints-enable"
section="checkpoints"
Expand Down
4 changes: 4 additions & 0 deletions webview-ui/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES,
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
DEFAULT_PER_WRITE_CHECKPOINTS,
DEFAULT_CHANGE_CARD_DETAIL,
ImageGenerationProvider,
} from "@roo-code/types"

Expand Down Expand Up @@ -177,6 +178,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
enableCheckpoints,
checkpointTimeout,
perWriteCheckpoints,
changeCardDetail,
experiments,
maxOpenTabsContext,
maxWorkspaceFiles,
Expand Down Expand Up @@ -413,6 +415,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
enableCheckpoints: enableCheckpoints ?? false,
checkpointTimeout: checkpointTimeout ?? DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
perWriteCheckpoints: perWriteCheckpoints ?? DEFAULT_PER_WRITE_CHECKPOINTS,
changeCardDetail: changeCardDetail ?? DEFAULT_CHANGE_CARD_DETAIL,
writeDelayMs,
diffFuzzyThreshold,
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? 30_000,
Expand Down Expand Up @@ -851,6 +854,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
enableCheckpoints={enableCheckpoints}
checkpointTimeout={checkpointTimeout}
perWriteCheckpoints={perWriteCheckpoints}
changeCardDetail={changeCardDetail}
setCachedStateField={setCachedStateField}
/>
)}
Expand Down
155 changes: 139 additions & 16 deletions webview-ui/src/components/settings/__tests__/CheckpointSettings.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// npx vitest src/components/settings/__tests__/CheckpointSettings.spec.tsx

import type { CSSProperties, ReactNode } from "react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { CheckpointSettings } from "../CheckpointSettings"

Expand All @@ -13,6 +14,12 @@ vi.mock("@/i18n/TranslationContext", () => ({
if (key === "settings:checkpoints.perWrite.description") {
return "Record a checkpoint snapshot after every successful file write by the agent"
}
if (key === "settings:checkpoints.changeCardDetail.label") {
return "Show full diff in change cards"
}
if (key === "settings:checkpoints.changeCardDetail.description") {
return "Include the full unified diff inline for every file in per-step change cards"
}
return key
},
}),
Expand All @@ -23,7 +30,17 @@ vi.mock("@/components/ui", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/components/ui")>()
return {
...actual,
Slider: ({ defaultValue, onValueChange, "data-testid": dataTestId }: any) => (
// Narrow typed double: only the props CheckpointSettings consumes, so
// drift in the Slider contract is a compile error here, not `any`.
Slider: ({
defaultValue,
onValueChange,
"data-testid": dataTestId,
}: {
defaultValue?: number[]
onValueChange?: (value: number[]) => void
"data-testid"?: string
}) => (
<input
type="range"
value={defaultValue?.[0] ?? 0}
Expand All @@ -43,21 +60,48 @@ vi.mock("@/utils/vscode", () => ({
}))

// Mock VSCode components to behave like standard HTML elements
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
VSCodeCheckbox: ({ checked, onChange, children, ...props }: any) => (
<label {...props}>
<input
type="checkbox"
role="checkbox"
checked={checked || false}
aria-checked={checked || false}
onChange={(e: any) => onChange?.({ target: { checked: e.target.checked } })}
/>
{children}
</label>
),
VSCodeLink: ({ children, ...props }: any) => <a {...props}>{children}</a>,
}))
vi.mock("@vscode/webview-ui-toolkit/react", () => {
// Narrow event double: the real toolkit dispatches a native Event whose
// currentTarget is the web component with a boolean `checked`; the mock
// forwards the input's checked state on both target and currentTarget so
// handlers can be typed against either surface.
type CheckboxChangeEvent = {
target: { checked: boolean }
currentTarget: { checked: boolean }
}
return {
VSCodeCheckbox: ({
checked,
onChange,
children,
"data-testid": dataTestId,
}: {
checked?: boolean
onChange?: (e: CheckboxChangeEvent) => void
children?: ReactNode
"data-testid"?: string
}) => (
<label data-testid={dataTestId}>
<input
type="checkbox"
role="checkbox"
checked={checked || false}
aria-checked={checked || false}
onChange={(e) => {
const value = e.currentTarget.checked
onChange?.({ target: { checked: value }, currentTarget: { checked: value } })
}}
/>
{children}
</label>
),
VSCodeLink: ({ children, href, style }: { children?: ReactNode; href?: string; style?: CSSProperties }) => (
<a href={href} style={style}>
{children}
</a>
),
}
})

describe("CheckpointSettings", () => {
const setCachedStateField = vi.fn()
Expand Down Expand Up @@ -122,4 +166,83 @@ describe("CheckpointSettings", () => {

expect(setCachedStateField).toHaveBeenCalledWith("perWriteCheckpoints", false)
})

it("renders the change card detail checkbox unchecked by default when the value is unset", () => {
render(<CheckpointSettings enableCheckpoints={false} setCachedStateField={setCachedStateField} />)

const checkbox = screen.getByRole("checkbox", { name: "Show full diff in change cards" })
expect(checkbox).not.toBeChecked()
})

it("renders the change card detail checkbox checked when the saved value is full", () => {
render(
<CheckpointSettings
enableCheckpoints={false}
changeCardDetail="full"
setCachedStateField={setCachedStateField}
/>,
)

const checkbox = screen.getByRole("checkbox", { name: "Show full diff in change cards" })
expect(checkbox).toBeChecked()
})

it("renders the change card detail checkbox unchecked when the saved value is summary", () => {
render(
<CheckpointSettings
enableCheckpoints={false}
changeCardDetail="summary"
setCachedStateField={setCachedStateField}
/>,
)

const checkbox = screen.getByRole("checkbox", { name: "Show full diff in change cards" })
expect(checkbox).not.toBeChecked()
})

it("caches the changeCardDetail full value when the user checks the box", () => {
render(
<CheckpointSettings
enableCheckpoints={false}
changeCardDetail="summary"
setCachedStateField={setCachedStateField}
/>,
)

const checkbox = screen.getByRole("checkbox", { name: "Show full diff in change cards" })
fireEvent.click(checkbox)

expect(setCachedStateField).toHaveBeenCalledWith("changeCardDetail", "full")
})

it("caches the changeCardDetail summary value when the user unchecks the box", () => {
render(
<CheckpointSettings
enableCheckpoints={false}
changeCardDetail="full"
setCachedStateField={setCachedStateField}
/>,
)

const checkbox = screen.getByRole("checkbox", { name: "Show full diff in change cards" })
fireEvent.click(checkbox)

expect(setCachedStateField).toHaveBeenCalledWith("changeCardDetail", "summary")
})

it("indexes the change card detail setting with its translated label for search", () => {
render(<CheckpointSettings enableCheckpoints={false} setCachedStateField={setCachedStateField} />)

const setting = document.querySelector('[data-setting-id="checkpoints-changeCardDetail"]')
expect(setting).not.toBeNull()
expect(setting?.getAttribute("data-setting-label")).toBe("Show full diff in change cards")
})

it("shows the change card detail description text", () => {
render(<CheckpointSettings enableCheckpoints={false} setCachedStateField={setCachedStateField} />)

expect(
screen.getByText("Include the full unified diff inline for every file in per-step change cards"),
).toBeInTheDocument()
})
})
69 changes: 69 additions & 0 deletions webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,75 @@ describe("SettingsView - Sound Settings", () => {
)
})

it("saves the changeCardDetail full selection on save", async () => {
const { activateTab, getSettingsContent } = renderSettingsView({
settingsImportedAt: new Date().toISOString(),
})

activateTab("checkpoints")
const content = getSettingsContent()
const checkbox = await within(content).findByTestId("change-card-detail-checkbox")
expect(checkbox).not.toBeChecked()

fireEvent.click(checkbox)
fireEvent.click(screen.getByTestId("save-button"))

await waitFor(() =>
expect(vscode.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: "updateSettings",
updatedSettings: expect.objectContaining({ changeCardDetail: "full" }),
}),
),
)
})

it("saves the changeCardDetail summary default when the value is unset", async () => {
const { activateTab, getSettingsContent } = renderSettingsView({
settingsImportedAt: new Date().toISOString(),
})

activateTab("checkpoints")
const content = getSettingsContent()
const checkbox = await within(content).findByTestId("change-card-detail-checkbox")
expect(checkbox).not.toBeChecked()

fireEvent.click(screen.getByTestId("save-button"))

await waitFor(() =>
expect(vscode.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: "updateSettings",
updatedSettings: expect.objectContaining({ changeCardDetail: "summary" }),
}),
),
)
})

it("reflects the saved changeCardDetail full value in the checkbox and saves summary when unchecked", async () => {
const { activateTab, getSettingsContent } = renderSettingsView({
changeCardDetail: "full",
settingsImportedAt: new Date().toISOString(),
})

activateTab("checkpoints")
const content = getSettingsContent()
const checkbox = await within(content).findByTestId("change-card-detail-checkbox")
await waitFor(() => expect(checkbox).toBeChecked())

fireEvent.click(checkbox)
fireEvent.click(screen.getByTestId("save-button"))

await waitFor(() =>
expect(vscode.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: "updateSettings",
updatedSettings: expect.objectContaining({ changeCardDetail: "summary" }),
}),
),
)
})

it("shows tts slider when sound is enabled", () => {
// Render once and get the activateTab helper
const { activateTab, getSettingsContent } = renderSettingsView()
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 @@ -213,6 +213,7 @@ export const createInitialExtensionState = (): ExtensionState => ({
ttsSpeed: 1.0,
enableCheckpoints: true,
perWriteCheckpoints: true,
changeCardDetail: "summary",
checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, // Default to 15 seconds
language: "en", // Default language code
writeDelayMs: 1000,
Expand Down
Loading
Loading