mirror of
https://github.com/cline/cline.git
synced 2026-09-24 23:20:16 +08:00
Scroll to message onclick from task timeline (#3890)
* Scroll to message onclick from task timeline * version * fixing ellipsis-dev's suggestion on potential infinite loop
This commit is contained in:
@@ -135,6 +135,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
const disableAutoScrollRef = useRef(false)
|
||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
||||
const [isAtBottom, setIsAtBottom] = useState(false)
|
||||
const [pendingScrollToMessage, setPendingScrollToMessage] = useState<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleCopy = async (e: ClipboardEvent) => {
|
||||
@@ -887,6 +888,61 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
})
|
||||
}, [])
|
||||
|
||||
const scrollToMessage = useCallback(
|
||||
(messageIndex: number) => {
|
||||
setPendingScrollToMessage(messageIndex)
|
||||
|
||||
const targetMessage = messages[messageIndex]
|
||||
if (!targetMessage) {
|
||||
setPendingScrollToMessage(null)
|
||||
return
|
||||
}
|
||||
|
||||
const visibleIndex = visibleMessages.findIndex((msg) => msg.ts === targetMessage.ts)
|
||||
if (visibleIndex === -1) {
|
||||
setPendingScrollToMessage(null)
|
||||
return
|
||||
}
|
||||
|
||||
let groupIndex = -1
|
||||
let currentVisibleIndex = 0
|
||||
|
||||
for (let i = 0; i < groupedMessages.length; i++) {
|
||||
const group = groupedMessages[i]
|
||||
if (Array.isArray(group)) {
|
||||
const groupSize = group.length
|
||||
const messageInGroup = group.some((msg) => msg.ts === targetMessage.ts)
|
||||
if (messageInGroup) {
|
||||
groupIndex = i
|
||||
break
|
||||
}
|
||||
currentVisibleIndex += groupSize
|
||||
} else {
|
||||
if (group.ts === targetMessage.ts) {
|
||||
groupIndex = i
|
||||
break
|
||||
}
|
||||
currentVisibleIndex++
|
||||
}
|
||||
}
|
||||
|
||||
if (groupIndex !== -1) {
|
||||
setPendingScrollToMessage(null)
|
||||
disableAutoScrollRef.current = true
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
virtuosoRef.current?.scrollToIndex({
|
||||
index: groupIndex,
|
||||
align: "start",
|
||||
behavior: "smooth",
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
[messages, visibleMessages, groupedMessages],
|
||||
)
|
||||
|
||||
// scroll when user toggles certain rows
|
||||
const toggleRowExpansion = useCallback(
|
||||
(ts: number) => {
|
||||
@@ -966,6 +1022,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
}
|
||||
}, [groupedMessages.length, scrollToBottomSmooth])
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingScrollToMessage !== null) {
|
||||
scrollToMessage(pendingScrollToMessage)
|
||||
}
|
||||
}, [pendingScrollToMessage, groupedMessages, scrollToMessage])
|
||||
|
||||
const handleWheel = useCallback((event: Event) => {
|
||||
const wheelEvent = event as WheelEvent
|
||||
if (wheelEvent.deltaY && wheelEvent.deltaY < 0) {
|
||||
@@ -1063,6 +1125,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
totalCost={apiMetrics.totalCost}
|
||||
lastApiReqTotalTokens={lastApiReqTotalTokens}
|
||||
onClose={handleTaskCloseButtonClick}
|
||||
onScrollToMessage={scrollToMessage}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
|
||||
@@ -24,6 +24,7 @@ interface TaskHeaderProps {
|
||||
totalCost: number
|
||||
lastApiReqTotalTokens?: number
|
||||
onClose: () => void
|
||||
onScrollToMessage?: (messageIndex: number) => void
|
||||
}
|
||||
|
||||
const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
@@ -36,6 +37,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
totalCost,
|
||||
lastApiReqTotalTokens,
|
||||
onClose,
|
||||
onScrollToMessage,
|
||||
}) => {
|
||||
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage, clineMessages, navigateToSettings } =
|
||||
useExtensionState()
|
||||
@@ -469,7 +471,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<TaskTimeline messages={clineMessages} />
|
||||
<TaskTimeline messages={clineMessages} onBlockClick={onScrollToMessage} />
|
||||
{ContextWindowComponent}
|
||||
</div>
|
||||
{checkpointTrackerErrorMessage && (
|
||||
|
||||
@@ -14,6 +14,7 @@ const TOOLTIP_MARGIN = 32 // 32px margin on each side
|
||||
|
||||
interface TaskTimelineProps {
|
||||
messages: ClineMessage[]
|
||||
onBlockClick?: (messageIndex: number) => void
|
||||
}
|
||||
|
||||
const getBlockColor = (message: ClineMessage): string => {
|
||||
@@ -94,16 +95,19 @@ const getBlockColor = (message: ClineMessage): string => {
|
||||
return COLOR_WHITE // Default color
|
||||
}
|
||||
|
||||
const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
|
||||
const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages, onBlockClick }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const scrollableRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const taskTimelinePropsMessages = useMemo(() => {
|
||||
if (messages.length <= 1) return []
|
||||
const { taskTimelinePropsMessages, messageIndexMap } = useMemo(() => {
|
||||
if (messages.length <= 1) return { taskTimelinePropsMessages: [], messageIndexMap: [] }
|
||||
|
||||
const processed = combineApiRequests(combineCommandSequences(messages.slice(1)))
|
||||
const indexMap: number[] = []
|
||||
|
||||
const filtered = processed.filter((msg, processedIndex) => {
|
||||
const originalIndex = messages.findIndex((originalMsg, idx) => idx > 0 && originalMsg.ts === msg.ts)
|
||||
|
||||
return processed.filter((msg) => {
|
||||
// Filter out standard "say" events we don't want to show
|
||||
if (
|
||||
msg.type === "say" &&
|
||||
@@ -124,9 +128,13 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (originalIndex !== -1) {
|
||||
indexMap.push(originalIndex)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
return { taskTimelinePropsMessages: filtered, messageIndexMap: indexMap }
|
||||
}, [messages])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -145,9 +153,18 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
|
||||
const TimelineBlock = useCallback(
|
||||
(index: number) => {
|
||||
const message = taskTimelinePropsMessages[index]
|
||||
const originalMessageIndex = messageIndexMap[index]
|
||||
|
||||
const handleClick = () => {
|
||||
if (onBlockClick && originalMessageIndex !== undefined) {
|
||||
onBlockClick(originalMessageIndex)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TaskTimelineTooltip message={message}>
|
||||
<div
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
width: BLOCK_WIDTH,
|
||||
height: "100%",
|
||||
@@ -160,7 +177,7 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
|
||||
</TaskTimelineTooltip>
|
||||
)
|
||||
},
|
||||
[taskTimelinePropsMessages],
|
||||
[taskTimelinePropsMessages, messageIndexMap, onBlockClick],
|
||||
)
|
||||
|
||||
// Scroll to the end when messages change
|
||||
|
||||
Reference in New Issue
Block a user