From ff062c6e2e526a325b0d6e4a40d2e1afe719aa4e Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sun, 29 Dec 2024 14:41:34 -0800 Subject: [PATCH] Allow selection of multiple browser viewport sizes and adjusting screenshot quality --- .changeset/five-gorillas-exist.md | 5 ++ README.md | 2 +- jest.config.js | 3 + src/core/Cline.ts | 4 +- src/core/prompts/system.ts | 6 +- src/core/webview/ClineProvider.ts | 28 +++++-- .../webview/__tests__/ClineProvider.test.ts | 2 +- src/services/browser/BrowserSession.ts | 20 +++-- src/shared/ExtensionMessage.ts | 3 +- src/shared/WebviewMessage.ts | 3 +- webview-ui/package.json | 2 +- .../src/components/chat/BrowserSessionRow.tsx | 30 ++++--- .../src/components/settings/SettingsView.tsx | 82 +++++++++++++++---- .../settings/__tests__/SettingsView.test.tsx | 34 +++++--- .../src/context/ExtensionStateContext.tsx | 12 ++- 15 files changed, 166 insertions(+), 70 deletions(-) create mode 100644 .changeset/five-gorillas-exist.md diff --git a/.changeset/five-gorillas-exist.md b/.changeset/five-gorillas-exist.md new file mode 100644 index 0000000000..ee8d3b3f6b --- /dev/null +++ b/.changeset/five-gorillas-exist.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Allow selection of multiple browser viewport sizes and adjusting screenshot quality diff --git a/README.md b/README.md index 2a191a7df1..125c9e3030 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A fork of Cline, an autonomous coding agent, with some additional experimental f - Drag and drop images into chats - "Enhance prompt" button (OpenRouter models only for now) - Sound effects for feedback -- Option to use a larger 1280x800 browser +- Option to use browsers of different sizes and adjust screenshot quality - Quick prompt copying from history - OpenRouter compression support - Includes current time in the system prompt diff --git a/jest.config.js b/jest.config.js index b6012c0506..1ef4d79907 100644 --- a/jest.config.js +++ b/jest.config.js @@ -29,6 +29,9 @@ module.exports = { transformIgnorePatterns: [ 'node_modules/(?!(@modelcontextprotocol|delay|p-wait-for|globby|serialize-error|strip-ansi|default-shell|os-name)/)' ], + modulePathIgnorePatterns: [ + '.vscode-test' + ], setupFiles: [], globals: { 'ts-jest': { diff --git a/src/core/Cline.ts b/src/core/Cline.ts index f8b76bc9bf..0b77c27880 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -786,8 +786,8 @@ export class Cline { throw new Error("MCP hub not available") } - const { browserLargeViewport, preferredLanguage } = await this.providerRef.deref()?.getState() ?? {} - const systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false, mcpHub, this.diffStrategy, browserLargeViewport) + await addCustomInstructions(this.customInstructions ?? '', cwd, preferredLanguage) + const { browserViewportSize, preferredLanguage } = await this.providerRef.deref()?.getState() ?? {} + const systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false, mcpHub, this.diffStrategy, browserViewportSize) + await addCustomInstructions(this.customInstructions ?? '', cwd, preferredLanguage) // If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request if (previousApiReqIndex >= 0) { diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 1e6920ed6d..b1a835a6d8 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -11,7 +11,7 @@ export const SYSTEM_PROMPT = async ( supportsComputerUse: boolean, mcpHub: McpHub, diffStrategy?: DiffStrategy, - browserLargeViewport?: boolean + browserViewportSize?: string ) => `You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. ==== @@ -114,7 +114,7 @@ Usage: Description: Request to interact with a Puppeteer-controlled browser. Every action, except \`close\`, will be responded to with a screenshot of the browser's current state, along with any new console logs. You may only perform one browser action per message, and wait for the user's response including a screenshot and logs to determine the next action. - The sequence of actions **must always start with** launching the browser at a URL, and **must always end with** closing the browser. If you need to visit a new URL that is not possible to navigate to from the current webpage, you must first close the browser, then launch again at the new URL. - While the browser is active, only the \`browser_action\` tool can be used. No other tools should be called during this time. You may proceed to use other tools only after closing the browser. For example if you run into an error and need to fix a file, you must close the browser, then use other tools to make the necessary changes, then re-launch the browser to verify the result. -- The browser window has a resolution of **${browserLargeViewport ? "1280x800" : "900x600"}** pixels. When performing any click actions, ensure the coordinates are within this resolution range. +- The browser window has a resolution of **${browserViewportSize || "900x600"}** pixels. When performing any click actions, ensure the coordinates are within this resolution range. - Before clicking on any elements such as icons, links, or buttons, you must consult the provided screenshot of the page to determine the coordinates of the element. The click should be targeted at the **center of the element**, not on its edges. Parameters: - action: (required) The action to perform. The available actions are: @@ -132,7 +132,7 @@ Parameters: - Example: \`close\` - url: (optional) Use this for providing the URL for the \`launch\` action. * Example: https://example.com -- coordinate: (optional) The X and Y coordinates for the \`click\` action. Coordinates should be within the **${browserLargeViewport ? "1280x800" : "900x600"}** resolution. +- coordinate: (optional) The X and Y coordinates for the \`click\` action. Coordinates should be within the **${browserViewportSize || "900x600"}** resolution. * Example: 450,300 - text: (optional) Use this for providing the text for the \`type\` action. * Example: Hello, world! diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index d27ed9b395..604bac6f33 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -71,7 +71,8 @@ type GlobalStateKey = | "soundVolume" | "diffEnabled" | "alwaysAllowMcp" - | "browserLargeViewport" + | "browserViewportSize" + | "screenshotQuality" | "fuzzyMatchThreshold" | "preferredLanguage" // Language setting for Cline's communication | "writeDelayMs" @@ -624,9 +625,9 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("diffEnabled", diffEnabled) await this.postStateToWebview() break - case "browserLargeViewport": - const browserLargeViewport = message.bool ?? false - await this.updateGlobalState("browserLargeViewport", browserLargeViewport) + case "browserViewportSize": + const browserViewportSize = message.text ?? "900x600" + await this.updateGlobalState("browserViewportSize", browserViewportSize) await this.postStateToWebview() break case "fuzzyMatchThreshold": @@ -641,6 +642,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("writeDelayMs", message.value) await this.postStateToWebview() break + case "screenshotQuality": + await this.updateGlobalState("screenshotQuality", message.value) + await this.postStateToWebview() + break case "enhancePrompt": if (message.text) { try { @@ -1015,7 +1020,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { diffEnabled, taskHistory, soundVolume, - browserLargeViewport, + browserViewportSize, + screenshotQuality, preferredLanguage, writeDelayMs, } = await this.getState() @@ -1043,7 +1049,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId, allowedCommands, soundVolume: soundVolume ?? 0.5, - browserLargeViewport: browserLargeViewport ?? false, + browserViewportSize: browserViewportSize ?? "900x600", + screenshotQuality: screenshotQuality ?? 75, preferredLanguage: preferredLanguage ?? 'English', writeDelayMs: writeDelayMs ?? 1000, } @@ -1140,10 +1147,11 @@ export class ClineProvider implements vscode.WebviewViewProvider { soundEnabled, diffEnabled, soundVolume, - browserLargeViewport, + browserViewportSize, fuzzyMatchThreshold, preferredLanguage, writeDelayMs, + screenshotQuality, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -1183,10 +1191,11 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("soundEnabled") as Promise, this.getGlobalState("diffEnabled") as Promise, this.getGlobalState("soundVolume") as Promise, - this.getGlobalState("browserLargeViewport") as Promise, + this.getGlobalState("browserViewportSize") as Promise, this.getGlobalState("fuzzyMatchThreshold") as Promise, this.getGlobalState("preferredLanguage") as Promise, this.getGlobalState("writeDelayMs") as Promise, + this.getGlobalState("screenshotQuality") as Promise, ]) let apiProvider: ApiProvider @@ -1244,7 +1253,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { soundEnabled: soundEnabled ?? false, diffEnabled: diffEnabled ?? true, soundVolume, - browserLargeViewport: browserLargeViewport ?? false, + browserViewportSize: browserViewportSize ?? "900x600", + screenshotQuality: screenshotQuality ?? 75, fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0, writeDelayMs: writeDelayMs ?? 1000, preferredLanguage: preferredLanguage ?? (() => { diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 16a976e689..af7563123e 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -253,7 +253,7 @@ describe('ClineProvider', () => { soundEnabled: false, diffEnabled: false, writeDelayMs: 1000, - browserLargeViewport: false, + browserViewportSize: "900x600", fuzzyMatchThreshold: 1.0, } diff --git a/src/services/browser/BrowserSession.ts b/src/services/browser/BrowserSession.ts index 6f2ccde712..54e04b8466 100644 --- a/src/services/browser/BrowserSession.ts +++ b/src/services/browser/BrowserSession.ts @@ -58,9 +58,11 @@ export class BrowserSession { "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", ], executablePath: stats.executablePath, - defaultViewport: await this.context.globalState.get("browserLargeViewport") - ? { width: 1280, height: 800 } - : { width: 900, height: 600 }, + defaultViewport: (() => { + const size = (this.context.globalState.get("browserViewportSize") as string | undefined) || "900x600" + const [width, height] = size.split("x").map(Number) + return { width, height } + })(), // headless: false, }) // (latest version of puppeteer does not add headless to user agent) @@ -134,7 +136,7 @@ export class BrowserSession { let screenshotBase64 = await this.page.screenshot({ ...options, type: "webp", - quality: 100, // Set maximum quality to prevent compression artifacts + quality: (await this.context.globalState.get("screenshotQuality") as number | undefined) ?? 75, }) let screenshot = `data:image/webp;base64,${screenshotBase64}` @@ -245,27 +247,29 @@ export class BrowserSession { } async scrollDown(): Promise { - const isLargeViewport = await this.context.globalState.get("browserLargeViewport") + const size = (await this.context.globalState.get("browserViewportSize") as string | undefined) || "900x600" + const height = parseInt(size.split("x")[1]) return this.doAction(async (page) => { await page.evaluate((scrollHeight) => { window.scrollBy({ top: scrollHeight, behavior: "auto", }) - }, isLargeViewport ? 800 : 600) + }, height) await delay(300) }) } async scrollUp(): Promise { - const isLargeViewport = await this.context.globalState.get("browserLargeViewport") + const size = (await this.context.globalState.get("browserViewportSize") as string | undefined) || "900x600" + const height = parseInt(size.split("x")[1]) return this.doAction(async (page) => { await page.evaluate((scrollHeight) => { window.scrollBy({ top: -scrollHeight, behavior: "auto", }) - }, isLargeViewport ? 800 : 600) + }, height) await delay(300) }) } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 92357d4e3d..cc461357ca 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -56,7 +56,8 @@ export interface ExtensionState { soundEnabled?: boolean soundVolume?: number diffEnabled?: boolean - browserLargeViewport?: boolean + browserViewportSize?: string + screenshotQuality?: number fuzzyMatchThreshold?: number preferredLanguage: string writeDelayMs: number diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 7df4cb6cb3..5ae5c02bcb 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -35,7 +35,8 @@ export interface WebviewMessage { | "soundEnabled" | "soundVolume" | "diffEnabled" - | "browserLargeViewport" + | "browserViewportSize" + | "screenshotQuality" | "openMcpSettings" | "restartMcpServer" | "toggleToolAlwaysAllow" diff --git a/webview-ui/package.json b/webview-ui/package.json index ef1013dd11..3ce9533d72 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -61,7 +61,7 @@ }, "jest": { "transformIgnorePatterns": [ - "/node_modules/(?!(rehype-highlight|react-remark|unist-util-visit|vfile|unified|bail|is-plain-obj|trough|vfile-message|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|character-entities|markdown-table|zwitch|longest-streak|escape-string-regexp|unist-util-is|hast-util-to-text|@vscode/webview-ui-toolkit|@microsoft/fast-react-wrapper|@microsoft/fast-element|@microsoft/fast-foundation|@microsoft/fast-web-utilities|exenv-es6)/)" + "/node_modules/(?!(rehype-highlight|react-remark|unist-util-visit|unist-util-find-after|vfile|unified|bail|is-plain-obj|trough|vfile-message|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|character-entities|markdown-table|zwitch|longest-streak|escape-string-regexp|unist-util-is|hast-util-to-text|@vscode/webview-ui-toolkit|@microsoft/fast-react-wrapper|@microsoft/fast-element|@microsoft/fast-foundation|@microsoft/fast-web-utilities|exenv-es6)/)" ], "moduleNameMapper": { "\\.(css|less|scss|sass)$": "identity-obj-proxy" diff --git a/webview-ui/src/components/chat/BrowserSessionRow.tsx b/webview-ui/src/components/chat/BrowserSessionRow.tsx index 7497383d2d..b5b20d1148 100644 --- a/webview-ui/src/components/chat/BrowserSessionRow.tsx +++ b/webview-ui/src/components/chat/BrowserSessionRow.tsx @@ -28,6 +28,11 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => { const [maxActionHeight, setMaxActionHeight] = useState(0) const [consoleLogsExpanded, setConsoleLogsExpanded] = useState(false) + const { browserViewportSize = "900x600" } = useExtensionState() + const [viewportWidth, viewportHeight] = browserViewportSize.split("x").map(Number) + const aspectRatio = (viewportHeight / viewportWidth * 100).toFixed(2) + const defaultMousePosition = `${Math.round(viewportWidth/2)},${Math.round(viewportHeight/2)}` + const isLastApiReqInterrupted = useMemo(() => { // Check if last api_req_started is cancelled const lastApiReqStarted = [...messages].reverse().find((m) => m.say === "api_req_started") @@ -165,13 +170,13 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => { const displayState = isLastPage ? { url: currentPage?.currentState.url || latestState.url || initialUrl, - mousePosition: currentPage?.currentState.mousePosition || latestState.mousePosition || "700,400", + mousePosition: currentPage?.currentState.mousePosition || latestState.mousePosition || defaultMousePosition, consoleLogs: currentPage?.currentState.consoleLogs, screenshot: currentPage?.currentState.screenshot || latestState.screenshot, } : { url: currentPage?.currentState.url || initialUrl, - mousePosition: currentPage?.currentState.mousePosition || "700,400", + mousePosition: currentPage?.currentState.mousePosition || defaultMousePosition, consoleLogs: currentPage?.currentState.consoleLogs, screenshot: currentPage?.currentState.screenshot, } @@ -220,10 +225,9 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => { }, [isBrowsing, currentPage?.nextAction?.messages]) // Use latest click position while browsing, otherwise use display state - const { browserLargeViewport } = useExtensionState() - const mousePosition = isBrowsing ? latestClickPosition || displayState.mousePosition : displayState.mousePosition + const mousePosition = isBrowsing ? latestClickPosition || displayState.mousePosition : displayState.mousePosition || defaultMousePosition - const [browserSessionRow, { height }] = useSize( + const [browserSessionRow, { height: rowHeight }] = useSize(
{isBrowsing ? ( @@ -277,9 +281,10 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => { {/* Screenshot Area */}
@@ -321,8 +326,8 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => { @@ -389,13 +394,13 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => { // Height change effect useEffect(() => { const isInitialRender = prevHeightRef.current === 0 - if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) { + if (isLast && rowHeight !== 0 && rowHeight !== Infinity && rowHeight !== prevHeightRef.current) { if (!isInitialRender) { - onHeightChange(height > prevHeightRef.current) + onHeightChange(rowHeight > prevHeightRef.current) } - prevHeightRef.current = height + prevHeightRef.current = rowHeight } - }, [height, isLast, onHeightChange]) + }, [rowHeight, isLast, onHeightChange]) return browserSessionRow }, deepEqual) @@ -552,6 +557,7 @@ const BrowserCursor: React.FC<{ style?: React.CSSProperties }> = ({ style }) => ...style, }} alt="cursor" + aria-label="cursor" /> ) } diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index ecaa9d433e..6952480aea 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -33,8 +33,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { setSoundVolume, diffEnabled, setDiffEnabled, - browserLargeViewport, - setBrowserLargeViewport, + browserViewportSize, + setBrowserViewportSize, openRouterModels, setAllowedCommands, allowedCommands, @@ -44,6 +44,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { setPreferredLanguage, writeDelayMs, setWriteDelayMs, + screenshotQuality, + setScreenshotQuality, } = useExtensionState() const [apiErrorMessage, setApiErrorMessage] = useState(undefined) const [modelIdErrorMessage, setModelIdErrorMessage] = useState(undefined) @@ -69,10 +71,11 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { vscode.postMessage({ type: "soundEnabled", bool: soundEnabled }) vscode.postMessage({ type: "soundVolume", value: soundVolume }) vscode.postMessage({ type: "diffEnabled", bool: diffEnabled }) - vscode.postMessage({ type: "browserLargeViewport", bool: browserLargeViewport }) + vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize }) vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 }) vscode.postMessage({ type: "preferredLanguage", text: preferredLanguage }) vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs }) + vscode.postMessage({ type: "screenshotQuality", value: screenshotQuality ?? 75 }) onDone() } } @@ -128,7 +131,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { marginBottom: "17px", paddingRight: 17, }}> -

Settings

+ +

Provider Settings

Done
{
+

Agent Settings

+ setBrowserViewportSize(e.target.value)} + style={{ + width: "100%", + padding: "4px 8px", + backgroundColor: "var(--vscode-input-background)", + color: "var(--vscode-input-foreground)", + border: "1px solid var(--vscode-input-border)", + borderRadius: "2px", + height: "28px" + }}> + + + + + +

- When enabled, Cline will use a larger viewport size for browser interactions. -

+ Select the viewport size for browser interactions. This affects how websites are displayed and interacted with. +

+
+ +
+
+ Screenshot Quality + setScreenshotQuality(parseInt(e.target.value))} + style={{ + flexGrow: 1, + accentColor: 'var(--vscode-button-background)', + height: '2px' + }} + /> + + {screenshotQuality ?? 75}% + +
+

+ Adjust the WebP quality of browser screenshots. Higher values provide clearer screenshots but increase token usage. +

+
+

Notification Settings

setSoundEnabled(e.target.checked)}> Enable sound effects @@ -468,9 +519,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { accentColor: 'var(--vscode-button-background)', height: '2px' }} + aria-label="Volume" /> - {Math.round((soundVolume ?? 0.5) * 100)}% + {((soundVolume ?? 0.5) * 100).toFixed(0)}%
diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx index c9e5f0a244..deb6a99ddc 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx @@ -61,6 +61,17 @@ jest.mock('@vscode/webview-ui-toolkit/react', () => ({
{children}
+ ), + VSCodeSlider: ({ value, onChange }: any) => ( + onChange({ target: { value: Number(e.target.value) } })} + min={0} + max={1} + step={0.01} + style={{ flexGrow: 1, height: '2px' }} + /> ) })) @@ -75,6 +86,8 @@ const mockPostMessage = (state: any) => { shouldShowAnnouncement: false, allowedCommands: [], alwaysAllowExecute: false, + soundEnabled: false, + soundVolume: 0.5, ...state } }, '*') @@ -106,7 +119,7 @@ describe('SettingsView - Sound Settings', () => { expect(soundCheckbox).not.toBeChecked() // Volume slider should not be visible when sound is disabled - expect(screen.queryByRole('slider')).not.toBeInTheDocument() + expect(screen.queryByRole('slider', { name: /volume/i })).not.toBeInTheDocument() }) it('toggles sound setting and sends message to VSCode', () => { @@ -142,9 +155,9 @@ describe('SettingsView - Sound Settings', () => { fireEvent.click(soundCheckbox) // Volume slider should be visible - const volumeSlider = screen.getByRole('slider') + const volumeSlider = screen.getByRole('slider', { name: /volume/i }) expect(volumeSlider).toBeInTheDocument() - expect(volumeSlider).toHaveValue('0.5') // Default value + expect(volumeSlider).toHaveValue('0.5') }) it('updates volume and sends message to VSCode when slider changes', () => { @@ -157,23 +170,18 @@ describe('SettingsView - Sound Settings', () => { fireEvent.click(soundCheckbox) // Change volume - const volumeSlider = screen.getByRole('slider') + const volumeSlider = screen.getByRole('slider', { name: /volume/i }) fireEvent.change(volumeSlider, { target: { value: '0.75' } }) - // Verify volume display updates - expect(screen.getByText('75%')).toBeInTheDocument() - // Click Done to save settings const doneButton = screen.getByText('Done') fireEvent.click(doneButton) // Verify message sent to VSCode - expect(vscode.postMessage).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'soundVolume', - value: 0.75 - }) - ) + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: 'soundVolume', + value: 0.75 + }) }) }) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index c9a8d56029..a80ec53fba 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -32,14 +32,16 @@ export interface ExtensionStateContextType extends ExtensionState { setSoundEnabled: (value: boolean) => void setSoundVolume: (value: number) => void setDiffEnabled: (value: boolean) => void - setBrowserLargeViewport: (value: boolean) => void + setBrowserViewportSize: (value: string) => void setFuzzyMatchThreshold: (value: number) => void preferredLanguage: string setPreferredLanguage: (value: string) => void setWriteDelayMs: (value: number) => void + screenshotQuality?: number + setScreenshotQuality: (value: number) => void } -const ExtensionStateContext = createContext(undefined) +export const ExtensionStateContext = createContext(undefined) export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [state, setState] = useState({ @@ -54,6 +56,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode fuzzyMatchThreshold: 1.0, preferredLanguage: 'English', writeDelayMs: 1000, + browserViewportSize: "900x600", + screenshotQuality: 75, }) const [didHydrateState, setDidHydrateState] = useState(false) const [showWelcome, setShowWelcome] = useState(false) @@ -151,6 +155,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode soundVolume: state.soundVolume, fuzzyMatchThreshold: state.fuzzyMatchThreshold, writeDelayMs: state.writeDelayMs, + screenshotQuality: state.screenshotQuality, setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value @@ -166,10 +171,11 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })), setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })), setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })), - setBrowserLargeViewport: (value) => setState((prevState) => ({ ...prevState, browserLargeViewport: value })), + setBrowserViewportSize: (value: string) => setState((prevState) => ({ ...prevState, browserViewportSize: value })), setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })), setPreferredLanguage: (value) => setState((prevState) => ({ ...prevState, preferredLanguage: value })), setWriteDelayMs: (value) => setState((prevState) => ({ ...prevState, writeDelayMs: value })), + setScreenshotQuality: (value) => setState((prevState) => ({ ...prevState, screenshotQuality: value })), } return {children}