Allow selection of multiple browser viewport sizes and adjusting screenshot quality

This commit is contained in:
Matt Rubens
2024-12-29 14:41:34 -08:00
parent 99ff8047fd
commit ff062c6e2e
15 changed files with 166 additions and 70 deletions

View File

@@ -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"

View File

@@ -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(
<div style={{ padding: "10px 6px 10px 15px", marginBottom: -10 }}>
<div style={{ display: "flex", alignItems: "center", gap: "10px", marginBottom: "10px" }}>
{isBrowsing ? (
@@ -277,9 +281,10 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
{/* Screenshot Area */}
<div
data-testid="screenshot-container"
style={{
width: "100%",
paddingBottom: browserLargeViewport ? "62.5%" : "66.67%", // 800/1280 = 0.625, 600/900 = 0.667
paddingBottom: `${aspectRatio}%`, // height/width ratio
position: "relative",
backgroundColor: "var(--vscode-input-background)",
}}>
@@ -321,8 +326,8 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
<BrowserCursor
style={{
position: "absolute",
top: `${(parseInt(mousePosition.split(",")[1]) / (browserLargeViewport ? 800 : 600)) * 100}%`,
left: `${(parseInt(mousePosition.split(",")[0]) / (browserLargeViewport ? 1280 : 900)) * 100}%`,
top: `${(parseInt(mousePosition.split(",")[1]) / viewportHeight) * 100}%`,
left: `${(parseInt(mousePosition.split(",")[0]) / viewportWidth) * 100}%`,
transition: "top 0.3s ease-out, left 0.3s ease-out",
}}
/>
@@ -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"
/>
)
}

View File

@@ -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<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(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,
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Provider Settings</h3>
<VSCodeButton onClick={handleSubmit}>Done</VSCodeButton>
</div>
<div
@@ -143,6 +147,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
<div style={{ marginBottom: 5 }}>
<div style={{ marginBottom: 15 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Agent Settings</h3>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Preferred Language</label>
<select
value={preferredLanguage}
@@ -264,7 +270,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</p>
</div>
<div style={{ marginBottom: 5, border: "2px solid var(--vscode-errorForeground)", borderRadius: "4px", padding: "10px" }}>
<div style={{ marginBottom: 15, border: "2px solid var(--vscode-errorForeground)", borderRadius: "4px", padding: "10px" }}>
<h4 style={{ fontWeight: 500, margin: "0 0 10px 0", color: "var(--vscode-errorForeground)" }}> High-Risk Auto-Approve Settings</h4>
<p style={{ fontSize: "12px", marginBottom: 15, color: "var(--vscode-descriptionForeground)" }}>
The following settings allow Cline to automatically perform potentially dangerous operations without requiring approval.
@@ -422,24 +428,69 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</div>
<div style={{ marginBottom: 5 }}>
<h4 style={{ fontWeight: 500, marginBottom: 10 }}>Experimental Features</h4>
<div style={{ marginBottom: 10 }}>
<VSCodeCheckbox checked={browserLargeViewport} onChange={(e: any) => setBrowserLargeViewport(e.target.checked)}>
<span style={{ fontWeight: "500" }}>Use larger browser viewport (1280x800)</span>
</VSCodeCheckbox>
<p
style={{
<div style={{ marginBottom: 15 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Browser Settings</h3>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Viewport Size</label>
<select
value={browserViewportSize}
onChange={(e) => 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"
}}>
<option value="1280x800">Large Desktop (1280x800)</option>
<option value="900x600">Small Desktop (900x600)</option>
<option value="768x1024">Tablet (768x1024)</option>
<option value="360x640">Mobile (360x640)</option>
</select>
<p style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
When enabled, Cline will use a larger viewport size for browser interactions.
</p>
Select the viewport size for browser interactions. This affects how websites are displayed and interacted with.
</p>
</div>
<div style={{ marginBottom: 15 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{ fontWeight: "500", minWidth: '100px' }}>Screenshot Quality</span>
<input
type="range"
min="1"
max="100"
step="1"
value={screenshotQuality ?? 75}
onChange={(e) => setScreenshotQuality(parseInt(e.target.value))}
style={{
flexGrow: 1,
accentColor: 'var(--vscode-button-background)',
height: '2px'
}}
/>
<span style={{ minWidth: '35px', textAlign: 'left' }}>
{screenshotQuality ?? 75}%
</span>
</div>
<p style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Adjust the WebP quality of browser screenshots. Higher values provide clearer screenshots but increase token usage.
</p>
</div>
</div>
<div style={{ marginBottom: 5 }}>
<div style={{ marginBottom: 10 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Notification Settings</h3>
<VSCodeCheckbox checked={soundEnabled} onChange={(e: any) => setSoundEnabled(e.target.checked)}>
<span style={{ fontWeight: "500" }}>Enable sound effects</span>
</VSCodeCheckbox>
@@ -468,9 +519,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
accentColor: 'var(--vscode-button-background)',
height: '2px'
}}
aria-label="Volume"
/>
<span style={{ minWidth: '35px', textAlign: 'left' }}>
{Math.round((soundVolume ?? 0.5) * 100)}%
{((soundVolume ?? 0.5) * 100).toFixed(0)}%
</span>
</div>
</div>

View File

@@ -61,6 +61,17 @@ jest.mock('@vscode/webview-ui-toolkit/react', () => ({
<div onChange={onChange}>
{children}
</div>
),
VSCodeSlider: ({ value, onChange }: any) => (
<input
type="range"
value={value}
onChange={(e) => 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
})
})
})

View File

@@ -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<ExtensionStateContextType | undefined>(undefined)
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, setState] = useState<ExtensionState>({
@@ -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 <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>