diff --git a/package.json b/package.json index b73e3610ea..5a79dcf657 100644 --- a/package.json +++ b/package.json @@ -169,6 +169,11 @@ "command": "roo.acceptInput", "title": "%command.acceptInput.title%", "category": "%configuration.title%" + }, + { + "command": "roo-cline.jumpToLastCheckpoint", + "title": "Last Checkpoint", + "category": "%configuration.title%" } ], "menus": { @@ -275,6 +280,13 @@ "group": "navigation@6", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" } + ], + "webview/context": [ + { + "command": "roo-cline.jumpToLastCheckpoint", + "when": "webviewId == 'roo-cline.SidebarProvider' || webviewId == 'roo-cline.TabPanelProvider'", + "group": "navigation" + } ] }, "configuration": { diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index c1712a8041..63ba01caf5 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -4,6 +4,7 @@ import delay from "delay" import { ClineProvider } from "../core/webview/ClineProvider" import { ContextProxy } from "../core/config/ContextProxy" import { telemetryService } from "../services/telemetry/TelemetryService" +import { ExtensionMessage } from "../shared/ExtensionMessage" // Corrected import path and type import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRelayResponse } from "./humanRelay" import { handleNewTask } from "./handleTask" @@ -172,6 +173,21 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt visibleProvider.postMessageToWebview({ type: "acceptInput" }) }, + "roo-cline.jumpToLastCheckpoint": async () => { + console.log("[Debug] Native 'roo-cline.jumpToLastCheckpoint' command triggered.") + const visibleProvider = getVisibleProviderOrLog(outputChannel) + if (visibleProvider) { + const message: ExtensionMessage = { + // Corrected type annotation + type: "action", + text: "jumpToCheckpoint", // Use 'text' field for this specific action + } + console.log("[Debug] Sending 'jumpToCheckpoint' message to webview:", message) + await visibleProvider.postMessageToWebview(message) + } else { + console.log("[Debug] No visible ClineProvider found.") + } + }, } } diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index c2fb21353b..1b8ab019c9 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -40,6 +40,7 @@ import TaskHeader from "./TaskHeader" import AutoApproveMenu from "./AutoApproveMenu" import SystemPromptWarning from "./SystemPromptWarning" import { CheckpointWarning } from "./CheckpointWarning" +import { jumpToLastCheckpoint } from "@src/utils/checkpoint-navigation" // Added import export interface ChatViewProps { isHidden: boolean @@ -537,68 +538,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction= MAX_IMAGES_PER_MESSAGE - const handleMessage = useCallback( - (e: MessageEvent) => { - const message: ExtensionMessage = e.data - - switch (message.type) { - case "action": - switch (message.action!) { - case "didBecomeVisible": - if (!isHidden && !textAreaDisabled && !enableButtons) { - textAreaRef.current?.focus() - } - break - case "focusInput": - textAreaRef.current?.focus() - break - } - break - case "selectedImages": - const newImages = message.images ?? [] - if (newImages.length > 0) { - setSelectedImages((prevImages) => - [...prevImages, ...newImages].slice(0, MAX_IMAGES_PER_MESSAGE), - ) - } - break - case "invoke": - switch (message.invoke!) { - case "newChat": - handleChatReset() - break - case "sendMessage": - handleSendMessage(message.text ?? "", message.images ?? []) - break - case "setChatBoxMessage": - handleSetChatBoxMessage(message.text ?? "", message.images ?? []) - break - case "primaryButtonClick": - handlePrimaryButtonClick(message.text ?? "", message.images ?? []) - break - case "secondaryButtonClick": - handleSecondaryButtonClick(message.text ?? "", message.images ?? []) - break - } - } - // textAreaRef.current is not explicitly required here since React - // guarantees that ref will be stable across re-renders, and we're - // not using its value but its reference. - }, - [ - isHidden, - textAreaDisabled, - enableButtons, - handleChatReset, - handleSendMessage, - handleSetChatBoxMessage, - handlePrimaryButtonClick, - handleSecondaryButtonClick, - ], - ) - - useEvent("message", handleMessage) - // NOTE: the VSCode window needs to be focused for this to work. useMount(() => textAreaRef.current?.focus()) @@ -930,6 +869,85 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + const message: ExtensionMessage = e.data + + switch (message.type) { + case "action": + // Handle jumpToCheckpoint based on message.text FIRST + if (message.text === "jumpToCheckpoint") { + console.log("[Webview Frontend] Received jumpToCheckpoint message") + // Ensure 'groupedMessages' (or the equivalent variable holding the message list) is used + jumpToLastCheckpoint(virtuosoRef, groupedMessages, () => { + console.log("[Webview Frontend] Checkpoint navigation complete") + // Optional: Re-enable auto-scrolling if it was disabled + // disableAutoScrollRef.current = false; + }) + } else { + // If not jumpToCheckpoint, THEN handle other actions based on message.action + // Handle regular action messages that use the action field + switch (message.action!) { + case "didBecomeVisible": + if (!isHidden && !textAreaDisabled && !enableButtons) { + textAreaRef.current?.focus() + } + break + case "focusInput": + textAreaRef.current?.focus() + break + // ... other action cases ... + } + } + break // Keep this break for the outer "action" case + case "selectedImages": + const newImages = message.images ?? [] + if (newImages.length > 0) { + setSelectedImages((prevImages) => + [...prevImages, ...newImages].slice(0, MAX_IMAGES_PER_MESSAGE), + ) + } + break + case "invoke": + switch (message.invoke!) { + case "newChat": + handleChatReset() + break + case "sendMessage": + handleSendMessage(message.text ?? "", message.images ?? []) + break + case "setChatBoxMessage": + handleSetChatBoxMessage(message.text ?? "", message.images ?? []) + break + case "primaryButtonClick": + handlePrimaryButtonClick(message.text ?? "", message.images ?? []) + break + case "secondaryButtonClick": + handleSecondaryButtonClick(message.text ?? "", message.images ?? []) + break + } + break // Keep this break + // Add other message types if needed + } + }, + [ + isHidden, + textAreaDisabled, + enableButtons, + groupedMessages, // Dependency is correctly placed now + handleChatReset, + handleSendMessage, + handleSetChatBoxMessage, + handlePrimaryButtonClick, + handleSecondaryButtonClick, + // virtuosoRef is stable, no need to add + ], + ) + + // Register the message handler + useEvent("message", handleMessage) + // scrolling const scrollToBottomSmooth = useMemo( diff --git a/webview-ui/src/utils/checkpoint-navigation.ts b/webview-ui/src/utils/checkpoint-navigation.ts new file mode 100644 index 0000000000..875e5f2b74 --- /dev/null +++ b/webview-ui/src/utils/checkpoint-navigation.ts @@ -0,0 +1,112 @@ +import { ClineMessage } from "@roo/shared/ExtensionMessage" +import React from "react" // Added missing React import for RefObject + +// --- findPreviousCheckpointIndex function (existing, correct) --- +export function findPreviousCheckpointIndex( + messages: (ClineMessage | ClineMessage[])[], + currentCheckpointTs: number, +): number { + // ... (implementation as previously read) ... + console.log("Finding previous checkpoint for ts:", currentCheckpointTs) + const currentIndex = messages.findIndex( + (item) => !Array.isArray(item) && item.say === "checkpoint_saved" && item.ts === currentCheckpointTs, + ) + console.log("Current checkpoint index:", currentIndex) + if (currentIndex <= 0) { + console.log("No previous checkpoint found (current is first or not found)") + return -1 + } + for (let i = currentIndex - 1; i >= 0; i--) { + const item = messages[i] + if (!Array.isArray(item) && item.say === "checkpoint_saved") { + console.log("Found previous checkpoint at index:", i) + return i + } + } + console.log("No previous checkpoint found after searching") + return -1 +} + +// --- findLastCheckpointIndex function (existing, correct) --- +export function findLastCheckpointIndex(messages: (ClineMessage | ClineMessage[])[]): number { + // ... (implementation as previously read) ... + console.log("Finding last checkpoint in message list") + for (let i = messages.length - 1; i >= 0; i--) { + const item = messages[i] + if (!Array.isArray(item) && item.say === "checkpoint_saved") { + console.log("Found last checkpoint at index:", i) + return i + } + } + console.log("No checkpoint found in message list") + return -1 +} + +// --- jumpToLastCheckpoint function (existing, correct) --- +export function jumpToLastCheckpoint( + virtuosoRef: React.RefObject, + messages: (ClineMessage | ClineMessage[])[], + onScrollComplete?: () => void, +): boolean { + // ... (implementation as previously read) ... + console.log("Jump to last checkpoint requested") + const lastCheckpointIndex = findLastCheckpointIndex(messages) + if (lastCheckpointIndex !== -1) { + jumpToCheckpoint(virtuosoRef, lastCheckpointIndex, onScrollComplete) + return true + } + console.log("No checkpoint found to jump to") + return false +} + +// --- jumpToCheckpoint function (existing, correct) --- +export function jumpToCheckpoint( + virtuosoRef: React.RefObject, + index: number, + onScrollComplete?: () => void, +): void { + // ... (implementation as previously read) ... + console.log("Jump to checkpoint called with index:", index) + if (index !== -1 && virtuosoRef.current) { + console.log("Virtuoso ref exists, attempting to scroll to index:", index) + virtuosoRef.current.scrollToIndex({ + index: index, + align: "start", + behavior: "smooth", + }) + console.log("Scroll to index command sent to Virtuoso") + if (onScrollComplete) { + setTimeout(onScrollComplete, 500) // Allow time for smooth scroll + } + } else { + console.log("Cannot jump: index is -1 or virtuosoRef is null", { + index, + virtuosoRefExists: !!virtuosoRef.current, + }) + } +} + +// --- jumpToPreviousCheckpoint function (existing, correct, optional for this feature) --- +export function jumpToPreviousCheckpoint( + virtuosoRef: React.RefObject, + messages: (ClineMessage | ClineMessage[])[], + currentCheckpointTs?: number, + onScrollComplete?: () => void, +): boolean { + // ... (implementation as previously read) ... + console.log("Jump to previous checkpoint requested") + let targetIndex: number + if (currentCheckpointTs) { + console.log("Current checkpoint timestamp:", currentCheckpointTs) + targetIndex = findPreviousCheckpointIndex(messages, currentCheckpointTs) + } else { + console.log("No current checkpoint, finding last checkpoint") + targetIndex = findLastCheckpointIndex(messages) + } + if (targetIndex !== -1) { + jumpToCheckpoint(virtuosoRef, targetIndex, onScrollComplete) + return true + } + console.log("No previous checkpoint found to jump to") + return false +}