Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<
Expand Down Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The !isAtBottom condition prevents the sticky nav from showing when the user is at the bottom of the chat. Based on issue #10690, the primary use case is when a user is at the bottom viewing a long AI response and wants to jump back to their previous messages. With this condition, the nav won't appear in that scenario since isAtBottom would be true. Consider removing !isAtBottom or inverting the logic if the intent is to show the nav when user messages are scrolled out of view above.

Suggested change
return stickyNavMessage !== null && firstVisibleIndex > 0 && !isAtBottom
return stickyNavMessage !== null && firstVisibleIndex > 0

Fix it with Roo Code or mention @roomote and request a fix.

}, [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(
Expand Down Expand Up @@ -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}
Expand All @@ -1506,6 +1553,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
}}
atBottomThreshold={10}
initialTopMostItemIndex={groupedMessages.length - 1}
rangeChanged={(range) => {
setFirstVisibleIndex(range.startIndex)
}}
/>
</div>
{areButtonsVisible && (
Expand Down
63 changes: 63 additions & 0 deletions webview-ui/src/components/chat/StickyUserMessageNav.tsx
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/en/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -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