Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
Open
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
9 changes: 9 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface ExtensionMessage {
| "deleteCustomModeCheck"
| "currentCheckpointUpdated"
| "checkpointInitWarning"
| "initialCheckpointState"
| "browserToolEnabled"
| "browserConnectionResult"
| "remoteBrowserEnabled"
Expand Down Expand Up @@ -116,6 +117,9 @@ export interface ExtensionMessage {
type: "WAIT_TIMEOUT" | "INIT_TIMEOUT"
timeout: number
}
// Initial checkpoint state for state-driven UI
initialCheckpointState?: "pending" | "ready" | "failed" | null
initialCheckpointHash?: string | null
action?:
| "chatButtonClicked"
| "settingsButtonClicked"
Expand Down Expand Up @@ -408,6 +412,10 @@ export type ExtensionState = Pick<
featureRoomoteControlEnabled: boolean
openAiCodexIsAuthenticated?: boolean
debug?: boolean

// Initial checkpoint state for state-driven UI
initialCheckpointState?: "pending" | "ready" | "failed" | null
initialCheckpointHash?: string | null
}

export interface Command {
Expand Down Expand Up @@ -729,6 +737,7 @@ export const checkoutRestorePayloadSchema = z.object({
ts: z.number(),
commitHash: z.string(),
mode: z.enum(["preview", "restore"]),
isInitial: z.boolean().optional(),
})

export type CheckpointRestorePayload = z.infer<typeof checkoutRestorePayloadSchema>
Expand Down
65 changes: 64 additions & 1 deletion src/core/checkpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ function sendCheckpointInitWarn(task: Task, type?: "WAIT_TIMEOUT" | "INIT_TIMEOU
})
}

function sendInitialCheckpointState(task: Task, state: "pending" | "ready" | "failed" | null, hash?: string) {
task.providerRef.deref()?.postMessageToWebview({
type: "initialCheckpointState",
initialCheckpointState: state,
initialCheckpointHash: hash ?? null,
})
}

