Skip to content

Commit a41d774

Browse files
committed
fix: prevent browser screenshots from displaying as base64 text after multiple actions
- Added ScreenshotImage component with proper error handling and validation - Validates screenshot data URLs before rendering to ensure they are proper image data - Provides fallback UI when screenshot data is invalid or corrupted - Fixes issue where screenshots would display as raw base64 text after ~5 browser actions Fixes #7795
1 parent 0ce4e89 commit a41d774

File tree

1 file changed

+101
-12
lines changed

1 file changed

+101
-12
lines changed

webview-ui/src/components/chat/BrowserSessionRow.tsx

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,27 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
8888
currentStateMessages.push(message)
8989
const resultData = JSON.parse(message.text || "{}") as BrowserActionResult
9090

91+
// Validate screenshot data to ensure it's a proper data URL
92+
let validatedScreenshot = resultData.screenshot
93+
if (validatedScreenshot && typeof validatedScreenshot === "string") {
94+
// Ensure the screenshot is a valid data URL
95+
if (!validatedScreenshot.startsWith("data:image/")) {
96+
// If it's just base64 without the data URL prefix, add it
97+
if (validatedScreenshot.match(/^[A-Za-z0-9+/]+=*$/)) {
98+
validatedScreenshot = `data:image/webp;base64,${validatedScreenshot}`
99+
} else {
100+
// Invalid screenshot data, set to undefined
101+
console.warn("Invalid screenshot data detected, skipping screenshot")
102+
validatedScreenshot = undefined
103+
}
104+
}
105+
}
106+
91107
// Add page with current state and previous next actions
92108
result.push({
93109
currentState: {
94110
url: resultData.currentUrl,
95-
screenshot: resultData.screenshot,
111+
screenshot: validatedScreenshot,
96112
mousePosition: resultData.currentMousePosition,
97113
consoleLogs: resultData.logs,
98114
messages: [...currentStateMessages],
@@ -296,18 +312,9 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
296312
backgroundColor: "var(--vscode-input-background)",
297313
}}>
298314
{displayState.screenshot ? (
299-
<img
300-
src={displayState.screenshot}
315+
<ScreenshotImage
316+
screenshot={displayState.screenshot}
301317
alt={t("chat:browser.screenshot")}
302-
style={{
303-
position: "absolute",
304-
top: 0,
305-
left: 0,
306-
width: "100%",
307-
height: "100%",
308-
objectFit: "contain",
309-
cursor: "pointer",
310-
}}
311318
onClick={() =>
312319
vscode.postMessage({
313320
type: "openImage",
@@ -576,4 +583,86 @@ const BrowserCursor: React.FC<{ style?: React.CSSProperties }> = ({ style }) =>
576583
)
577584
}
578585

586+
// Separate component to handle screenshot rendering with error boundaries
587+
const ScreenshotImage: React.FC<{
588+
screenshot: string
589+
alt: string
590+
onClick: () => void
591+
}> = React.memo(({ screenshot, alt, onClick }) => {
592+
const [imageError, setImageError] = useState(false)
593+
const [validatedSrc, setValidatedSrc] = useState<string>("")
594+
595+
useEffect(() => {
596+
// Validate and sanitize the screenshot data
597+
if (screenshot && typeof screenshot === "string") {
598+
// Check if it's a valid data URL
599+
if (screenshot.startsWith("data:image/")) {
600+
// Extract the base64 part and validate it
601+
const base64Match = screenshot.match(/^data:image\/[^;]+;base64,(.+)$/)
602+
if (base64Match && base64Match[1]) {
603+
try {
604+
// Validate base64 by attempting to decode a small portion
605+
// This will throw if the base64 is invalid
606+
atob(base64Match[1].substring(0, 100))
607+
// If successful, use the original screenshot
608+
setValidatedSrc(screenshot)
609+
setImageError(false)
610+
} catch (_e) {
611+
console.error("Invalid base64 in screenshot data")
612+
setImageError(true)
613+
}
614+
} else {
615+
setImageError(true)
616+
}
617+
} else {
618+
setImageError(true)
619+
}
620+
} else {
621+
setImageError(true)
622+
}
623+
}, [screenshot])
624+
625+
if (imageError || !validatedSrc) {
626+
// Fallback UI when screenshot is invalid
627+
return (
628+
<div
629+
style={{
630+
position: "absolute",
631+
top: 0,
632+
left: 0,
633+
width: "100%",
634+
height: "100%",
635+
display: "flex",
636+
alignItems: "center",
637+
justifyContent: "center",
638+
backgroundColor: "var(--vscode-editor-background)",
639+
color: "var(--vscode-descriptionForeground)",
640+
fontSize: "12px",
641+
textAlign: "center",
642+
padding: "20px",
643+
}}>
644+
Screenshot unavailable
645+
</div>
646+
)
647+
}
648+
649+
return (
650+
<img
651+
src={validatedSrc}
652+
alt={alt}
653+
style={{
654+
position: "absolute",
655+
top: 0,
656+
left: 0,
657+
width: "100%",
658+
height: "100%",
659+
objectFit: "contain",
660+
cursor: "pointer",
661+
}}
662+
onClick={onClick}
663+
onError={() => setImageError(true)}
664+
/>
665+
)
666+
})
667+
579668
export default BrowserSessionRow

0 commit comments

Comments
 (0)