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
10 changes: 8 additions & 2 deletions webview-ui/playwright/gallery/stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,16 @@ export const stories: Record<string, Story> = {
</AppProviders>
)
},
"auto-approve-settings": async () => {
"auto-approve-settings": async ({ followupAutoApproveTimeoutMs }) => {
const { AutoApproveSettingsStory } =
await import("@/components/settings/__tests__/AutoApproveSettings.visual.fixture")
return <AutoApproveSettingsStory />
return (
<AutoApproveSettingsStory
followupAutoApproveTimeoutMs={
typeof followupAutoApproveTimeoutMs === "number" ? followupAutoApproveTimeoutMs : undefined
}
/>
)
},
"chat-text-area": async () => {
const { ChatTextAreaStory } = await import("@/components/chat/__tests__/ChatTextArea.visual.fixture")
Expand Down
7 changes: 7 additions & 0 deletions webview-ui/src/components/chat/FollowUpSuggest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@
? followupAutoApproveTimeoutMs
: DEFAULT_FOLLOWUP_TIMEOUT_MS

// A timeout of 0 (or negative) means "never auto-select": don't start a
// countdown at all, and let the user choose an answer manually.
if (timeoutMs <= 0) {
setCountdown(null)

Check warning on line 65 in webview-ui/src/components/chat/FollowUpSuggest.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/chat/FollowUpSuggest.tsx:65: Survived CallExpression mutant (replacement: ;). See the job summary for the complete list and resolution guidance.
return () => onCancelAutoApproval?.()

Check warning on line 66 in webview-ui/src/components/chat/FollowUpSuggest.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/chat/FollowUpSuggest.tsx:66: Survived OptionalChaining mutant (replacement: onCancelAutoApproval()). See the job summary for the complete list and resolution guidance.
}

// Convert milliseconds to seconds for the countdown
setCountdown(Math.floor(timeoutMs / 1000))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,55 @@ describe("FollowUpSuggest", () => {
expect(screen.getByText(/Selecting in 3s/)).toBeInTheDocument()
})

it("should not show countdown and should not auto-select when timeout is 0", () => {
const testState: TestExtensionState = {
...defaultTestState,
followupAutoApproveTimeoutMs: 0,
}

const { unmount } = renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
/>,
testState,
)

// A timeout of 0 means "never auto-select": no countdown should be shown.
expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument()

// Advancing time must not trigger the auto-selection callback.
act(() => {
vi.advanceTimersByTime(10000)
})
expect(mockOnSuggestionClick).not.toHaveBeenCalled()

// Unmounting still notifies the caller that no auto-approval is pending.
unmount()
expect(mockOnCancelAutoApproval).toHaveBeenCalled()
})

it("should not show countdown for negative timeout values", () => {
const testState: TestExtensionState = {
...defaultTestState,
followupAutoApproveTimeoutMs: -1000,
}

renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
/>,
testState,
)

expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument()
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("should not display countdown timer when isAnswered is true", () => {
renderWithTestProviders(
<FollowUpSuggest
Expand Down
6 changes: 4 additions & 2 deletions webview-ui/src/components/settings/AutoApproveSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export const AutoApproveSettings = ({
label={t("settings:autoApprove.followupQuestions.timeoutLabel")}>
<div className="flex items-center gap-2">
<Slider
min={1000}
min={0}
max={300000}
step={1000}
value={[followupAutoApproveTimeoutMs]}
Expand All @@ -318,7 +318,9 @@ export const AutoApproveSettings = ({
<span className="w-20">{followupAutoApproveTimeoutMs / 1000}s</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.followupQuestions.timeoutLabel")}
{followupAutoApproveTimeoutMs === 0
? t("settings:autoApprove.followupQuestions.timeoutZeroDescription")
: t("settings:autoApprove.followupQuestions.timeoutLabel")}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
</SearchableSetting>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import React from "react"

import { within } from "@testing-library/react"

import { render, screen, fireEvent } from "@/utils/test-utils"

import { AutoApproveSettings } from "../AutoApproveSettings"
Expand Down Expand Up @@ -354,3 +356,38 @@ describe("AutoApproveSettings - Save/Discard contract", () => {
expectNoImmediateUpdateSettings()
})
})

// The follow-up timeout slider is exercised here rather than in the visual suite:
// behavior lives in Vitest and *.visual.tsx keeps only a focused screenshot.
describe("AutoApproveSettings - follow-up timeout slider", () => {
// The Radix Slider root carries data-testid; the element with role="slider" is
// its thumb, which owns aria-valuemin/aria-valuenow.
const getSliderThumb = (): HTMLElement => within(screen.getByTestId("followup-timeout-slider")).getByRole("slider")

it("renders the slider with a zero minimum that allows a never-auto-select timeout", () => {
renderSettings({ alwaysAllowFollowupQuestions: true, followupAutoApproveTimeoutMs: 0 })

const thumb = getSliderThumb()

expect(thumb).toHaveAttribute("aria-valuemin", "0")
expect(thumb).toHaveAttribute("aria-valuenow", "0")
})

it("describes a zero timeout as never auto-selecting", () => {
renderSettings({ alwaysAllowFollowupQuestions: true, followupAutoApproveTimeoutMs: 0 })

// The translation mock returns the key verbatim, so the zero branch is
// distinguishable from the positive branch by its key alone.
expect(screen.getByText("settings:autoApprove.followupQuestions.timeoutZeroDescription")).toBeInTheDocument()
expect(screen.queryByText("settings:autoApprove.followupQuestions.timeoutLabel")).not.toBeInTheDocument()
})

it("describes a positive timeout with the wait-time label", () => {
renderSettings({ alwaysAllowFollowupQuestions: true, followupAutoApproveTimeoutMs: 60000 })

expect(screen.getByText("settings:autoApprove.followupQuestions.timeoutLabel")).toBeInTheDocument()
expect(
screen.queryByText("settings:autoApprove.followupQuestions.timeoutZeroDescription"),
).not.toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface AutoApproveState {
alwaysAllowSubtasks: boolean
alwaysAllowExecute: boolean
alwaysAllowFollowupQuestions: boolean
followupAutoApproveTimeoutMs?: number
destructiveCommandGuardEnabled: boolean
alwaysDenyUnapprovedCommands: boolean
allowedCommands: string[]
Expand All @@ -23,7 +24,7 @@ interface AutoApproveState {
allowedMaxCost?: number
}

export function AutoApproveSettingsStory() {
export function AutoApproveSettingsStory({ followupAutoApproveTimeoutMs }: { followupAutoApproveTimeoutMs?: number }) {
// The blanket auto-deny checkbox is pinned here in its operative mode: with
// the destructive command guard on it is the fail-closed policy, and the
// guard's hidden command-list editors would otherwise vary the snapshot.
Expand All @@ -34,7 +35,8 @@ export function AutoApproveSettingsStory() {
alwaysAllowModeSwitch: false,
alwaysAllowSubtasks: false,
alwaysAllowExecute: true,
alwaysAllowFollowupQuestions: false,
alwaysAllowFollowupQuestions: followupAutoApproveTimeoutMs !== undefined,
followupAutoApproveTimeoutMs,
destructiveCommandGuardEnabled: true,
alwaysDenyUnapprovedCommands: true,
allowedCommands: [],
Expand Down Expand Up @@ -62,6 +64,7 @@ export function AutoApproveSettingsStory() {
return { ...current, [field]: Array.isArray(value) ? [...value] : [] }
case "allowedMaxRequests":
case "allowedMaxCost":
case "followupAutoApproveTimeoutMs":
return { ...current, [field]: typeof value === "number" ? value : undefined }
default:
return current
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,14 @@ for (const theme of visualThemes) {
await expect(checkbox).toBeChecked()
await expect(story).toHaveScreenshot(`auto-approve-settings-${theme.name}.png`)
})

// Behavioral assertions for the slider (minimum, current value, and the zero-timeout
// description) live in AutoApproveSettings.spec.tsx. This visual test establishes a
// deterministic zero-timeout state and makes the focused screenshot assertion.
test(`renders the zero follow-up timeout in the VS Code ${theme.name} theme`, async ({ mount, page }) => {
const component = mountedStory(await mount("auto-approve-settings", { followupAutoApproveTimeoutMs: 0 }))
await applyVisualTheme(page, theme)
const story = component.getByTestId("auto-approve-settings-story")
await expect(story).toHaveScreenshot(`auto-approve-settings-followup-zero-${theme.name}.png`)
})
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion 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.

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

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,8 @@
"followupQuestions": {
"label": "Question",
"description": "Automatically select the first suggested answer for follow-up questions after the configured timeout",
"timeoutLabel": "Time to wait before auto-selecting the first answer"
"timeoutLabel": "Time to wait before auto-selecting the first answer",
"timeoutZeroDescription": "Set to 0 to never auto-select — always wait for you to choose an answer manually"
},
"execute": {
"label": "Execute",
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/es/settings.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/settings.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/settings.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/settings.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/settings.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/settings.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/settings.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/settings.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/settings.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/settings.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/settings.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/settings.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/settings.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/settings.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-TW/settings.json

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

Loading