Skip to content

Commit 92268b9

Browse files
committed
Implement workspace checkpoints to save and restore file states manually or automatically before applying AI-generated changes
1 parent 01cc5dc commit 92268b9

24 files changed

Lines changed: 2427 additions & 6 deletions

‎apps/editor/package.json‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,17 @@
285285
"category": "Code Web Chat",
286286
"icon": "$(search)"
287287
},
288+
{
289+
"command": "codeWebChat.checkpoints",
290+
"title": "%codeWebChat.checkpoints.title%",
291+
"category": "Code Web Chat",
292+
"icon": "$(history)"
293+
},
294+
{
295+
"command": "codeWebChat.createNewCheckpoint",
296+
"title": "%codeWebChat.createNewCheckpoint.title%",
297+
"category": "Code Web Chat"
298+
},
288299
{
289300
"command": "codeWebChat.settings",
290301
"title": "%codeWebChat.settings.title%",

‎apps/editor/src/commands/apply-chat-response-command/apply-chat-response-command.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import * as vscode from 'vscode'
22
import * as fs from 'fs'
3+
import {
4+
create_checkpoint,
5+
delete_checkpoint
6+
} from '../checkpoints-command/actions'
37
import { FileInPreview } from '@shared/types/file-in-preview'
8+
import { get_checkpoint_path } from '../checkpoints-command/utils'
49
import { PanelProvider } from '@/views/panel/backend/panel-provider'
510
import { dictionary } from '@shared/constants/dictionary'
611
import { WorkspaceProvider } from '@/context/providers/workspace/workspace-provider'
@@ -15,6 +20,7 @@ import {
1520
process_chat_response,
1621
ApplyChatResponseCommandArgs
1722
} from './response-processor'
23+
import { Checkpoint } from '../checkpoints-command/types'
1824
import { CHECKPOINTS_STATE_KEY } from '@/constants/state-keys'
1925
import { ResponseHistoryItem } from '@shared/types/response-history-item'
2026
import { ApiManager } from '@/services/api-manager'
@@ -24,12 +30,6 @@ import {
2430
CwcPreviewProvider
2531
} from './utils/preview/virtual-document-provider'
2632
import { parse_response } from './utils/clipboard-parser'
27-
import { Checkpoint } from '@/features/checkpoints/types'
28-
import {
29-
create_checkpoint,
30-
delete_checkpoint
31-
} from '@/features/checkpoints/actions'
32-
import { get_checkpoint_path } from '@/features/checkpoints/utils'
3333

3434
let in_progress = false
3535

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as vscode from 'vscode'
2+
import {
3+
CHECKPOINTS_STATE_KEY,
4+
TEMPORARY_CHECKPOINT_STATE_KEY
5+
} from '../../../constants/state-keys'
6+
import { get_checkpoint_path } from '../utils'
7+
import { get_checkpoints } from './get-checkpoints'
8+
import { Logger } from '@shared/utils/logger'
9+
import { PanelProvider } from '@/views/panel/backend/panel-provider'
10+
import type { Checkpoint } from '../types'
11+
12+
export const clear_all_checkpoints = async (
13+
context: vscode.ExtensionContext,
14+
panel_provider: PanelProvider
15+
) => {
16+
const clear_task = async () => {
17+
const checkpoints = await get_checkpoints(context)
18+
for (const checkpoint of checkpoints) {
19+
try {
20+
const checkpoint_path = get_checkpoint_path(checkpoint.timestamp)
21+
await vscode.workspace.fs.delete(vscode.Uri.file(checkpoint_path), {
22+
recursive: true
23+
})
24+
} catch (error) {
25+
Logger.warn({
26+
function_name: 'clear_all_checkpoints',
27+
message: 'Could not delete checkpoint file',
28+
data: error
29+
})
30+
}
31+
}
32+
const temp_checkpoint = context.workspaceState.get<Checkpoint>(
33+
TEMPORARY_CHECKPOINT_STATE_KEY
34+
)
35+
if (temp_checkpoint) {
36+
try {
37+
const checkpoint_path = get_checkpoint_path(temp_checkpoint.timestamp)
38+
await vscode.workspace.fs.delete(vscode.Uri.file(checkpoint_path), {
39+
recursive: true
40+
})
41+
} catch (error) {
42+
Logger.warn({
43+
function_name: 'clear_all_checkpoints',
44+
message: 'Could not delete temporary checkpoint file',
45+
data: error
46+
})
47+
}
48+
}
49+
await context.workspaceState.update(
50+
TEMPORARY_CHECKPOINT_STATE_KEY,
51+
undefined
52+
)
53+
await context.workspaceState.update(CHECKPOINTS_STATE_KEY, [])
54+
await panel_provider.send_checkpoints()
55+
}
56+
await vscode.window.withProgress(
57+
{
58+
location: vscode.ProgressLocation.Notification,
59+
title: 'Clearing all checkpoints...',
60+
cancellable: false
61+
},
62+
clear_task
63+
)
64+
}
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import * as vscode from 'vscode'
2+
import {
3+
CHECKPOINTS_STATE_KEY,
4+
TEMPORARY_CHECKPOINT_STATE_KEY,
5+
CHECKPOINT_OPERATION_IN_PROGRESS_STATE_KEY
6+
} from '../../../constants/state-keys'
7+
import { WorkspaceProvider } from '../../../context/providers/workspace/workspace-provider'
8+
import type { Checkpoint, CheckpointTab, CheckpointTrigger } from '../types'
9+
import { get_checkpoints } from './get-checkpoints'
10+
import {
11+
copy_optimised_recursively,
12+
get_checkpoint_path,
13+
get_incremented_description
14+
} from '../utils'
15+
import {
16+
get_git_diff,
17+
get_git_info,
18+
is_git_repository
19+
} from '../utils/git-utils'
20+
import * as path from 'path'
21+
import { Logger } from '@shared/utils/logger'
22+
import { PanelProvider } from '@/views/panel/backend/panel-provider'
23+
import { dictionary } from '@shared/constants/dictionary'
24+
import { response_preview_promise_resolve } from '../../apply-chat-response-command/utils/preview'
25+
import { remove_old_checkpoints } from './get-checkpoints'
26+
27+
export const create_checkpoint = async (params: {
28+
workspace_provider: WorkspaceProvider
29+
context: vscode.ExtensionContext
30+
panel_provider: PanelProvider
31+
trigger?: CheckpointTrigger
32+
description?: string
33+
silent?: boolean
34+
}): Promise<Checkpoint | undefined> => {
35+
try {
36+
const trigger = params.trigger ?? 'manual'
37+
const are_automatic_checkpoints_disabled = vscode.workspace
38+
.getConfiguration('codeWebChat')
39+
.get<boolean>('areAutomaticCheckpointsDisabled', false)
40+
const is_automatic_checkpoint = trigger != 'manual'
41+
42+
if (are_automatic_checkpoints_disabled && is_automatic_checkpoint) {
43+
Logger.info({
44+
function_name: 'create_checkpoint',
45+
message: 'Automatic checkpoint creation is disabled. Skipping.',
46+
data: { trigger }
47+
})
48+
return undefined
49+
}
50+
51+
const operation_in_progress = params.context.workspaceState.get<number>(
52+
CHECKPOINT_OPERATION_IN_PROGRESS_STATE_KEY
53+
)
54+
if (
55+
operation_in_progress &&
56+
Date.now() - operation_in_progress < 60 * 1000
57+
) {
58+
vscode.window.showWarningMessage(
59+
dictionary.warning_message.CHECKPOINT_OPERATION_IN_PROGRESS
60+
)
61+
return undefined
62+
}
63+
64+
const old_temp_checkpoint = params.context.workspaceState.get<Checkpoint>(
65+
TEMPORARY_CHECKPOINT_STATE_KEY
66+
)
67+
if (old_temp_checkpoint) {
68+
try {
69+
const checkpoint_path = get_checkpoint_path(
70+
old_temp_checkpoint.timestamp
71+
)
72+
await vscode.workspace.fs.delete(vscode.Uri.file(checkpoint_path), {
73+
recursive: true
74+
})
75+
} catch (error) {
76+
Logger.warn({
77+
function_name: 'create_checkpoint',
78+
message: 'Could not delete old temporary checkpoint file',
79+
data: error
80+
})
81+
}
82+
await params.context.workspaceState.update(
83+
TEMPORARY_CHECKPOINT_STATE_KEY,
84+
undefined
85+
)
86+
}
87+
const timestamp = Date.now()
88+
const workspace_folders = vscode.workspace.workspaceFolders!
89+
90+
const folder_git_statuses = await Promise.all(
91+
workspace_folders.map((folder) => is_git_repository(folder))
92+
)
93+
let new_checkpoint: Checkpoint | undefined
94+
95+
const create_checkpoint_task = async () => {
96+
await params.context.workspaceState.update(
97+
CHECKPOINT_OPERATION_IN_PROGRESS_STATE_KEY,
98+
Date.now()
99+
)
100+
await vscode.workspace.saveAll()
101+
const checkpoint_dir_path = get_checkpoint_path(timestamp)
102+
const checkpoint_dir_uri = vscode.Uri.file(checkpoint_dir_path)
103+
await vscode.workspace.fs.createDirectory(checkpoint_dir_uri)
104+
105+
const git_data: Record<string, any> = {}
106+
let uses_git = false
107+
108+
for (const [index, folder] of workspace_folders.entries()) {
109+
const has_git = folder_git_statuses[index]
110+
111+
if (has_git) {
112+
uses_git = true
113+
const git_info = await get_git_info(folder)
114+
const diff = await get_git_diff(folder)
115+
116+
if (git_info && diff !== null) {
117+
const folder_name = folder.name
118+
git_data[folder_name] = {
119+
branch: git_info.branch,
120+
commit_hash: git_info.commit_hash,
121+
folder_name
122+
}
123+
124+
const diff_file_path = path.join(
125+
checkpoint_dir_path,
126+
`${folder_name}.diff`
127+
)
128+
await vscode.workspace.fs.writeFile(
129+
vscode.Uri.file(diff_file_path),
130+
Buffer.from(diff, 'utf8')
131+
)
132+
} else {
133+
Logger.warn({
134+
function_name: 'create_checkpoint',
135+
message: `Failed to get git information for repository in ${folder.name}. Falling back to file copy.`
136+
})
137+
const dest_folder_path =
138+
workspace_folders.length > 1
139+
? path.join(checkpoint_dir_path, folder.name)
140+
: checkpoint_dir_path
141+
const dest_folder_uri = vscode.Uri.file(dest_folder_path)
142+
if (workspace_folders.length > 1) {
143+
await vscode.workspace.fs.createDirectory(dest_folder_uri)
144+
}
145+
146+
const entries = await vscode.workspace.fs.readDirectory(folder.uri)
147+
for (const [name] of entries) {
148+
await copy_optimised_recursively(
149+
vscode.Uri.joinPath(folder.uri, name),
150+
vscode.Uri.joinPath(dest_folder_uri, name),
151+
folder.uri.fsPath,
152+
params.workspace_provider
153+
)
154+
}
155+
}
156+
} else {
157+
const dest_folder_path =
158+
workspace_folders.length > 1
159+
? path.join(checkpoint_dir_path, folder.name)
160+
: checkpoint_dir_path
161+
const dest_folder_uri = vscode.Uri.file(dest_folder_path)
162+
if (workspace_folders.length > 1) {
163+
await vscode.workspace.fs.createDirectory(dest_folder_uri)
164+
}
165+
166+
const entries = await vscode.workspace.fs.readDirectory(folder.uri)
167+
for (const [name] of entries) {
168+
await copy_optimised_recursively(
169+
vscode.Uri.joinPath(folder.uri, name),
170+
vscode.Uri.joinPath(dest_folder_uri, name),
171+
folder.uri.fsPath,
172+
params.workspace_provider
173+
)
174+
}
175+
}
176+
}
177+
178+
let checkpoints = await get_checkpoints(params.context)
179+
180+
// Remove checkpoints older than 48 hours
181+
checkpoints = await remove_old_checkpoints(checkpoints)
182+
183+
const final_description = get_incremented_description(
184+
trigger,
185+
params.description,
186+
checkpoints
187+
)
188+
189+
const active_tabs: CheckpointTab[] = []
190+
const tab_groups = vscode.window.tabGroups.all
191+
for (const group of tab_groups) {
192+
for (const tab of group.tabs) {
193+
if (tab.input instanceof vscode.TabInputText) {
194+
active_tabs.push({
195+
uri: (tab.input as vscode.TabInputText).uri.toString(),
196+
view_column: group.viewColumn,
197+
is_active: tab.isActive,
198+
is_group_active: group.isActive
199+
})
200+
}
201+
}
202+
}
203+
204+
const checkpoint_object: Checkpoint = {
205+
trigger,
206+
timestamp,
207+
description: final_description,
208+
uses_git,
209+
git_data: Object.keys(git_data).length > 0 ? git_data : undefined,
210+
checked_files: params.workspace_provider.get_all_checked_paths(),
211+
active_tabs
212+
}
213+
214+
if (trigger == 'manual' && !response_preview_promise_resolve) {
215+
checkpoint_object.response_history = [
216+
...params.panel_provider.response_history
217+
]
218+
}
219+
220+
checkpoints.unshift(checkpoint_object)
221+
await params.context.workspaceState.update(
222+
CHECKPOINTS_STATE_KEY,
223+
checkpoints
224+
)
225+
await params.panel_provider.send_checkpoints()
226+
await params.context.workspaceState.update(
227+
CHECKPOINT_OPERATION_IN_PROGRESS_STATE_KEY,
228+
undefined
229+
)
230+
new_checkpoint = checkpoint_object
231+
}
232+
233+
let timer: ReturnType<typeof setTimeout> | undefined
234+
let did_show_modal = false
235+
236+
try {
237+
if (!params.silent) {
238+
timer = setTimeout(() => {
239+
did_show_modal = true
240+
params.panel_provider.send_message({
241+
command: 'SHOW_PROGRESS',
242+
title: 'Creating checkpoint...'
243+
})
244+
}, 1000)
245+
}
246+
247+
await create_checkpoint_task()
248+
} finally {
249+
if (timer) clearTimeout(timer)
250+
if (did_show_modal) {
251+
params.panel_provider.send_message({
252+
command: 'HIDE_PROGRESS'
253+
})
254+
}
255+
}
256+
return new_checkpoint
257+
} catch (err: any) {
258+
vscode.window.showErrorMessage(
259+
`Failed to create checkpoint: ${err.message}`
260+
)
261+
await params.context.workspaceState.update(
262+
CHECKPOINT_OPERATION_IN_PROGRESS_STATE_KEY,
263+
undefined
264+
)
265+
return undefined
266+
}
267+
}

0 commit comments

Comments
 (0)