mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: add sticky user message navigation for easier scrolling
Implements a sticky navigation banner that appears at the top of the chat when users scroll up past their messages. Clicking the banner scrolls to the most recent out-of-view user message, making it easier to navigate long conversations. Addresses Issue #10690
This commit is contained in:
parent
78821a3951
commit
98ad090a77
3 changed files with 118 additions and 1 deletions
|
|
@ -44,6 +44,7 @@ import SystemPromptWarning from "./SystemPromptWarning"
|
|||
import ProfileViolationWarning from "./ProfileViolationWarning"
|
||||
import { CheckpointWarning } from "./CheckpointWarning"
|
||||
import { QueuedMessages } from "./QueuedMessages"
|
||||
import StickyUserMessageNav from "./StickyUserMessageNav"
|
||||
import DismissibleUpsell from "../common/DismissibleUpsell"
|
||||
import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
|
||||
import { Cloud } from "lucide-react"
|
||||
|
|
@ -150,6 +151,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
const stickyFollowRef = useRef<boolean>(false)
|
||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
||||
const [isAtBottom, setIsAtBottom] = useState(false)
|
||||
const [firstVisibleIndex, setFirstVisibleIndex] = useState(0)
|
||||
const lastTtsRef = useRef<string>("")
|
||||
const [wasStreaming, setWasStreaming] = useState<boolean>(false)
|
||||
const [checkpointWarning, setCheckpointWarning] = useState<
|
||||
|
|
@ -1113,6 +1115,46 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
return result
|
||||
}, [isCondensing, visibleMessages, isBrowserSessionMessage])
|
||||
|
||||
// Compute user message indices from groupedMessages for sticky navigation
|
||||
const userMessageIndices = useMemo(() => {
|
||||
const indices: number[] = []
|
||||
groupedMessages.forEach((msg, index) => {
|
||||
if (msg.say === "user_feedback") {
|
||||
indices.push(index)
|
||||
}
|
||||
})
|
||||
return indices
|
||||
}, [groupedMessages])
|
||||
|
||||
// Find the last user message that is scrolled out of view (before firstVisibleIndex)
|
||||
const stickyNavMessage = useMemo(() => {
|
||||
// Find the last user message index that is before the first visible index
|
||||
const lastUserIndexBeforeVisible = userMessageIndices.findLast((idx) => idx < firstVisibleIndex)
|
||||
if (lastUserIndexBeforeVisible !== undefined) {
|
||||
return groupedMessages[lastUserIndexBeforeVisible]
|
||||
}
|
||||
return null
|
||||
}, [userMessageIndices, firstVisibleIndex, groupedMessages])
|
||||
|
||||
// Show sticky nav when user has scrolled up past a user message and not at top
|
||||
const showStickyNav = useMemo(() => {
|
||||
return stickyNavMessage !== null && firstVisibleIndex > 0 && !isAtBottom
|
||||
}, [stickyNavMessage, firstVisibleIndex, isAtBottom])
|
||||
|
||||
// Handler to scroll to the sticky nav message
|
||||
const handleStickyNavClick = useCallback(() => {
|
||||
if (stickyNavMessage) {
|
||||
const messageIndex = groupedMessages.findIndex((msg) => msg.ts === stickyNavMessage.ts)
|
||||
if (messageIndex >= 0) {
|
||||
virtuosoRef.current?.scrollToIndex({
|
||||
index: messageIndex,
|
||||
behavior: "smooth",
|
||||
align: "start",
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [stickyNavMessage, groupedMessages])
|
||||
|
||||
// scrolling
|
||||
|
||||
const scrollToBottomSmooth = useMemo(
|
||||
|
|
@ -1490,7 +1532,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
|
||||
{task && (
|
||||
<>
|
||||
<div className="grow flex" ref={scrollContainerRef}>
|
||||
<div className="grow flex flex-col relative" ref={scrollContainerRef}>
|
||||
<StickyUserMessageNav
|
||||
message={stickyNavMessage}
|
||||
onNavigate={handleStickyNavClick}
|
||||
isVisible={showStickyNav}
|
||||
/>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
key={task.ts}
|
||||
|
|
@ -1506,6 +1553,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
}}
|
||||
atBottomThreshold={10}
|
||||
initialTopMostItemIndex={groupedMessages.length - 1}
|
||||
rangeChanged={(range) => {
|
||||
setFirstVisibleIndex(range.startIndex)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{areButtonsVisible && (
|
||||
|
|
|
|||
63
webview-ui/src/components/chat/StickyUserMessageNav.tsx
Normal file
63
webview-ui/src/components/chat/StickyUserMessageNav.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import React, { memo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ChevronUp, User } from "lucide-react"
|
||||
import type { ClineMessage } from "@roo-code/types"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface StickyUserMessageNavProps {
|
||||
/** The user message to display in the sticky nav */
|
||||
message: ClineMessage | null
|
||||
/** Callback when the sticky nav is clicked to scroll to the message */
|
||||
onNavigate: () => void
|
||||
/** Whether the sticky nav should be visible */
|
||||
isVisible: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A sticky navigation component that appears at the top of the chat when the user
|
||||
* scrolls up past their messages. Clicking it scrolls back to the most recent
|
||||
* user message that is out of view.
|
||||
*/
|
||||
const StickyUserMessageNav = memo(({ message, onNavigate, isVisible }: StickyUserMessageNavProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!isVisible || !message) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Truncate message text for display
|
||||
const displayText = message.text || ""
|
||||
const truncatedText = displayText.length > 80 ? displayText.substring(0, 80) + "..." : displayText
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"sticky top-0 z-10 flex items-center gap-2 px-3 py-2 cursor-pointer",
|
||||
"bg-vscode-editor-background/95 backdrop-blur-sm",
|
||||
"border-b border-vscode-editorGroup-border",
|
||||
"transition-all duration-200 ease-in-out",
|
||||
"hover:bg-vscode-list-hoverBackground",
|
||||
)}
|
||||
onClick={onNavigate}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault()
|
||||
onNavigate()
|
||||
}
|
||||
}}
|
||||
aria-label={t("chat:stickyNav.scrollToMessage")}>
|
||||
<ChevronUp className="w-4 h-4 text-vscode-descriptionForeground shrink-0" />
|
||||
<User className="w-4 h-4 text-vscode-descriptionForeground shrink-0" />
|
||||
<span className="text-sm text-vscode-foreground truncate flex-1">{truncatedText}</span>
|
||||
<span className="text-xs text-vscode-descriptionForeground shrink-0">
|
||||
{t("chat:stickyNav.clickToJump")}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
StickyUserMessageNav.displayName = "StickyUserMessageNav"
|
||||
|
||||
export default StickyUserMessageNav
|
||||
|
|
@ -477,5 +477,9 @@
|
|||
"updated": "Updated the to-do list",
|
||||
"completed": "Completed",
|
||||
"started": "Started"
|
||||
},
|
||||
"stickyNav": {
|
||||
"scrollToMessage": "Scroll to your message",
|
||||
"clickToJump": "Click to jump"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue