mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Add pagination and screenshots to browser
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
import deepEqual from "fast-deep-equal"
|
import deepEqual from "fast-deep-equal"
|
||||||
import React, { memo, useEffect, useRef, useState } from "react"
|
import React, { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { useSize } from "react-use"
|
import { useSize } from "react-use"
|
||||||
import { BrowserActionResult, ClineMessage, ClineSayBrowserAction } from "../../../../src/shared/ExtensionMessage"
|
import { BrowserActionResult, ClineMessage, ClineSayBrowserAction } from "../../../../src/shared/ExtensionMessage"
|
||||||
import { vscode } from "../../utils/vscode"
|
import { vscode } from "../../utils/vscode"
|
||||||
import CodeAccordian from "../common/CodeAccordian"
|
import CodeAccordian from "../common/CodeAccordian"
|
||||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||||
import { ChatRowContent } from "./ChatRow"
|
import { ChatRowContent } from "./ChatRow"
|
||||||
|
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||||
|
|
||||||
interface BrowserSessionRowProps {
|
interface BrowserSessionRowProps {
|
||||||
messages: ClineMessage[]
|
messages: ClineMessage[]
|
||||||
@@ -25,29 +26,134 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||||||
const { messages, isLast, onHeightChange } = props
|
const { messages, isLast, onHeightChange } = props
|
||||||
const prevHeightRef = useRef(0)
|
const prevHeightRef = useRef(0)
|
||||||
|
|
||||||
const [consoleLogsExpanded, setConsoleLogsExpanded] = useState(false)
|
// Organize messages into pages with current state and next action
|
||||||
const consoleLogs = "console logs\nhere\n..."
|
const pages = useMemo(() => {
|
||||||
|
const result: {
|
||||||
|
currentState: {
|
||||||
|
url?: string
|
||||||
|
screenshot?: 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") {
|
||||||
|
// 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,
|
||||||
|
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,
|
||||||
|
screenshot: page.currentState.screenshot,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { url: 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,
|
||||||
|
screenshot: currentPage?.currentState.screenshot || latestState.screenshot,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
url: currentPage?.currentState.url || initialUrl,
|
||||||
|
screenshot: currentPage?.currentState.screenshot,
|
||||||
|
}
|
||||||
|
|
||||||
const [browserSessionRow, { height }] = useSize(
|
const [browserSessionRow, { height }] = useSize(
|
||||||
<div style={{ padding: "10px 6px 10px 15px" }}>
|
<div style={{ padding: "10px 6px 10px 15px" }}>
|
||||||
<h3>Browser Session Group</h3>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
border: "1px solid var(--vscode-editorGroup-border)",
|
border: "1px solid var(--vscode-editorGroup-border)",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
backgroundColor: CODE_BLOCK_BG_COLOR,
|
backgroundColor: CODE_BLOCK_BG_COLOR,
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}>
|
}}>
|
||||||
|
{/* URL Bar */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "calc(100% - 10px)",
|
|
||||||
boxSizing: "border-box", // includes padding in width calculation
|
|
||||||
margin: "5px auto",
|
margin: "5px auto",
|
||||||
|
width: "calc(100% - 10px)",
|
||||||
backgroundColor: "var(--vscode-input-background)",
|
backgroundColor: "var(--vscode-input-background)",
|
||||||
border: "1px solid var(--vscode-input-border)",
|
border: "1px solid var(--vscode-input-border)",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
@@ -57,79 +163,90 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
color: "var(--vscode-input-foreground)",
|
color: "var(--vscode-input-foreground)",
|
||||||
fontSize: "12px",
|
fontSize: "12px",
|
||||||
wordBreak: "break-all", // Allow breaks anywhere
|
wordBreak: "break-all",
|
||||||
whiteSpace: "normal", // Allow wrapping
|
whiteSpace: "normal",
|
||||||
}}>
|
}}>
|
||||||
{"https://example.com/thisisalongurl/asdfasfdasdf?asdfasdfasf"}
|
{displayState.url}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Screenshot Area */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
paddingBottom: "75%", // This creates a 4:3 aspect ratio
|
paddingBottom: "75%",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
backgroundColor: "var(--vscode-input-background)",
|
||||||
}}>
|
}}>
|
||||||
{/* <div
|
{displayState.screenshot ? (
|
||||||
|
<img
|
||||||
|
src={displayState.screenshot}
|
||||||
|
alt="Browser screenshot"
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
width: "100%",
|
||||||
bottom: 0,
|
height: "100%",
|
||||||
backgroundColor: "red",
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/> */}
|
onClick={() =>
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "openImage",
|
||||||
|
text: displayState.screenshot,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "50%",
|
top: "50%",
|
||||||
left: "50%",
|
left: "50%",
|
||||||
transform: "translate(-50%, -50%)",
|
transform: "translate(-50%, -50%)",
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
}}>
|
}}>
|
||||||
<span
|
<span
|
||||||
className="codicon codicon-globe"
|
className="codicon codicon-globe"
|
||||||
style={{
|
style={{ fontSize: "80px", color: "var(--vscode-descriptionForeground)" }}
|
||||||
fontSize: "80px",
|
/>
|
||||||
color: "var(--vscode-input-background)",
|
|
||||||
}}></span>
|
|
||||||
</div>
|
</div>
|
||||||
<BrowserCursor style={{ position: "absolute", bottom: "10%", right: "20%" }} />
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: "100%" }}>
|
|
||||||
|
{/* Pagination */}
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
|
||||||
if (consoleLogs) {
|
|
||||||
setConsoleLogsExpanded(!consoleLogsExpanded)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "4px",
|
padding: "8px",
|
||||||
width: "100%",
|
borderTop: "1px solid var(--vscode-editorGroup-border)",
|
||||||
justifyContent: "flex-start",
|
|
||||||
cursor: consoleLogs ? "pointer" : "default",
|
|
||||||
opacity: consoleLogs ? 1 : 0.5,
|
|
||||||
padding: `8px 8px ${consoleLogsExpanded ? 0 : 8}px 8px`,
|
|
||||||
}}>
|
}}>
|
||||||
<span className={`codicon codicon-chevron-${consoleLogsExpanded ? "down" : "right"}`}></span>
|
<div>
|
||||||
<span style={{ fontSize: "0.8em" }}>Console Logs</span>
|
Step {currentPageIndex + 1} of {pages.length}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: "4px" }}>
|
||||||
|
<VSCodeButton
|
||||||
|
disabled={currentPageIndex === 0}
|
||||||
|
onClick={() => setCurrentPageIndex((i) => i - 1)}>
|
||||||
|
Previous
|
||||||
|
</VSCodeButton>
|
||||||
|
<VSCodeButton
|
||||||
|
disabled={currentPageIndex === pages.length - 1}
|
||||||
|
onClick={() => setCurrentPageIndex((i) => i + 1)}>
|
||||||
|
Next
|
||||||
|
</VSCodeButton>
|
||||||
</div>
|
</div>
|
||||||
{consoleLogsExpanded && <CodeBlock source={`${"```"}shell\n${consoleLogs}\n${"```"}`} />}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{messages.map((message, index) => (
|
{/* Show next action messages if they exist */}
|
||||||
|
{currentPage?.nextAction?.messages.map((message) => (
|
||||||
<BrowserSessionRowContent key={message.ts} {...props} message={message} />
|
<BrowserSessionRowContent key={message.ts} {...props} message={message} />
|
||||||
))}
|
))}
|
||||||
<h3>END Browser Session Group</h3>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Height change effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isInitialRender = prevHeightRef.current === 0
|
const isInitialRender = prevHeightRef.current === 0
|
||||||
if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) {
|
if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) {
|
||||||
|
|||||||
Reference in New Issue
Block a user