Implement virtualized rendering of items in the chat view

This commit is contained in:
Saoud Rizwan
2024-07-24 12:19:14 -04:00
parent 6649f8f495
commit 9c85a36b2c
4 changed files with 55 additions and 44 deletions

View File

@@ -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",

View File

@@ -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"

View File

@@ -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

View File

@@ -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}