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
24 changes: 24 additions & 0 deletions webview-ui/src/components/chat/ChangeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ export const ChangeCard = ({ message }: { message: ClineMessage }) => {
setStepRollback({ status: "pending" })
}

// Open the changed file in the editor. The extension host resolves relative
// paths against the current cwd (webviewMessageHandler "openFile"), so
// normalize the "./" prefix the same way FileChangesPanel does.
const openFileInEditor = (path: string) => {
vscode.postMessage({ type: "openFile", text: path.startsWith("./") ? path : "./" + path })
}

const diffBadges = (additions: number, deletions: number) =>
additions > 0 || deletions > 0 ? (
<span className="flex items-center gap-2 shrink-0" aria-hidden>
Expand Down Expand Up @@ -306,6 +313,7 @@ export const ChangeCard = ({ message }: { message: ClineMessage }) => {
language="diff"
isExpanded={expandedFiles.has(file.path)}
onToggleExpand={() => toggleFile(file.path)}
onJumpToFile={() => openFileInEditor(file.path)}
diffStats={{ added: file.additions, removed: file.deletions }}
/>
) : (
Expand All @@ -315,6 +323,22 @@ export const ChangeCard = ({ message }: { message: ClineMessage }) => {
</span>
<span className="grow" />
{diffBadges(file.additions, file.deletions)}
{/* Native button (not a span) so keyboard users can open the
file from a compact row: Enter/Space activate it for free. */}
<Button
variant="ghost"
size="icon"
aria-label={t("chat:changeCard.openFile")}
title={t("chat:changeCard.openFile")}
data-testid={`change-card-file-open-${index}`}
onClick={() => openFileInEditor(file.path)}>
<span
className="codicon codicon-link-external"
// Stryker disable next-line ObjectLiteral : decorative icon font size; the open-file affordance is asserted via tagName/aria-label/title and covered by visual-regression baselines
style={{ fontSize: 13.5 }}
aria-hidden
/>
</Button>
</div>
)}
</div>
Expand Down
62 changes: 62 additions & 0 deletions webview-ui/src/components/chat/__tests__/ChangeCard.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ vi.mock("react-i18next", () => ({
"chat:changeCard.rolledBack": "Rolled back",
"chat:changeCard.stepRolledBack": "Step rolled back",
"chat:changeCard.rollbackFailed": "Rollback failed",
"chat:changeCard.openFile": "Open file",
}
return map[key] || key
},
Expand Down Expand Up @@ -341,6 +342,67 @@ describe("ChangeCard", () => {
expect(screen.getByTestId("change-card-step-rollback")).toBeInTheDocument()
expect(mockPostMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "checkpointRollbackStep" }))
})

it("posts an openFile message from both the diff-row jump icon and the no-diff-row button", () => {
renderWithExtensionState(
<ChangeCard
message={makeCardMessage({
detail: "full",
files: [
{ path: "src/a.ts", additions: 1, deletions: 1, diff: DIFF_A },
{ path: "src/b.ts", additions: 1, deletions: 1 },
],
totalFiles: 2,
})}
/>,
)

// Diff row: the CodeAccordion header jump icon (own aria-label).
fireEvent.click(screen.getByLabelText("Open file: src/a.ts"))
expect(mockPostMessage).toHaveBeenCalledWith({ type: "openFile", text: "./src/a.ts" })
mockPostMessage.mockClear()

// No-diff row: the open control on the plain path row.
fireEvent.click(screen.getByTestId("change-card-file-open-1"))
expect(mockPostMessage).toHaveBeenCalledWith({ type: "openFile", text: "./src/b.ts" })
})

it("does not double-prefix paths that already carry the ./ marker", () => {
renderWithExtensionState(
<ChangeCard
message={makeCardMessage({
detail: "summary",
files: [{ path: "./src/c.ts", additions: 1, deletions: 1 }],
totalFiles: 1,
})}
/>,
)

fireEvent.click(screen.getByTestId("change-card-file-open-0"))
expect(mockPostMessage).toHaveBeenCalledWith({ type: "openFile", text: "./src/c.ts" })
})

it("renders the no-diff row open control as a native button so keyboard users can activate it", () => {
renderWithExtensionState(
<ChangeCard
message={makeCardMessage({
detail: "summary",
files: [{ path: "src/b.ts", additions: 1, deletions: 1 }],
totalFiles: 1,
})}
/>,
)

// A native <button> gets Enter/Space activation from platform semantics;
// the previous span role=button had no keydown handler, so keyboard users
// could not open the file from a compact row. (jsdom does not implement
// button activation behavior, so the accessibility contract is asserted on
// the element itself; the mouse path is covered by the click tests above.)
const control = screen.getByTestId("change-card-file-open-0")
expect(control.tagName).toBe("BUTTON")
expect(control).toHaveAttribute("aria-label", "Open file")
expect(control).toHaveAttribute("title", "Open file")
})
})

