mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
fix: prevent browser screenshot memory accumulation
- Remove old screenshots from apiConversationHistory after each browser action - Simplify BrowserSessionRow to display only current state without pagination - Remove screenshot history and pagination controls This prevents memory issues where screenshots accumulated indefinitely, consuming significant memory and wasting tokens by sending all historical screenshots to the model on every request. Now only the latest screenshot is kept in memory (~100-200KB) instead of potentially MBs of accumulated screenshots.
This commit is contained in:
parent
98b8d5b005
commit
c3bf7fb964
2 changed files with 53 additions and 273 deletions
|
|
@ -148,6 +148,32 @@ export async function browserActionTool(
|
|||
}
|
||||
}
|
||||
|
||||
// Clean up old browser screenshots from API conversation history to prevent memory accumulation
|
||||
// Only keep the latest screenshot - old ones are no longer needed by the model
|
||||
if (browserActionResult?.screenshot) {
|
||||
const apiHistory = cline.apiConversationHistory
|
||||
for (let i = apiHistory.length - 1; i >= 0; i--) {
|
||||
const message = apiHistory[i]
|
||||
if (Array.isArray(message.content)) {
|
||||
// Filter out old screenshot image blocks
|
||||
message.content = message.content.filter((block) => {
|
||||
// Remove base64 image blocks (browser screenshots)
|
||||
// Keep text blocks and other content
|
||||
if (
|
||||
block.type === "image" &&
|
||||
"source" in block &&
|
||||
block.source.type === "base64" &&
|
||||
(block.source.media_type === "image/webp" || block.source.media_type === "image/png")
|
||||
) {
|
||||
// This is likely an old browser screenshot - remove it
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "launch":
|
||||
case "click":
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { memo, useEffect, useMemo, useRef, useState } from "react"
|
|||
import { useSize } from "react-use"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import type { ClineMessage } from "@roo-code/types"
|
||||
|
||||
|
|
@ -12,7 +11,6 @@ import { vscode } from "@src/utils/vscode"
|
|||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import { ChatRowContent } from "./ChatRow"
|
||||
import { ProgressIndicator } from "./ProgressIndicator"
|
||||
import { Globe, Pointer, SquareTerminal } from "lucide-react"
|
||||
|
||||
|
|
@ -30,7 +28,6 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||
const { messages, isLast, onHeightChange, lastModifiedMessage } = props
|
||||
const { t } = useTranslation()
|
||||
const prevHeightRef = useRef(0)
|
||||
const [maxActionHeight, setMaxActionHeight] = useState(0)
|
||||
const [consoleLogsExpanded, setConsoleLogsExpanded] = useState(false)
|
||||
|
||||
const { browserViewportSize = "900x600" } = useExtensionState()
|
||||
|
|
@ -55,182 +52,58 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||
}, [messages, lastModifiedMessage, isLast])
|
||||
|
||||
const isBrowsing = useMemo(() => {
|
||||
return isLast && messages.some((m) => m.say === "browser_action_result") && !isLastApiReqInterrupted // after user approves, browser_action_result with "" is sent to indicate that the session has started
|
||||
return isLast && messages.some((m) => m.say === "browser_action_result") && !isLastApiReqInterrupted
|
||||
}, [isLast, messages, isLastApiReqInterrupted])
|
||||
|
||||
// Organize messages into pages with current state and next action
|
||||
const pages = useMemo(() => {
|
||||
const result: {
|
||||
currentState: {
|
||||
url?: string
|
||||
screenshot?: string
|
||||
mousePosition?: string
|
||||
consoleLogs?: string
|
||||
messages: ClineMessage[] // messages up to and including the result
|
||||
}
|
||||
nextAction?: {
|
||||
messages: ClineMessage[] // messages leading to next result
|
||||
}
|
||||
}[] = []
|
||||
|
||||
let currentStateMessages: ClineMessage[] = []
|
||||
let nextActionMessages: ClineMessage[] = []
|
||||
|
||||
messages.forEach((message) => {
|
||||
if (message.ask === "browser_action_launch") {
|
||||
// Start first page
|
||||
currentStateMessages = [message]
|
||||
} else if (message.say === "browser_action_result") {
|
||||
if (message.text === "") {
|
||||
// first browser_action_result is an empty string that signals that session has started
|
||||
return
|
||||
}
|
||||
// Complete current state
|
||||
currentStateMessages.push(message)
|
||||
const resultData = JSON.parse(message.text || "{}") as BrowserActionResult
|
||||
|
||||
// Add page with current state and previous next actions
|
||||
result.push({
|
||||
currentState: {
|
||||
url: resultData.currentUrl,
|
||||
screenshot: resultData.screenshot,
|
||||
mousePosition: resultData.currentMousePosition,
|
||||
consoleLogs: resultData.logs,
|
||||
messages: [...currentStateMessages],
|
||||
},
|
||||
nextAction:
|
||||
nextActionMessages.length > 0
|
||||
? {
|
||||
messages: [...nextActionMessages],
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
|
||||
// Reset for next page
|
||||
currentStateMessages = []
|
||||
nextActionMessages = []
|
||||
} else if (
|
||||
message.say === "api_req_started" ||
|
||||
message.say === "text" ||
|
||||
message.say === "browser_action"
|
||||
) {
|
||||
// These messages lead to the next result, so they should always go in nextActionMessages
|
||||
nextActionMessages.push(message)
|
||||
} else {
|
||||
// Any other message types
|
||||
currentStateMessages.push(message)
|
||||
}
|
||||
})
|
||||
|
||||
// Add incomplete page if exists
|
||||
if (currentStateMessages.length > 0 || nextActionMessages.length > 0) {
|
||||
result.push({
|
||||
currentState: {
|
||||
messages: [...currentStateMessages],
|
||||
},
|
||||
nextAction:
|
||||
nextActionMessages.length > 0
|
||||
? {
|
||||
messages: [...nextActionMessages],
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}, [messages])
|
||||
|
||||
// Auto-advance to latest page
|
||||
const [currentPageIndex, setCurrentPageIndex] = useState(0)
|
||||
useEffect(() => {
|
||||
setCurrentPageIndex(pages.length - 1)
|
||||
}, [pages.length])
|
||||
|
||||
// Get initial URL from launch message
|
||||
const initialUrl = useMemo(() => {
|
||||
const launchMessage = messages.find((m) => m.ask === "browser_action_launch")
|
||||
return launchMessage?.text || ""
|
||||
}, [messages])
|
||||
|
||||
// Find the latest available URL and screenshot
|
||||
// Find the LATEST browser action result only (no history needed)
|
||||
const latestState = useMemo(() => {
|
||||
for (let i = pages.length - 1; i >= 0; i--) {
|
||||
const page = pages[i]
|
||||
if (page.currentState.url || page.currentState.screenshot) {
|
||||
// Search backwards to find the most recent browser_action_result
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i]
|
||||
if (message.say === "browser_action_result" && message.text && message.text !== "") {
|
||||
const resultData = JSON.parse(message.text) as BrowserActionResult
|
||||
return {
|
||||
url: page.currentState.url,
|
||||
mousePosition: page.currentState.mousePosition,
|
||||
consoleLogs: page.currentState.consoleLogs,
|
||||
screenshot: page.currentState.screenshot,
|
||||
url: resultData.currentUrl,
|
||||
screenshot: resultData.screenshot,
|
||||
mousePosition: resultData.currentMousePosition,
|
||||
consoleLogs: resultData.logs,
|
||||
}
|
||||
}
|
||||
}
|
||||
return { url: undefined, mousePosition: undefined, consoleLogs: undefined, screenshot: undefined }
|
||||
}, [pages])
|
||||
return { url: undefined, screenshot: undefined, mousePosition: undefined, consoleLogs: undefined }
|
||||
}, [messages])
|
||||
|
||||
const currentPage = pages[currentPageIndex]
|
||||
const isLastPage = currentPageIndex === pages.length - 1
|
||||
// Display state is simply the latest state or defaults
|
||||
const displayState = {
|
||||
url: latestState.url || initialUrl,
|
||||
screenshot: latestState.screenshot,
|
||||
mousePosition: latestState.mousePosition || defaultMousePosition,
|
||||
consoleLogs: latestState.consoleLogs,
|
||||
}
|
||||
|
||||
// Use latest state if we're on the last page and don't have a state yet
|
||||
const displayState = isLastPage
|
||||
? {
|
||||
url: currentPage?.currentState.url || latestState.url || initialUrl,
|
||||
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 || defaultMousePosition,
|
||||
consoleLogs: currentPage?.currentState.consoleLogs,
|
||||
screenshot: currentPage?.currentState.screenshot,
|
||||
}
|
||||
|
||||
const [actionContent, { height: actionHeight }] = useSize(
|
||||
<div>
|
||||
{currentPage?.nextAction?.messages.map((message) => (
|
||||
<BrowserSessionRowContent
|
||||
key={message.ts}
|
||||
{...props}
|
||||
message={message}
|
||||
setMaxActionHeight={setMaxActionHeight}
|
||||
/>
|
||||
))}
|
||||
{!isBrowsing && messages.some((m) => m.say === "browser_action_result") && currentPageIndex === 0 && (
|
||||
<BrowserActionBox action={"launch"} text={initialUrl} />
|
||||
)}
|
||||
</div>,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (actionHeight === 0 || actionHeight === Infinity) {
|
||||
return
|
||||
}
|
||||
if (actionHeight > maxActionHeight) {
|
||||
setMaxActionHeight(actionHeight)
|
||||
}
|
||||
}, [actionHeight, maxActionHeight])
|
||||
|
||||
// Track latest click coordinate
|
||||
// Find latest click position for cursor display
|
||||
const latestClickPosition = useMemo(() => {
|
||||
if (!isBrowsing) return undefined
|
||||
|
||||
// Look through current page's next actions for the latest browser_action
|
||||
const actions = currentPage?.nextAction?.messages || []
|
||||
for (let i = actions.length - 1; i >= 0; i--) {
|
||||
const message = actions[i]
|
||||
if (message.say === "browser_action") {
|
||||
const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
|
||||
// Look through messages backwards for the latest browser_action with click
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i]
|
||||
if (message.say === "browser_action" && message.text) {
|
||||
const browserAction = JSON.parse(message.text) as ClineSayBrowserAction
|
||||
if (browserAction.action === "click" && browserAction.coordinate) {
|
||||
return browserAction.coordinate
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}, [isBrowsing, currentPage?.nextAction?.messages])
|
||||
}, [isBrowsing, messages])
|
||||
|
||||
// Use latest click position while browsing, otherwise use display state
|
||||
const mousePosition = isBrowsing
|
||||
? latestClickPosition || displayState.mousePosition
|
||||
: displayState.mousePosition || defaultMousePosition
|
||||
|
|
@ -353,38 +226,6 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||
<CodeBlock source={displayState.consoleLogs || t("chat:browser.noNewLogs")} language="shell" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action content with min height */}
|
||||
<div style={{ minHeight: maxActionHeight }}>{actionContent}</div>
|
||||
|
||||
{/* Pagination moved to bottom */}
|
||||
{pages.length > 1 && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "8px 0px",
|
||||
marginTop: "15px",
|
||||
borderTop: "1px solid var(--vscode-editorGroup-border)",
|
||||
}}>
|
||||
<div>
|
||||
{t("chat:browser.navigation.step", { current: currentPageIndex + 1, total: pages.length })}
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "4px" }}>
|
||||
<VSCodeButton
|
||||
disabled={currentPageIndex === 0 || isBrowsing}
|
||||
onClick={() => setCurrentPageIndex((i) => i - 1)}>
|
||||
{t("chat:browser.navigation.previous")}
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
disabled={currentPageIndex === pages.length - 1 || isBrowsing}
|
||||
onClick={() => setCurrentPageIndex((i) => i + 1)}>
|
||||
{t("chat:browser.navigation.next")}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>,
|
||||
)
|
||||
|
||||
|
|
@ -402,93 +243,6 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||
return browserSessionRow
|
||||
}, deepEqual)
|
||||
|
||||
interface BrowserSessionRowContentProps extends Omit<BrowserSessionRowProps, "messages"> {
|
||||
message: ClineMessage
|
||||
setMaxActionHeight: (height: number) => void
|
||||
isStreaming: boolean
|
||||
}
|
||||
|
||||
const BrowserSessionRowContent = ({
|
||||
message,
|
||||
isExpanded,
|
||||
onToggleExpand,
|
||||
lastModifiedMessage,
|
||||
isLast,
|
||||
setMaxActionHeight,
|
||||
isStreaming,
|
||||
}: BrowserSessionRowContentProps) => {
|
||||
const { t } = useTranslation()
|
||||
const headerStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
marginBottom: "10px",
|
||||
wordBreak: "break-word",
|
||||
}
|
||||
|
||||
switch (message.type) {
|
||||
case "say":
|
||||
switch (message.say) {
|
||||
case "api_req_started":
|
||||
case "text":
|
||||
return (
|
||||
<div style={{ padding: "10px 0 10px 0" }}>
|
||||
<ChatRowContent
|
||||
message={message}
|
||||
isExpanded={isExpanded(message.ts)}
|
||||
onToggleExpand={() => {
|
||||
if (message.say === "api_req_started") {
|
||||
setMaxActionHeight(0)
|
||||
}
|
||||
onToggleExpand(message.ts)
|
||||
}}
|
||||
lastModifiedMessage={lastModifiedMessage}
|
||||
isLast={isLast}
|
||||
isStreaming={isStreaming}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
case "browser_action":
|
||||
const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
|
||||
return (
|
||||
<BrowserActionBox
|
||||
action={browserAction.action}
|
||||
coordinate={browserAction.coordinate}
|
||||
text={browserAction.text}
|
||||
/>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
case "ask":
|
||||
switch (message.ask) {
|
||||
case "browser_action_launch":
|
||||
return (
|
||||
<>
|
||||
<div style={headerStyle}>
|
||||
<span style={{ fontWeight: "bold" }}>{t("chat:browser.sessionStarted")}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 3,
|
||||
border: "1px solid var(--vscode-editorGroup-border)",
|
||||
overflow: "hidden",
|
||||
backgroundColor: CODE_BLOCK_BG_COLOR,
|
||||
}}>
|
||||
<CodeBlock source={message.text} language="shell" />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BrowserActionBox = ({
|
||||
action,
|
||||
coordinate,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue