Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
0688f25
issue #685 - point 8
murd0cc Aug 9, 2026
04aa5fa
PR requested changes implemented
murd0cc Aug 11, 2026
9411e41
Maintainability & Code Quality
murd0cc Aug 11, 2026
ca2ed95
coverage improvements
murd0cc Aug 11, 2026
d763846
Merge branch 'main' into issue/685
murd0cc Aug 11, 2026
d4023d1
refactor(types): canonicalize shared provider settings identifiers (#…
WebMad Aug 12, 2026
c35f6e8
[Feat] Add GLM 5.3 support to Z AI and OpenCode Go (#1244)
zoomote[bot] Aug 14, 2026
13cc3fe
[Feat] Add Gemini 3.7 Flash support (#1241)
zoomote[bot] Aug 14, 2026
c4daefc
[Feat] Add Qwen3.8 Max to OpenCode Go (#1245)
zoomote[bot] Aug 14, 2026
63634be
[Fix] DeepSeek V4 costs are underestimated after pricing changes (#1237)
zoomote[bot] Aug 14, 2026
f7239c8
fix: update deepseek-v4-pro reasoning efforts and mappings (#1236)
WHMHammer Aug 14, 2026
193c2f2
feat(api): add NanoGPT provider support (#1239)
taltas Aug 15, 2026
732651f
fix(config): use zoo-code-settings.json for settings export/import (#…
Rafael-Silva-Oliveira Aug 15, 2026
e064cf0
chore: prepare v3.78.0 release (#1249)
taltas Aug 15, 2026
d52f659
fix(task): recover stale delegated children after restart (#1210)
edelauna Aug 16, 2026
73602a9
fixed screenshots
murd0cc Aug 16, 2026
52da89d
Merge branch 'issue/685' of https://github.com/murd0cc/Zoo-Code into …
murd0cc Aug 16, 2026
43c865b
workaround - webview visual test throws error when deleting snapshots.
murd0cc Aug 16, 2026
c427ecb
fixed format from jpeg to png
murd0cc Aug 16, 2026
c9ebb87
fixed snapshot tests
murd0cc Aug 16, 2026
1f99585
test(settings): harden OpenAICompatible mocks with partial-actual imp…
murd0cc Aug 16, 2026
8cac2ac
fix(webview): throttle state pushes to prevent gray screen OOM (#1078)
JunyongParkDev Aug 16, 2026
61cad5b
test: wire AutoApprove visual tests to manual follow-up timer snapshots
murd0cc Aug 16, 2026
3901fec
missing snapshot
murd0cc Aug 16, 2026
dfc839e
Updated screenshot assertion
murd0cc Aug 16, 2026
9081dde
refactor(webview): canonicalize provider and model message identifier…
WebMad Aug 16, 2026
5b6a506
coverage
murd0cc Aug 16, 2026
3e8144a
feat(api): support official GLM-5.3 Z.ai release (#1284)
zoomote[bot] Aug 18, 2026
dbad4af
feat(api): introduce RequestConfigBuilder for SDK-agnostic abort sign…
easonLiangWorldedtech Aug 18, 2026
38d5ee0
fix(shell): delegate VS Code profile reads to Terminal methods (issue…
edelauna Aug 19, 2026
959826b
removed manual snapshots
murd0cc Aug 19, 2026
775f565
generated screenshots
murd0cc Aug 19, 2026
eac2f0c
regenerated snapshots with correct playwright image
murd0cc Aug 19, 2026
05f8a3e
[Fix] Mermaid labels are unreadable in light IDE themes (#1285)
zoomote[bot] Aug 19, 2026
b7f78f8
refactor(providers): canonicalize model-router calls (#1165)
WebMad Aug 20, 2026
252c69b
fix: stream reasoning_content in LM Studio provider (#1175)
daewoongoh Aug 20, 2026
afdede5
lint(providers): enforce canonical identifiers (#1297)
WebMad Aug 20, 2026
63aca68
fix(semble): increase archive download limit (#1306)
navedmerchant Aug 20, 2026
21d35c4
fix(task-history): atomic per-task merge and drop shared index file (…
edelauna Aug 20, 2026
871bb98
[Fix] Chat controls fade into light IDE themes (#1298)
zoomote[bot] Aug 20, 2026
39bdfb1
[Fix] Remaining chat controls fade into light IDE themes (#1312)
zoomote[bot] Aug 21, 2026
1ad8f52
fix(telemetry): default telemetry to opt-out with explicit consent UI…
edelauna Aug 21, 2026
db52d7f
feat(models): add Gemini 3.5 Flash Lite and 3.1 Flash Lite to Gemini …
easonLiangWorldedtech Aug 22, 2026
3dcac60
[Chore] Enforce safe async handling in core tools (#1255)
zoomote[bot] Aug 22, 2026
6ad8a6e
fix(zoo-gateway): stop inventing UI cost from default model prices (#…
JamesRobert20 Aug 22, 2026
fec4e13
[Improve] Keep shared controls legible across IDE themes (#1333)
zoomote[bot] Aug 22, 2026
3aacd91
Reverted the loose pixel tolerance
murd0cc Aug 22, 2026
351d9f3
initialized the i18next instance with the real English settings.json
murd0cc Aug 22, 2026
a9e9462
regenerated snapshots
murd0cc Aug 22, 2026
7da96a1
issue #685 - point 8
murd0cc Aug 9, 2026
2aa8138
PR requested changes implemented
murd0cc Aug 11, 2026
7f3a22a
Maintainability & Code Quality
murd0cc Aug 11, 2026
301d8ae
coverage improvements
murd0cc Aug 11, 2026
aeeec15
fixed screenshots
murd0cc Aug 16, 2026
20d1987
workaround - webview visual test throws error when deleting snapshots.
murd0cc Aug 16, 2026
8e01947
fixed format from jpeg to png
murd0cc Aug 16, 2026
dfc2c63
fixed snapshot tests
murd0cc Aug 16, 2026
04698d2
test(settings): harden OpenAICompatible mocks with partial-actual imp…
murd0cc Aug 16, 2026
e815e91
test: wire AutoApprove visual tests to manual follow-up timer snapshots
murd0cc Aug 16, 2026
19a7e3d
Updated screenshot assertion
murd0cc Aug 16, 2026
8f08b2b
coverage
murd0cc Aug 16, 2026
989d451
removed manual snapshots
murd0cc Aug 19, 2026
a5f2a42
generated screenshots
murd0cc Aug 19, 2026
835d680
regenerated snapshots with correct playwright image
murd0cc Aug 19, 2026
e9a3103
Reverted the loose pixel tolerance
murd0cc Aug 22, 2026
8705bfc
initialized the i18next instance with the real English settings.json
murd0cc Aug 22, 2026
907f420
regenerated snapshots
murd0cc Aug 22, 2026
c8c69b3
Merge branch 'issue/685' of https://github.com/murd0cc/Zoo-Code into …
murd0cc Aug 22, 2026
a16d6e4
regen snapshots in docker
murd0cc Aug 22, 2026
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 webview-ui/eslint-suppressions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
5 changes: 3 additions & 2 deletions webview-ui/src/components/chat/FollowUpSuggest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ export const FollowUpSuggest = ({
// Start countdown timer when auto-approval is enabled for follow-up questions
useEffect(() => {
// Only start countdown if auto-approval is enabled for follow-up questions and no suggestion has been selected
// Also stop countdown if the question has been answered or auto-approval is paused (user is typing)
// Also stop countdown if the question has been answered or auto-approval is paused (user is typing) or timer is disabled (set to 0)
if (
autoApprovalEnabled &&
alwaysAllowFollowupQuestions &&
suggestions.length > 0 &&
!suggestionSelected &&
!isAnswered &&
!isFollowUpAutoApprovalPaused
!isFollowUpAutoApprovalPaused &&
(followupAutoApproveTimeoutMs ?? DEFAULT_FOLLOWUP_TIMEOUT_MS) > 0
Comment thread
murd0cc marked this conversation as resolved.
) {
// Start with the configured timeout in seconds
const timeoutMs =
Expand Down
136 changes: 134 additions & 2 deletions webview-ui/src/components/chat/__tests__/FollowUpSuggest.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { createContext, useContext } from "react"
import { render, screen, act } from "@testing-library/react"
import { render, screen, act, fireEvent } from "@testing-library/react"
import { TooltipProvider } from "@radix-ui/react-tooltip"

import { FollowUpSuggest } from "../FollowUpSuggest"
Expand Down Expand Up @@ -28,7 +28,7 @@ vi.mock("@src/i18n/TranslationContext", () => ({
interface TestExtensionState {
autoApprovalEnabled: boolean
alwaysAllowFollowupQuestions: boolean
followupAutoApproveTimeoutMs: number
followupAutoApproveTimeoutMs?: number
}

const TestExtensionStateContext = createContext<TestExtensionState | undefined>(undefined)
Expand Down Expand Up @@ -74,6 +74,13 @@ describe("FollowUpSuggest", () => {
followupAutoApproveTimeoutMs: 3000, // 3 seconds for testing
}

// Test state with timeout disabled (0)
const disabledTimeoutState: TestExtensionState = {
autoApprovalEnabled: true,
alwaysAllowFollowupQuestions: true,
followupAutoApproveTimeoutMs: 0, // Disabled
}

beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
Expand Down Expand Up @@ -218,6 +225,41 @@ describe("FollowUpSuggest", () => {
expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument()
})

// Should not show countdown when timeout is disabled (set to 0)
it("should not show countdown when timeout is disabled (set to 0)", () => {
renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={1}
onCancelAutoApproval={mockOnCancelAutoApproval}
/>,
disabledTimeoutState,
)

// Should not show countdown when timeout is disabled
expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument()
})

it("should not show countdown when timeout is negative", () => {
const negativeTimeoutState: TestExtensionState = {
...defaultTestState,
followupAutoApproveTimeoutMs: -1000,
}

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

expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument()
})

it("should not render when no suggestions are provided", () => {
const { container } = renderWithTestProviders(
<FollowUpSuggest
Expand Down Expand Up @@ -707,4 +749,94 @@ describe("FollowUpSuggest", () => {
expect(mockOnCancelAutoApproval).toHaveBeenCalled()
})
})

describe("suggestion interactions", () => {
it("cancels countdown and forwards click when user clicks a suggestion", () => {
renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
/>,
defaultTestState,
)

fireEvent.click(screen.getByText("First suggestion"))

expect(mockOnSuggestionClick).toHaveBeenCalledWith(
expect.objectContaining({ answer: "First suggestion" }),
expect.objectContaining({ shiftKey: false }),
)
expect(mockOnCancelAutoApproval).toHaveBeenCalled()
expect(screen.queryByText(/Selecting in \d+s/)).not.toBeInTheDocument()
})

it("keeps countdown when shift-clicking a suggestion", () => {
renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
/>,
defaultTestState,
)

mockOnCancelAutoApproval.mockClear()
fireEvent.click(screen.getByText("First suggestion"), { shiftKey: true })

expect(mockOnSuggestionClick).toHaveBeenCalledWith(
expect.objectContaining({ answer: "First suggestion" }),
expect.objectContaining({ shiftKey: true }),
)
expect(mockOnCancelAutoApproval).not.toHaveBeenCalled()
expect(screen.getByText(/Selecting in 3s/)).toBeInTheDocument()
})

it("copies suggestion into input when the copy affordance is clicked", () => {
const { container } = renderWithTestProviders(
<FollowUpSuggest
suggestions={mockSuggestions}
onSuggestionClick={mockOnSuggestionClick}
ts={123}
onCancelAutoApproval={mockOnCancelAutoApproval}
/>,
defaultTestState,
)

const copyAffordance = container.querySelector(
".absolute.cursor-pointer.top-1\\.5.right-1\\.5",
) as HTMLElement

expect(copyAffordance).toBeTruthy()
fireEvent.click(copyAffordance)

expect(mockOnSuggestionClick).toHaveBeenCalledWith(
expect.objectContaining({ answer: "First suggestion" }),
expect.objectContaining({ shiftKey: true }),
)
expect(mockOnCancelAutoApproval).toHaveBeenCalled()
expect(screen.queryByText(/Selecting in \d+s/)).not.toBeInTheDocument()
})

it("uses default timeout when extension state timeout is undefined", () => {
const stateWithUndefinedTimeout = {
...defaultTestState,
followupAutoApproveTimeoutMs: undefined,
}

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

expect(screen.getByText(/Selecting in 60s/)).toBeInTheDocument()
})
})
})
8 changes: 6 additions & 2 deletions webview-ui/src/components/settings/AutoApproveSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export const AutoApproveSettings = ({
label={t("settings:autoApprove.followupQuestions.timeoutLabel")}>
<div className="flex items-center gap-2">
<Slider
min={1000}
min={0}
Comment thread
murd0cc marked this conversation as resolved.
max={300000}
step={1000}
value={[followupAutoApproveTimeoutMs]}
Expand All @@ -263,7 +263,11 @@ export const AutoApproveSettings = ({
}
data-testid="followup-timeout-slider"
/>
<span className="w-20">{followupAutoApproveTimeoutMs / 1000}s</span>
<span className="w-20">
Comment thread
murd0cc marked this conversation as resolved.
{followupAutoApproveTimeoutMs === 0
? t("settings:autoApprove.followupQuestions.timeoutDisabled")
: `${followupAutoApproveTimeoutMs / 1000}s`}
</span>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.followupQuestions.timeoutLabel")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ vi.mock("@/hooks/useAutoApprovalState", () => ({
useAutoApprovalState: () => ({ effectiveAutoApprovalEnabled: false, hasEnabledOptions: false }),
}))

vi.mock("@/components/ui", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/components/ui")>()

return {
...actual,
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
Input: (props: any) => <input {...props} />,
Slider: ({ value, onValueChange, ...props }: any) => (
<input
type="range"
value={value?.[0] ?? 0}
onChange={(event) => onValueChange?.([Number((event.target as HTMLInputElement).value)])}
{...props}
/>
),
}
})

const renderSettings = (overrides = {}) => {
const setCachedStateField = vi.fn()
const props = {
Expand Down Expand Up @@ -161,4 +179,71 @@ describe("AutoApproveSettings - Save/Discard contract", () => {
expect(screen.getByTestId("allowed-commands-heading")).toBeInTheDocument()
expect(screen.getByTestId("denied-commands-heading")).toBeInTheDocument()
})

it("renders disabled timeout label when follow-up auto-approve timeout is 0", () => {
const { setCachedStateField } = renderSettings({
alwaysAllowFollowupQuestions: true,
followupAutoApproveTimeoutMs: 0,
})

const slider = screen.getByTestId("followup-timeout-slider") as HTMLInputElement
expect(slider).toBeInTheDocument()
expect(slider.value).toBe("0")
expect(screen.getByText("settings:autoApprove.followupQuestions.timeoutDisabled")).toBeInTheDocument()

fireEvent.change(slider, { target: { value: "4000" } })

expect(setCachedStateField).toHaveBeenCalledWith("followupAutoApproveTimeoutMs", 4000)
expectNoImmediateUpdateSettings()
})

it("renders timeout in seconds when follow-up auto-approve timeout is non-zero", () => {
const { setCachedStateField } = renderSettings({
alwaysAllowFollowupQuestions: true,
followupAutoApproveTimeoutMs: 5000,
})

const slider = screen.getByTestId("followup-timeout-slider") as HTMLInputElement
expect(slider).toBeInTheDocument()
expect(slider.value).toBe("5000")
expect(screen.getByText("5s")).toBeInTheDocument()

fireEvent.change(slider, { target: { value: "0" } })

expect(setCachedStateField).toHaveBeenCalledWith("followupAutoApproveTimeoutMs", 0)
expectNoImmediateUpdateSettings()
})

it("uses the default timeout value when timeout is unset and follow-up auto-approve is enabled", () => {
renderSettings({ alwaysAllowFollowupQuestions: true })

const slider = screen.getByTestId("followup-timeout-slider") as HTMLInputElement
expect(slider.value).toBe("60000")
expect(screen.getByText("60s")).toBeInTheDocument()
})

it("does not render the follow-up timeout controls when follow-up auto-approve is disabled or unset", () => {
const { rerender } = render(
<AutoApproveSettings
alwaysAllowExecute
allowedCommands={[]}
deniedCommands={[]}
alwaysAllowFollowupQuestions={false}
setCachedStateField={vi.fn()}
/>,
)

expect(screen.queryByTestId("followup-timeout-slider")).not.toBeInTheDocument()

rerender(
<AutoApproveSettings
alwaysAllowExecute
allowedCommands={[]}
deniedCommands={[]}
setCachedStateField={vi.fn()}
/>,
)

expect(screen.queryByTestId("followup-timeout-slider")).not.toBeInTheDocument()
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* v8 ignore file -- Playwright component fixture is covered by the visual test. */
import React from "react"

import { TranslationContext } from "@/i18n/TranslationContext"
import i18next from "@/i18n/setup"
import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext"
import { AutoApproveSettings } from "../AutoApproveSettings"

export const AutoApproveSettingsFixture = () => (
<TranslationContext.Provider
value={{
t: (key) => (key === "settings:autoApprove.followupQuestions.timeoutDisabled" ? "Disabled" : key),

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.

This t returns the raw key for every string except timeoutDisabled, so snapshots 1-3 freeze untranslated keys and can't catch layout or translation regressions. Snapshot 3 does prove the === 0 "Disabled" state, but that's already covered by the unit test at AutoApproveSettings.spec.tsx:192. Consider initializing the createInstance() above with the en/settings.json resources so the snapshots exercise real copy.

i18n: i18next,
}}>
<ExtensionStateContextProvider
initialState={{ autoApprovalEnabled: false, alwaysAllowFollowupQuestions: true }}>
<div className="w-[680px] bg-vscode-editor-background p-4 text-vscode-foreground">
<AutoApproveSettings
alwaysAllowFollowupQuestions
followupAutoApproveTimeoutMs={0}
setCachedStateField={() => {}}
/>
</div>
</ExtensionStateContextProvider>
</TranslationContext.Provider>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react"

import { expect, test } from "../../../../playwright/coverage-fixture"
import { AutoApproveSettingsFixture } from "./AutoApproveSettings.visual.fixture"

test("renders follow-up timeout row with disabled state label in the VS Code dark theme", async ({ mount, page }) => {
// The full settings bundle can leave a bare Zod reference after CT tree-shaking.
await page.evaluate(() => Object.assign(globalThis, { z: undefined }))

const component = await mount(<AutoApproveSettingsFixture />)

await component.evaluate(async () => {
await document.fonts.ready
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
})

await expect(component).toHaveScreenshot("auto-approve-followup-timeout-disabled-dark.png")
})

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.

this doesnt look right

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

redid the snapshots.
also, main updates meant that I wasnt passing tests anymore, so a few minor changes were required on my part.

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 @@ -401,7 +401,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",
"timeoutDisabled": "Disabled"
},
"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.

Loading
Loading