export async function getCheckpointService(task: Task, { interval = 250 }: { interval?: number } = {}) {
if (!task.enableCheckpoints) {
return undefined
Expand Down Expand Up @@ -98,6 +106,7 @@ export async function getCheckpointService(task: Task, { interval = 250 }: { int
)
if (!task?.checkpointService) {
sendCheckpointInitWarn(task, "INIT_TIMEOUT", task.checkpointTimeout)
sendInitialCheckpointState(task, "failed")
task.enableCheckpoints = false
return undefined
} else {
Expand All @@ -121,6 +130,7 @@ export async function getCheckpointService(task: Task, { interval = 250 }: { int
} catch (err) {
if (err.name === "TimeoutError" && task.enableCheckpoints) {
sendCheckpointInitWarn(task, "INIT_TIMEOUT", task.checkpointTimeout)
sendInitialCheckpointState(task, "failed")
}
log(`[Task#getCheckpointService] ${err.message}`)
task.enableCheckpoints = false
Expand All @@ -140,6 +150,7 @@ async function checkGitInstallation(

if (!gitInstalled) {
log("[Task#getCheckpointService] Git is not installed, disabling checkpoints")
sendInitialCheckpointState(task, "failed")
task.enableCheckpoints = false
task.checkpointServiceInitializing = false

Expand All @@ -157,9 +168,18 @@ async function checkGitInstallation(
}

// Git is installed, proceed with initialization
service.on("initialize", () => {
service.on("initialize", ({ baseHash }) => {
log("[Task#getCheckpointService] service initialized")
task.checkpointServiceInitializing = false

// Send initial checkpoint state as ready with the hash
sendInitialCheckpointState(task, "ready", baseHash)

// Update webview with initial checkpoint hash (for currentCheckpoint tracking)
provider?.postMessageToWebview({
type: "currentCheckpointUpdated",
text: baseHash,
})
})

service.on("checkpoint", ({ fromHash: from, toHash: to, suppressMessage }) => {
Expand Down Expand Up @@ -195,15 +215,20 @@ async function checkGitInstallation(

log("[Task#getCheckpointService] initializing shadow git")

// Set pending state before starting initialization
sendInitialCheckpointState(task, "pending")

try {
await service.initShadowGit()
} catch (err) {
log(`[Task#getCheckpointService] initShadowGit -> ${err.message}`)
sendInitialCheckpointState(task, "failed")
task.enableCheckpoints = false
}
} catch (err) {
log(`[Task#getCheckpointService] Unexpected error during Git check: ${err.message}`)
console.error("Git check error:", err)
sendInitialCheckpointState(task, "failed")
task.enableCheckpoints = false
task.checkpointServiceInitializing = false
}
Expand Down Expand Up @@ -301,6 +326,44 @@ export async function checkpointRestore(
}
}

/**
* Restore to the initial checkpoint (base state when task started).
* This is used by the InitialCheckpoint component since it doesn't have a message timestamp.
*/
export async function checkpointRestoreToBase(task: Task): Promise<boolean> {
const service = await getCheckpointService(task)

if (!service) {
return false
}

const baseHash = service.baseHash

if (!baseHash) {
const provider = task.providerRef.deref()
provider?.log("[checkpointRestoreToBase] no baseHash available")
return false
}

const provider = task.providerRef.deref()

try {
await service.restoreCheckpoint(baseHash)
TelemetryService.instance.captureCheckpointRestored(task.taskId)
await provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: baseHash })

// Cancel the task to reinitialize with the restored state
// This follows the same pattern as checkpointRestore
provider?.cancelTask()

return true
} catch (err) {
provider?.log("[checkpointRestoreToBase] disabling checkpoints for this task")
task.enableCheckpoints = false
return false
}
}

export type CheckpointDiffOptions = {
ts?: number
previousCommitHash?: string
Expand Down
13 changes: 12 additions & 1 deletion src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1200,7 +1200,18 @@ export const webviewMessageHandler = async (
}

try {
await provider.getCurrentTask()?.checkpointRestore(result.data)
// For initial checkpoint, use checkpointRestoreToBase instead of checkpointRestore
// because there's no message with ts to find in clineMessages
if (result.data.isInitial) {
const { checkpointRestoreToBase } = await import("../checkpoints")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In the initial-checkpoint path, this uses a dynamic import from "../checkpoints", but the only checkpoints module here appears to be src/core/checkpoints/index.ts (and it does not export checkpointRestoreToBase). As written, the restore action will throw at runtime (module or export not found) and the initial restore will fail.

Fix it with Roo Code or mention @roomote and request a fix.

const success = await checkpointRestoreToBase(provider.getCurrentTask()!)

if (!success) {
vscode.window.showErrorMessage(t("common:errors.checkpoint_restore_base_failed"))
}
} else {
await provider.getCurrentTask()?.checkpointRestore(result.data)
}
} catch (error) {
vscode.window.showErrorMessage(t("common:errors.checkpoint_failed"))
}
Expand Down
11 changes: 11 additions & 0 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import TaskHeader from "./TaskHeader"
import SystemPromptWarning from "./SystemPromptWarning"
import ProfileViolationWarning from "./ProfileViolationWarning"
import { CheckpointWarning } from "./CheckpointWarning"
import { InitialCheckpoint } from "./checkpoints/InitialCheckpoint"
import { QueuedMessages } from "./QueuedMessages"
import { WorktreeSelector } from "./WorktreeSelector"
import DismissibleUpsell from "../common/DismissibleUpsell"
Expand Down Expand Up @@ -97,6 +98,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
messageQueue = [],
isBrowserSessionActive,
showWorktreesInHomeScreen,
initialCheckpointState,
initialCheckpointHash,
enableCheckpoints,
} = useExtensionState()

const messagesRef = useRef(messages)
Expand Down Expand Up @@ -1510,6 +1514,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
<CheckpointWarning warning={checkpointWarning} />
</div>
)}

{/* Initial Checkpoint UI - state-driven element */}
{initialCheckpointState && enableCheckpoints && (
<div className="px-3">
<InitialCheckpoint state={initialCheckpointState} hash={initialCheckpointHash} />
</div>
)}
</>
) : (
<div className="flex flex-col h-full justify-center p-6 min-h-0 overflow-y-auto gap-4 relative">
Expand Down
51 changes: 32 additions & 19 deletions webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type CheckpointMenuBaseProps = {
ts: number
commitHash: string
checkpoint: Checkpoint
isInitial?: boolean
}
type CheckpointMenuControlledProps = {
onOpenChange: (open: boolean) => void
Expand All @@ -21,7 +22,7 @@ type CheckpointMenuUncontrolledProps = {
}
type CheckpointMenuProps = CheckpointMenuBaseProps & (CheckpointMenuControlledProps | CheckpointMenuUncontrolledProps)

export const CheckpointMenu = ({ ts, commitHash, checkpoint, onOpenChange }: CheckpointMenuProps) => {
export const CheckpointMenu = ({ ts, commitHash, checkpoint, isInitial, onOpenChange }: CheckpointMenuProps) => {
const { t } = useTranslation()
const [internalRestoreOpen, setInternalRestoreOpen] = useState(false)
const [restoreConfirming, setRestoreConfirming] = useState(false)
Expand Down Expand Up @@ -74,14 +75,20 @@ export const CheckpointMenu = ({ ts, commitHash, checkpoint, onOpenChange }: Che
}, [ts, commitHash])

const onPreview = useCallback(() => {
vscode.postMessage({ type: "checkpointRestore", payload: { ts, commitHash, mode: "preview" } })
vscode.postMessage({
type: "checkpointRestore",
payload: { ts, commitHash, mode: "preview", ...(isInitial && { isInitial: true }) },
})
setRestoreOpen(false)
}, [ts, commitHash, setRestoreOpen])
}, [ts, commitHash, isInitial, setRestoreOpen])

const onRestore = useCallback(() => {
vscode.postMessage({ type: "checkpointRestore", payload: { ts, commitHash, mode: "restore" } })
vscode.postMessage({
type: "checkpointRestore",
payload: { ts, commitHash, mode: "restore", ...(isInitial && { isInitial: true }) },
})
setRestoreOpen(false)
}, [ts, commitHash, setRestoreOpen])
}, [ts, commitHash, isInitial, setRestoreOpen])

const handleOpenChange = useCallback(
(open: boolean) => {
Expand All @@ -95,11 +102,14 @@ export const CheckpointMenu = ({ ts, commitHash, checkpoint, onOpenChange }: Che

return (
<div className="flex flex-row gap-1">
<StandardTooltip content={t("chat:checkpoint.menu.viewDiff")}>
<Button variant="ghost" size="icon" onClick={onCheckpointDiff}>
<span className="codicon codicon-diff-single" />
</Button>
</StandardTooltip>
{/* Hide "View Diff" for initial checkpoint - no previous checkpoint to diff against */}
{!isInitial && (
<StandardTooltip content={t("chat:checkpoint.menu.viewDiff")}>
<Button variant="ghost" size="icon" onClick={onCheckpointDiff}>
<span className="codicon codicon-diff-single" />
</Button>
</StandardTooltip>
)}
<Popover
open={restoreOpen}
onOpenChange={(open) => {
Expand Down Expand Up @@ -175,15 +185,18 @@ export const CheckpointMenu = ({ ts, commitHash, checkpoint, onOpenChange }: Che
</StandardTooltip>
<PopoverContent align="end" container={portalContainer} className="w-auto min-w-max">
<div className="flex flex-col gap-2">
<Button
variant="secondary"
onClick={() => {
onDiffFromInit()
setMoreOpen(false)
}}>
<span className="codicon codicon-versions mr-2" />
{t("chat:checkpoint.menu.viewDiffFromInit")}
</Button>
{/* Hide "View All Changes" for initial checkpoint - already at init */}
{!isInitial && (
<Button
variant="secondary"
onClick={() => {
onDiffFromInit()
setMoreOpen(false)
}}>
<span className="codicon codicon-versions mr-2" />
{t("chat:checkpoint.menu.viewDiffFromInit")}
</Button>
)}
<Button
variant="secondary"
onClick={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin
onMouseLeave={handleMouseLeave}>
<div className="flex items-center gap-2 text-blue-400 whitespace-nowrap">
<GitCommitVertical className="w-4" />
<span className="font-semibold">{t("chat:checkpoint.regular")}</span>
<span className="font-semibold">
{metadata.isInitial ? t("chat:checkpoint.initial") : t("chat:checkpoint.regular")}
</span>
{isCurrent && <span className="text-muted">({t("chat:checkpoint.current")})</span>}
</div>
<span
Expand All @@ -99,6 +101,7 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin
ts={props.ts}
commitHash={props.commitHash}
checkpoint={metadata}
isInitial={metadata.isInitial ?? false}
onOpenChange={handlePopoverOpenChange}
/>
</div>
Expand Down
Loading
Loading