Skip to content

Commit a0655a2

Browse files
committed
feat: Implement Jump to Last Checkpoint context menu action
1 parent c3b8597 commit a0655a2

File tree

4 files changed

+220
-62
lines changed

4 files changed

+220
-62
lines changed

package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@
169169
"command": "roo.acceptInput",
170170
"title": "%command.acceptInput.title%",
171171
"category": "%configuration.title%"
172+
},
173+
{
174+
"command": "roo-cline.jumpToLastCheckpoint",
175+
"title": "Last Checkpoint",
176+
"category": "%configuration.title%"
172177
}
173178
],
174179
"menus": {
@@ -275,6 +280,13 @@
275280
"group": "navigation@6",
276281
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
277282
}
283+
],
284+
"webview/context": [
285+
{
286+
"command": "roo-cline.jumpToLastCheckpoint",
287+
"when": "webviewId == 'roo-cline.SidebarProvider' || webviewId == 'roo-cline.TabPanelProvider'",
288+
"group": "navigation"
289+
}
278290
]
279291
},
280292
"configuration": {

src/activate/registerCommands.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import delay from "delay"
44
import { ClineProvider } from "../core/webview/ClineProvider"
55
import { ContextProxy } from "../core/config/ContextProxy"
66
import { telemetryService } from "../services/telemetry/TelemetryService"
7+
import { ExtensionMessage } from "../shared/ExtensionMessage" // Corrected import path and type
78

89
import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRelayResponse } from "./humanRelay"
910
import { handleNewTask } from "./handleTask"
@@ -172,6 +173,21 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
172173

173174
visibleProvider.postMessageToWebview({ type: "acceptInput" })
174175
},
176+
"roo-cline.jumpToLastCheckpoint": async () => {
177+
console.log("[Debug] Native 'roo-cline.jumpToLastCheckpoint' command triggered.")
178+
const visibleProvider = getVisibleProviderOrLog(outputChannel)
179+
if (visibleProvider) {
180+
const message: ExtensionMessage = {
181+
// Corrected type annotation
182+
type: "action",
183+
text: "jumpToCheckpoint", // Use 'text' field for this specific action
184+
}
185+
console.log("[Debug] Sending 'jumpToCheckpoint' message to webview:", message)
186+
await visibleProvider.postMessageToWebview(message)
187+
} else {
188+
console.log("[Debug] No visible ClineProvider found.")
189+
}
190+
},
175191
}
176192
}
177193

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

Lines changed: 80 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import TaskHeader from "./TaskHeader"
4040
import AutoApproveMenu from "./AutoApproveMenu"
4141
import SystemPromptWarning from "./SystemPromptWarning"
4242
import { CheckpointWarning } from "./CheckpointWarning"
43+
import { jumpToLastCheckpoint } from "@src/utils/checkpoint-navigation" // Added import
4344

4445
export interface ChatViewProps {
4546
isHidden: boolean
@@ -537,68 +538,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
537538
const shouldDisableImages =
538539
!model?.supportsImages || textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
539540

540-
const handleMessage = useCallback(
541-
(e: MessageEvent) => {
542-
const message: ExtensionMessage = e.data
543-
544-
switch (message.type) {
545-
case "action":
546-
switch (message.action!) {
547-
case "didBecomeVisible":
548-
if (!isHidden && !textAreaDisabled && !enableButtons) {
549-
textAreaRef.current?.focus()
550-
}
551-
break
552-
case "focusInput":
553-
textAreaRef.current?.focus()
554-
break
555-
}
556-
break
557-
case "selectedImages":
558-
const newImages = message.images ?? []
559-
if (newImages.length > 0) {
560-
setSelectedImages((prevImages) =>
561-
[...prevImages, ...newImages].slice(0, MAX_IMAGES_PER_MESSAGE),
562-
)
563-
}
564-
break
565-
case "invoke":
566-
switch (message.invoke!) {
567-
case "newChat":
568-
handleChatReset()
569-
break
570-
case "sendMessage":
571-
handleSendMessage(message.text ?? "", message.images ?? [])
572-
break
573-
case "setChatBoxMessage":
574-
handleSetChatBoxMessage(message.text ?? "", message.images ?? [])
575-
break
576-
case "primaryButtonClick":
577-
handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
578-
break
579-
case "secondaryButtonClick":
580-
handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
581-
break
582-
}
583-
}
584-
// textAreaRef.current is not explicitly required here since React
585-
// guarantees that ref will be stable across re-renders, and we're
586-
// not using its value but its reference.
587-
},
588-
[
589-
isHidden,
590-
textAreaDisabled,
591-
enableButtons,
592-
handleChatReset,
593-
handleSendMessage,
594-
handleSetChatBoxMessage,
595-
handlePrimaryButtonClick,
596-
handleSecondaryButtonClick,
597-
],
598-
)
599-
600-
useEvent("message", handleMessage)
601-
602541
// NOTE: the VSCode window needs to be focused for this to work.
603542
useMount(() => textAreaRef.current?.focus())
604543

@@ -930,6 +869,85 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
930869
return result
931870
}, [visibleMessages])
932871

