Skip to content

Commit 5c3e7a3

Browse files
authored
Scroll to message onclick from task timeline (RooCodeInc#3890)
* Scroll to message onclick from task timeline * version * fixing ellipsis-dev's suggestion on potential infinite loop
1 parent d6ccbcd commit 5c3e7a3

File tree

4 files changed

+93
-6
lines changed

4 files changed

+93
-6
lines changed

.changeset/new-ads-yell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
scroll to task timeline

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
135135
const disableAutoScrollRef = useRef(false)
136136
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
137137
const [isAtBottom, setIsAtBottom] = useState(false)
138+
const [pendingScrollToMessage, setPendingScrollToMessage] = useState<number | null>(null)
138139

139140
useEffect(() => {
140141
const handleCopy = async (e: ClipboardEvent) => {
@@ -887,6 +888,61 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
887888
})
888889
}, [])
889890

891+
const scrollToMessage = useCallback(
892+
(messageIndex: number) => {
893+
setPendingScrollToMessage(messageIndex)
894+
895+
const targetMessage = messages[messageIndex]
896+
if (!targetMessage) {
897+
setPendingScrollToMessage(null)
898+
return
899+
}
900+
901+
const visibleIndex = visibleMessages.findIndex((msg) => msg.ts === targetMessage.ts)
902+
if (visibleIndex === -1) {
903+
setPendingScrollToMessage(null)
904+
return
905+
}
906+
907+
let groupIndex = -1
908+
let currentVisibleIndex = 0
909+
910+
for (let i = 0; i < groupedMessages.length; i++) {
911+
const group = groupedMessages[i]
912+
if (Array.isArray(group)) {
913+
const groupSize = group.length
914+
const messageInGroup = group.some((msg) => msg.ts === targetMessage.ts)
915+
if (messageInGroup) {
916+
groupIndex = i
917+
break
918+
}
919+
currentVisibleIndex += groupSize
920+
} else {
921+
if (group.ts === targetMessage.ts) {
922+
groupIndex = i
923+
break
924+
}
925+
currentVisibleIndex++
926+
}
927+
}
928+
929+
if (groupIndex !== -1) {
930+
setPendingScrollToMessage(null)
931+
disableAutoScrollRef.current = true
932+
requestAnimationFrame(() => {
933+
requestAnimationFrame(() => {
934+
virtuosoRef.current?.scrollToIndex({
935+
index: groupIndex,
936+
align: "start",
937+
behavior: "smooth",
938+
})
939+
})
940+
})
941+
}
942+
},
943+
[messages, visibleMessages, groupedMessages],
944+
)
945+
890946
// scroll when user toggles certain rows
891947
const toggleRowExpansion = useCallback(
892948
(ts: number) => {
@@ -966,6 +1022,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
9661022
}
9671023
}, [groupedMessages.length, scrollToBottomSmooth])
9681024

1025+
useEffect(() => {
1026+
if (pendingScrollToMessage !== null) {
1027+
scrollToMessage(pendingScrollToMessage)
1028+
}
1029+
}, [pendingScrollToMessage, groupedMessages, scrollToMessage])
1030+
9691031
const handleWheel = useCallback((event: Event) => {
9701032
const wheelEvent = event as WheelEvent
9711033
if (wheelEvent.deltaY && wheelEvent.deltaY < 0) {
@@ -1063,6 +1125,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
10631125
totalCost={apiMetrics.totalCost}
10641126
lastApiReqTotalTokens={lastApiReqTotalTokens}
10651127
onClose={handleTaskCloseButtonClick}
1128+
onScrollToMessage={scrollToMessage}
10661129
/>
10671130
) : (
10681131
<div

webview-ui/src/components/chat/TaskHeader.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface TaskHeaderProps {
2424
totalCost: number
2525
lastApiReqTotalTokens?: number
2626
onClose: () => void
27+
onScrollToMessage?: (messageIndex: number) => void
2728
}
2829

2930
const TaskHeader: React.FC<TaskHeaderProps> = ({
@@ -36,6 +37,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
3637
totalCost,
3738
lastApiReqTotalTokens,
3839
onClose,
40+
onScrollToMessage,
3941
}) => {
4042
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage, clineMessages, navigateToSettings } =
4143
useExtensionState()
@@ -469,7 +471,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
469471
</div>
470472
)}
471473
<div className="flex flex-col">
472-
<TaskTimeline messages={clineMessages} />
474+
<TaskTimeline messages={clineMessages} onBlockClick={onScrollToMessage} />
473475
{ContextWindowComponent}
474476
</div>
475477
{checkpointTrackerErrorMessage && (

webview-ui/src/components/chat/TaskTimeline.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const TOOLTIP_MARGIN = 32 // 32px margin on each side
1414

1515
interface TaskTimelineProps {
1616
messages: ClineMessage[]
17+
onBlockClick?: (messageIndex: number) => void
1718
}
1819

1920
const getBlockColor = (message: ClineMessage): string => {
@@ -94,16 +95,19 @@ const getBlockColor = (message: ClineMessage): string => {
9495
return COLOR_WHITE // Default color
9596
}
9697

97-
const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
98+
const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages, onBlockClick }) => {
9899
const containerRef = useRef<HTMLDivElement>(null)
99100
const scrollableRef = useRef<HTMLDivElement>(null)
100101

101-
const taskTimelinePropsMessages = useMemo(() => {
102-
if (messages.length <= 1) return []
102+
const { taskTimelinePropsMessages, messageIndexMap } = useMemo(() => {
103+
if (messages.length <= 1) return { taskTimelinePropsMessages: [], messageIndexMap: [] }
103104

104105
const processed = combineApiRequests(combineCommandSequences(messages.slice(1)))
106+
const indexMap: number[] = []
107+
108+
const filtered = processed.filter((msg, processedIndex) => {
109+
const originalIndex = messages.findIndex((originalMsg, idx) => idx > 0 && originalMsg.ts === msg.ts)
105110

106-
return processed.filter((msg) => {
107111
// Filter out standard "say" events we don't want to show
108112
if (
109113
msg.type === "say" &&
@@ -124,9 +128,13 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
124128
) {
125129
return false
126130
}
131+
if (originalIndex !== -1) {
132+
indexMap.push(originalIndex)
133+
}
127134

128135
return true
129136
})
137+
return { taskTimelinePropsMessages: filtered, messageIndexMap: indexMap }
130138
}, [messages])
131139

132140
useEffect(() => {
@@ -145,9 +153,18 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
145153
const TimelineBlock = useCallback(
146154
(index: number) => {
147155
const message = taskTimelinePropsMessages[index]
156+
const originalMessageIndex = messageIndexMap[index]
157+
158+
const handleClick = () => {
159+
if (onBlockClick && originalMessageIndex !== undefined) {
160+
onBlockClick(originalMessageIndex)
161+
}
162+
}
163+
148164
return (
149165
<TaskTimelineTooltip message={message}>
150166
<div
167+
onClick={handleClick}
151168
style={{
152169
width: BLOCK_WIDTH,
153170
height: "100%",
@@ -160,7 +177,7 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
160177
</TaskTimelineTooltip>
161178
)
162179
},
163-
[taskTimelinePropsMessages],
180+
[taskTimelinePropsMessages, messageIndexMap, onBlockClick],
164181
)
165182

166183
// Scroll to the end when messages change

0 commit comments

Comments
 (0)