describe("ChatRow - change_card say", () => {
Expand Down
29 changes: 22 additions & 7 deletions webview-ui/src/components/settings/CheckpointSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HTMLAttributes } from "react"
import { HTMLAttributes, type FormEvent } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { Trans } from "react-i18next"
Expand All @@ -18,6 +18,14 @@ import {
type ChangeCardDetail,
} from "@roo-code/types"

/**
* The `checked` state of a VSCodeCheckbox change event. The real toolkit
* dispatches a native `Event` whose current target is the web component;
* the test mock forwards a synthetic event on the underlying input. Both
* expose a boolean `checked` on the current target.
*/
type CheckboxEventTarget = { checked?: boolean }

type CheckpointSettingsProps = HTMLAttributes<HTMLDivElement> & {
enableCheckpoints?: boolean
checkpointTimeout?: number
Expand Down Expand Up @@ -47,9 +55,12 @@ export const CheckpointSettings = ({
section="checkpoints"
label={t("settings:checkpoints.perWrite.label")}>
<VSCodeCheckbox
data-testid="per-write-checkbox"
checked={perWriteCheckpoints ?? DEFAULT_PER_WRITE_CHECKPOINTS}
onChange={(e: any) => {
setCachedStateField("perWriteCheckpoints", e.target.checked)
onChange={(e: Event | FormEvent<HTMLElement>) => {
const target = e.currentTarget as CheckboxEventTarget | null
// Stryker disable next-line OptionalChaining : currentTarget is non-null for every dispatched change event; the ?. guard covers pre-React-17 pooled-event semantics only, unobservable in tests
setCachedStateField("perWriteCheckpoints", target?.checked === true)
}}>
<span className="font-medium">{t("settings:checkpoints.perWrite.label")}</span>
</VSCodeCheckbox>
Expand All @@ -64,8 +75,10 @@ export const CheckpointSettings = ({
label={t("settings:checkpoints.changeCardDetail.label")}>
<VSCodeCheckbox
checked={(changeCardDetail ?? DEFAULT_CHANGE_CARD_DETAIL) === "full"}
onChange={(e: any) => {
setCachedStateField("changeCardDetail", e.target.checked ? "full" : "summary")
onChange={(e: Event | FormEvent<HTMLElement>) => {
const target = e.currentTarget as CheckboxEventTarget | null
// Stryker disable next-line OptionalChaining : currentTarget is non-null for every dispatched change event; the ?. guard covers pre-React-17 pooled-event semantics only, unobservable in tests
setCachedStateField("changeCardDetail", target?.checked === true ? "full" : "summary")
}}
data-testid="change-card-detail-checkbox">
<span className="font-medium">{t("settings:checkpoints.changeCardDetail.label")}</span>
Expand All @@ -81,8 +94,10 @@ export const CheckpointSettings = ({
label={t("settings:checkpoints.enable.label")}>
<VSCodeCheckbox
checked={enableCheckpoints}
onChange={(e: any) => {
setCachedStateField("enableCheckpoints", e.target.checked)
onChange={(e: Event | FormEvent<HTMLElement>) => {
const target = e.currentTarget as CheckboxEventTarget | null
// Stryker disable next-line OptionalChaining : currentTarget is non-null for every dispatched change event; the ?. guard covers pre-React-17 pooled-event semantics only, unobservable in tests
setCachedStateField("enableCheckpoints", target?.checked === true)
}}>
<span className="font-medium">{t("settings:checkpoints.enable.label")}</span>
</VSCodeCheckbox>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ vi.mock("@/i18n/TranslationContext", () => ({
if (key === "settings:checkpoints.changeCardDetail.description") {
return "Include the full unified diff inline for every file in per-step change cards"
}
if (key === "settings:checkpoints.enable.label") {
return "Enable automatic checkpoints"
}
return key
},
}),
Expand Down Expand Up @@ -245,4 +248,20 @@ describe("CheckpointSettings", () => {
screen.getByText("Include the full unified diff inline for every file in per-step change cards"),
).toBeInTheDocument()
})

it("caches the enableCheckpoints value when the user checks the enable checkbox", () => {
render(<CheckpointSettings enableCheckpoints={false} setCachedStateField={setCachedStateField} />)

const checkbox = screen.getByRole("checkbox", { name: "Enable automatic checkpoints" })
fireEvent.click(checkbox)
expect(setCachedStateField).toHaveBeenCalledWith("enableCheckpoints", true)
})

it("caches the enableCheckpoints false value when the user unchecks the enable checkbox", () => {
render(<CheckpointSettings enableCheckpoints={true} setCachedStateField={setCachedStateField} />)

const checkbox = screen.getByRole("checkbox", { name: "Enable automatic checkpoints" })
fireEvent.click(checkbox)
expect(setCachedStateField).toHaveBeenCalledWith("enableCheckpoints", false)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange({ target: { checked: e.target.checked } })}
onChange={(e) => {
// Mirror the real web component: the change event carries the
// checked state on both target and currentTarget.
const value = e.currentTarget.checked
onChange({ target: { checked: value }, currentTarget: { checked: value } })
}}
aria-label={typeof children === "string" ? children : undefined}
data-testid={dataTestId}
/>
Expand Down
3 changes: 2 additions & 1 deletion 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.

3 changes: 2 additions & 1 deletion 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.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/en/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@
"rollingBack": "Rolling back...",
"rolledBack": "Rolled back",
"stepRolledBack": "Step rolled back",
"rollbackFailed": "Rollback failed"
"rollbackFailed": "Rollback failed",
"openFile": "Open file"
},
"checkpoint": {
"regular": "Checkpoint",
Expand Down
3 changes: 2 additions & 1 deletion 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.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/fr/chat.json

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

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/hi/chat.json

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

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/id/chat.json

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

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/it/chat.json

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

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/ja/chat.json

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

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/ko/chat.json

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

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/nl/chat.json

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

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/pl/chat.json

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

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/pt-BR/chat.json

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

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/ru/chat.json

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

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/tr/chat.json

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

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/vi/chat.json

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

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/zh-CN/chat.json

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

Loading
Loading