872+
// Define handleMessage *after* groupedMessages and other dependencies
873+
const handleMessage = useCallback(
874+
(e: MessageEvent) => {
875+
const message: ExtensionMessage = e.data
876+
877+
switch (message.type) {
878+
case "action":
879+
// Handle jumpToCheckpoint based on message.text FIRST
880+
if (message.text === "jumpToCheckpoint") {
881+
console.log("[Webview Frontend] Received jumpToCheckpoint message")
882+
// Ensure 'groupedMessages' (or the equivalent variable holding the message list) is used
883+
jumpToLastCheckpoint(virtuosoRef, groupedMessages, () => {
884+
console.log("[Webview Frontend] Checkpoint navigation complete")
885+
// Optional: Re-enable auto-scrolling if it was disabled
886+
// disableAutoScrollRef.current = false;
887+
})
888+
} else {
889+
// If not jumpToCheckpoint, THEN handle other actions based on message.action
890+
// Handle regular action messages that use the action field
891+
switch (message.action!) {
892+
case "didBecomeVisible":
893+
if (!isHidden && !textAreaDisabled && !enableButtons) {
894+
textAreaRef.current?.focus()
895+
}
896+
break
897+
case "focusInput":
898+
textAreaRef.current?.focus()
899+
break
900+
// ... other action cases ...
901+
}
902+
}
903+
break // Keep this break for the outer "action" case
904+
case "selectedImages":
905+
const newImages = message.images ?? []
906+
if (newImages.length > 0) {
907+
setSelectedImages((prevImages) =>
908+
[...prevImages, ...newImages].slice(0, MAX_IMAGES_PER_MESSAGE),
909+
)
910+
}
911+
break
912+
case "invoke":
913+
switch (message.invoke!) {
914+
case "newChat":
915+
handleChatReset()
916+
break
917+
case "sendMessage":
918+
handleSendMessage(message.text ?? "", message.images ?? [])
919+
break
920+
case "setChatBoxMessage":
921+
handleSetChatBoxMessage(message.text ?? "", message.images ?? [])
922+
break
923+
case "primaryButtonClick":
924+
handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
925+
break
926+
case "secondaryButtonClick":
927+
handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
928+
break
929+
}
930+
break // Keep this break
931+
// Add other message types if needed
932+
}
933+
},
934+
[
935+
isHidden,
936+
textAreaDisabled,
937+
enableButtons,
938+
groupedMessages, // Dependency is correctly placed now
939+
handleChatReset,
940+
handleSendMessage,
941+
handleSetChatBoxMessage,
942+
handlePrimaryButtonClick,
943+
handleSecondaryButtonClick,
944+
// virtuosoRef is stable, no need to add
945+
],
946+
)
947+
948+
// Register the message handler
949+
useEvent("message", handleMessage)
950+
933951
// scrolling
934952

