mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Implement virtualized rendering of items in the chat view
This commit is contained in:
13
webview-ui/package-lock.json
generated
13
webview-ui/package-lock.json
generated
@@ -23,6 +23,7 @@
|
|||||||
"react-text-truncate": "^0.19.0",
|
"react-text-truncate": "^0.19.0",
|
||||||
"react-textarea-autosize": "^8.5.3",
|
"react-textarea-autosize": "^8.5.3",
|
||||||
"react-use": "^17.5.1",
|
"react-use": "^17.5.1",
|
||||||
|
"react-virtuoso": "^4.7.13",
|
||||||
"rewire": "^7.0.0",
|
"rewire": "^7.0.0",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
@@ -16567,6 +16568,18 @@
|
|||||||
"react-dom": "*"
|
"react-dom": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-virtuoso": {
|
||||||
|
"version": "4.7.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.7.13.tgz",
|
||||||
|
"integrity": "sha512-rabPhipwJ8rdA6TDk1vdVqVoU6eOkWukqoC1pNQVBCsvjBvIeJMi9nO079s0L7EsRzAxFFQNahX+8vuuY4F1Qg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16 || >=17 || >= 18",
|
||||||
|
"react-dom": ">=16 || >=17 || >= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"react-text-truncate": "^0.19.0",
|
"react-text-truncate": "^0.19.0",
|
||||||
"react-textarea-autosize": "^8.5.3",
|
"react-textarea-autosize": "^8.5.3",
|
||||||
"react-use": "^17.5.1",
|
"react-use": "^17.5.1",
|
||||||
|
"react-virtuoso": "^4.7.13",
|
||||||
"rewire": "^7.0.0",
|
"rewire": "^7.0.0",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
case "api_req_finished":
|
case "api_req_finished":
|
||||||
return null // Hide this message type
|
return null // we should never see this message type
|
||||||
case "text":
|
case "text":
|
||||||
return <p style={contentStyle}>{message.text}</p>
|
return <p style={contentStyle}>{message.text}</p>
|
||||||
case "user_feedback":
|
case "user_feedback":
|
||||||
@@ -332,18 +332,14 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we need to return null here instead of in getContent since that way would result in padding being applied
|
// we need to return null here instead of in getContent since that way would result in padding being applied
|
||||||
if (message.say === "api_req_finished") {
|
if (!shouldShowChatRow(message)) {
|
||||||
return null // Don't render anything for this message type
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === "ask" && message.ask === "completion_result" && message.text === "") {
|
|
||||||
return null // Don't render anything for this message type
|
return null // Don't render anything for this message type
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "10px 0px 10px 0px",
|
padding: "10px 6px 10px 15px",
|
||||||
}}>
|
}}>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
{isExpanded && message.say === "api_req_started" && (
|
{isExpanded && message.say === "api_req_started" && (
|
||||||
@@ -359,4 +355,18 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const shouldShowChatRow = (message: ClaudeMessage) => {
|
||||||
|
// combineApiRequests removes this from modifiedMessages anyways
|
||||||
|
if (message.say === "api_req_finished") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't show a chat row for a completion_result ask without text. This specific type of message only occurs if Claude wants to execute a command as part of its completion result, in which case we interject the completion_result tool with the execute_command tool.
|
||||||
|
if (message.type === "ask" && message.ask === "completion_result" && message.text === "") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
export default ChatRow
|
export default ChatRow
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import { combineCommandSequences } from "../utilities/combineCommandSequences"
|
|||||||
import { getApiMetrics } from "../utilities/getApiMetrics"
|
import { getApiMetrics } from "../utilities/getApiMetrics"
|
||||||
import { getSyntaxHighlighterStyleFromTheme } from "../utilities/getSyntaxHighlighterStyleFromTheme"
|
import { getSyntaxHighlighterStyleFromTheme } from "../utilities/getSyntaxHighlighterStyleFromTheme"
|
||||||
import { vscode } from "../utilities/vscode"
|
import { vscode } from "../utilities/vscode"
|
||||||
import ChatRow from "./ChatRow"
|
import ChatRow, { shouldShowChatRow } from "./ChatRow"
|
||||||
import TaskHeader from "./TaskHeader"
|
import TaskHeader from "./TaskHeader"
|
||||||
|
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
|
||||||
|
|
||||||
interface ChatViewProps {
|
interface ChatViewProps {
|
||||||
messages: ClaudeMessage[]
|
messages: ClaudeMessage[]
|
||||||
@@ -39,7 +40,7 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
|
|||||||
|
|
||||||
const [syntaxHighlighterStyle, setSyntaxHighlighterStyle] = useState(vsDarkPlus)
|
const [syntaxHighlighterStyle, setSyntaxHighlighterStyle] = useState(vsDarkPlus)
|
||||||
|
|
||||||
const chatContainerRef = useRef<HTMLDivElement>(null)
|
const virtuosoRef = useRef<VirtuosoHandle>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!vscodeThemeName) return
|
if (!vscodeThemeName) return
|
||||||
@@ -49,31 +50,6 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
|
|||||||
}
|
}
|
||||||
}, [vscodeThemeName])
|
}, [vscodeThemeName])
|
||||||
|
|
||||||
const scrollToBottom = (instant: boolean = false) => {
|
|
||||||
if (chatContainerRef.current) {
|
|
||||||
const scrollOptions: ScrollToOptions = {
|
|
||||||
top: chatContainerRef.current.scrollHeight,
|
|
||||||
behavior: instant ? "auto" : "smooth",
|
|
||||||
}
|
|
||||||
chatContainerRef.current.scrollTo(scrollOptions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// scroll to bottom when new message is added
|
|
||||||
const visibleMessages = useMemo(
|
|
||||||
() =>
|
|
||||||
modifiedMessages.filter(
|
|
||||||
(message) => !(message.type === "ask" && message.ask === "completion_result" && message.text === "")
|
|
||||||
),
|
|
||||||
[modifiedMessages]
|
|
||||||
)
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
scrollToBottom()
|
|
||||||
}, 0)
|
|
||||||
return () => clearTimeout(timer)
|
|
||||||
}, [visibleMessages])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// if last message is an ask, show user ask UI
|
// if last message is an ask, show user ask UI
|
||||||
|
|
||||||
@@ -323,18 +299,27 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<Virtuoso
|
||||||
ref={chatContainerRef}
|
ref={virtuosoRef}
|
||||||
className="scrollable"
|
className="scrollable"
|
||||||
style={{
|
style={{
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
overflowY: "scroll",
|
overflowY: "scroll", // always show scrollbar
|
||||||
padding: "0 6px 0 15px",
|
}}
|
||||||
}}>
|
followOutput={(isAtBottom) => {
|
||||||
{modifiedMessages.map((message, index) => (
|
// TODO: we can use isAtBottom to prevent scrolling if user is scrolled up, and show a 'scroll to bottom' button for better UX
|
||||||
<ChatRow key={index} message={message} syntaxHighlighterStyle={syntaxHighlighterStyle} />
|
const lastMessage = modifiedMessages.at(-1)
|
||||||
))}
|
if (lastMessage && shouldShowChatRow(lastMessage)) {
|
||||||
</div>
|
return "smooth"
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}}
|
||||||
|
increaseViewportBy={{ top: 0, bottom: Infinity }} // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added
|
||||||
|
data={modifiedMessages}
|
||||||
|
itemContent={(index, message) => (
|
||||||
|
<ChatRow key={message.ts} message={message} syntaxHighlighterStyle={syntaxHighlighterStyle} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
opacity: primaryButtonText || secondaryButtonText ? (enableButtons ? 1 : 0.5) : 0,
|
opacity: primaryButtonText || secondaryButtonText ? (enableButtons ? 1 : 0.5) : 0,
|
||||||
@@ -370,7 +355,9 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
|
|||||||
disabled={textAreaDisabled}
|
disabled={textAreaDisabled}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onHeightChange={() => scrollToBottom(true)}
|
onHeightChange={() =>
|
||||||
|
virtuosoRef.current?.scrollToIndex({ index: "LAST", align: "end", behavior: "auto" })
|
||||||
|
}
|
||||||
placeholder={task ? "Type a message..." : "Type your task here..."}
|
placeholder={task ? "Type a message..." : "Type your task here..."}
|
||||||
maxRows={10}
|
maxRows={10}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
|||||||
Reference in New Issue
Block a user