import deepEqual from "fast-deep-equal" import React, { memo, useEffect, useMemo, useRef, useState } from "react" import { useSize } from "react-use" import { BrowserAction, BrowserActionResult, ClineMessage, ClineSayBrowserAction, } from "../../../../src/shared/ExtensionMessage" import { vscode } from "../../utils/vscode" import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock" import { ChatRowContent, ProgressIndicator } from "./ChatRow" import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" interface BrowserSessionRowProps { messages: ClineMessage[] isExpanded: (messageTs: number) => boolean onToggleExpand: (messageTs: number) => void lastModifiedMessage?: ClineMessage isLast: boolean onHeightChange: (isTaller: boolean) => void } const BrowserSessionRow = memo((props: BrowserSessionRowProps) => { const { messages, isLast, onHeightChange, lastModifiedMessage } = props const prevHeightRef = useRef(0) const [maxActionHeight, setMaxActionHeight] = useState(0) const [consoleLogsExpanded, setConsoleLogsExpanded] = useState(false) const isLastApiReqInterrupted = useMemo(() => { // Check if last api_req_started is cancelled const lastApiReqStarted = [...messages].reverse().find((m) => m.say === "api_req_started") if (lastApiReqStarted?.text != null) { const info = JSON.parse(lastApiReqStarted.text) if (info.cancelReason != null) { return true } } const lastApiReqFailed = isLast && lastModifiedMessage?.ask === "api_req_failed" if (lastApiReqFailed) { return true } return false }, [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 }, [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 const latestState = useMemo(() => { for (let i = pages.length - 1; i >= 0; i--) { const page = pages[i] if (page.currentState.url || page.currentState.screenshot) { return { url: page.currentState.url, mousePosition: page.currentState.mousePosition, consoleLogs: page.currentState.consoleLogs, screenshot: page.currentState.screenshot, } } } return { url: undefined, mousePosition: undefined, consoleLogs: undefined, screenshot: undefined } }, [pages]) const currentPage = pages[currentPageIndex] const isLastPage = currentPageIndex === pages.length - 1 // 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 || "700,400", consoleLogs: currentPage?.currentState.consoleLogs, screenshot: currentPage?.currentState.screenshot || latestState.screenshot, } : { url: currentPage?.currentState.url || initialUrl, mousePosition: currentPage?.currentState.mousePosition || "700,400", consoleLogs: currentPage?.currentState.consoleLogs, screenshot: currentPage?.currentState.screenshot, } const [actionContent, { height: actionHeight }] = useSize(