935953
const scrollToBottomSmooth = useMemo(
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { ClineMessage } from "@roo/shared/ExtensionMessage"
2+
import React from "react" // Added missing React import for RefObject
3+
4+
// --- findPreviousCheckpointIndex function (existing, correct) ---
5+
export function findPreviousCheckpointIndex(
6+
messages: (ClineMessage | ClineMessage[])[],
7+
currentCheckpointTs: number,
8+
): number {
9+
// ... (implementation as previously read) ...
10+
console.log("Finding previous checkpoint for ts:", currentCheckpointTs)
11+
const currentIndex = messages.findIndex(
12+
(item) => !Array.isArray(item) && item.say === "checkpoint_saved" && item.ts === currentCheckpointTs,
13+
)
14+
console.log("Current checkpoint index:", currentIndex)
15+
if (currentIndex <= 0) {
16+
console.log("No previous checkpoint found (current is first or not found)")
17+
return -1
18+
}
19+
for (let i = currentIndex - 1; i >= 0; i--) {
20+
const item = messages[i]
21+
if (!Array.isArray(item) && item.say === "checkpoint_saved") {
22+
console.log("Found previous checkpoint at index:", i)
23+
return i
24+
}
25+
}
26+
console.log("No previous checkpoint found after searching")
27+
return -1
28+
}
29+
30+
// --- findLastCheckpointIndex function (existing, correct) ---
31+
export function findLastCheckpointIndex(messages: (ClineMessage | ClineMessage[])[]): number {
32+
// ... (implementation as previously read) ...
33+
console.log("Finding last checkpoint in message list")
34+
for (let i = messages.length - 1; i >= 0; i--) {
35+
const item = messages[i]
36+
if (!Array.isArray(item) && item.say === "checkpoint_saved") {
37+
console.log("Found last checkpoint at index:", i)
38+
return i
39+
}
40+
}
41+
console.log("No checkpoint found in message list")
42+
return -1
43+
}
44+
45+
// --- jumpToLastCheckpoint function (existing, correct) ---
46+
export function jumpToLastCheckpoint(
47+
virtuosoRef: React.RefObject<any>,
48+
messages: (ClineMessage | ClineMessage[])[],
49+
onScrollComplete?: () => void,
50+
): boolean {
51+
// ... (implementation as previously read) ...
52+
console.log("Jump to last checkpoint requested")
53+
const lastCheckpointIndex = findLastCheckpointIndex(messages)
54+
if (lastCheckpointIndex !== -1) {
55+
jumpToCheckpoint(virtuosoRef, lastCheckpointIndex, onScrollComplete)
56+
return true
57+
}
58+
console.log("No checkpoint found to jump to")
59+
return false
60+
}
61+
62+
// --- jumpToCheckpoint function (existing, correct) ---
63+
export function jumpToCheckpoint(
64+
virtuosoRef: React.RefObject<any>,
65+
index: number,
66+
onScrollComplete?: () => void,
67+
): void {
68+
// ... (implementation as previously read) ...
69+
console.log("Jump to checkpoint called with index:", index)
70+
if (index !== -1 && virtuosoRef.current) {
71+
console.log("Virtuoso ref exists, attempting to scroll to index:", index)
72+
virtuosoRef.current.scrollToIndex({
73+
index: index,
74+
align: "start",
75+
behavior: "smooth",
76+
})
77+
console.log("Scroll to index command sent to Virtuoso")
78+
if (onScrollComplete) {
79+
setTimeout(onScrollComplete, 500) // Allow time for smooth scroll
80+
}
81+
} else {
82+
console.log("Cannot jump: index is -1 or virtuosoRef is null", {
83+
index,
84+
virtuosoRefExists: !!virtuosoRef.current,
85+
})
86+
}
87+
}
88+
89+
// --- jumpToPreviousCheckpoint function (existing, correct, optional for this feature) ---
90+
export function jumpToPreviousCheckpoint(
91+
virtuosoRef: React.RefObject<any>,
92+
messages: (ClineMessage | ClineMessage[])[],
93+
currentCheckpointTs?: number,
94+
onScrollComplete?: () => void,
95+
): boolean {
96+
// ... (implementation as previously read) ...
97+
console.log("Jump to previous checkpoint requested")
98+
let targetIndex: number
99+
if (currentCheckpointTs) {
100+
console.log("Current checkpoint timestamp:", currentCheckpointTs)
101+
targetIndex = findPreviousCheckpointIndex(messages, currentCheckpointTs)
102+
} else {
103+
console.log("No current checkpoint, finding last checkpoint")
104+
targetIndex = findLastCheckpointIndex(messages)
105+
}
106+
if (targetIndex !== -1) {
107+
jumpToCheckpoint(virtuosoRef, targetIndex, onScrollComplete)
108+
return true
109+
}
110+
console.log("No previous checkpoint found to jump to")
111+
return false
112+
}

0 commit comments

Comments
 (0)