Skip to content
Closed
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
12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@
"command": "roo.acceptInput",
"title": "%command.acceptInput.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.jumpToLastCheckpoint",
"title": "Last Checkpoint",
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider internationalizing the command title for 'jumpToLastCheckpoint' (e.g., using a placeholder similar to other commands) to ensure consistency with other user-facing texts.

Suggested change
"title": "Last Checkpoint",
"title": "%command.jumpToLastCheckpoint.title%",

This comment was generated because it violated a code review rule: mrule_JLtLS6tLppVEv8iV.

"category": "%configuration.title%"
}
],
"menus": {
Expand Down Expand Up @@ -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": {
Expand Down
16 changes: 16 additions & 0 deletions src/activate/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.")
}
},
}
}

Expand Down
142 changes: 80 additions & 62 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -537,68 +538,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const shouldDisableImages =
!model?.supportsImages || textAreaDisabled || selectedImages.length >= 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())

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

// Define handleMessage *after* groupedMessages and other dependencies
const handleMessage = useCallback(
(e: MessageEvent) => {
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(
Expand Down
112 changes: 112 additions & 0 deletions webview-ui/src/utils/checkpoint-navigation.ts
Original file line number Diff line number Diff line change
@@ -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<any>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using a more specific type rather than React.RefObject<any> for the virtuosoRef parameter to improve type safety. Additionally, review the debug console.log statements; they are useful for development but might be removed or replaced with a proper logging mechanism in production.

This comment was generated because it violated a code review rule: mrule_QkEwsCio7v34DaCF.

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<any>,
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<any>,
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
}