Skip to content

Commit 9b0bf0d

Browse files
committed
Add checkpoint mechanism for AI copilot chat
- Capture workspace snapshots on user messages - Add revert button to restore conversation and files to previous state - Implement real-time notification when checkpoint is ready - Sync localStorage with state machine on restore
1 parent 2c2685a commit 9b0bf0d

File tree

10 files changed

+505
-15
lines changed

10 files changed

+505
-15
lines changed

workspaces/ballerina/ballerina-core/src/state-machine-types.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ export enum AIChatMachineEventType {
421421
DISABLE_AUTO_APPROVE = 'DISABLE_AUTO_APPROVE',
422422
REJECT_TASK = 'REJECT_TASK',
423423
RESTORE_STATE = 'RESTORE_STATE',
424+
RESTORE_CHECKPOINT = 'RESTORE_CHECKPOINT',
424425
ERROR = 'ERROR',
425426
RETRY = 'RETRY',
426427
CONNECTOR_GENERATION_REQUESTED = 'CONNECTOR_GENERATION_REQUESTED',
@@ -434,6 +435,16 @@ export interface ChatMessage {
434435
uiResponse: string;
435436
modelMessages: any[];
436437
timestamp: number;
438+
checkpointId?: string;
439+
}
440+
441+
export interface Checkpoint {
442+
id: string;
443+
messageId: string;
444+
timestamp: number;
445+
workspaceSnapshot: { [filePath: string]: string };
446+
fileList: string[];
447+
snapshotSize: number;
437448
}
438449

439450
/**
@@ -497,6 +508,7 @@ export interface AIChatMachineContext {
497508
comment?: string;
498509
};
499510
previousState?: AIChatMachineStateValue;
511+
checkpoints?: Checkpoint[];
500512
}
501513

502514
export type AIChatMachineSendableEvent =
@@ -515,6 +527,7 @@ export type AIChatMachineSendableEvent =
515527
| { type: AIChatMachineEventType.REJECT_TASK; payload: { comment?: string } }
516528
| { type: AIChatMachineEventType.RESET }
517529
| { type: AIChatMachineEventType.RESTORE_STATE; payload: { state: AIChatMachineContext } }
530+
| { type: AIChatMachineEventType.RESTORE_CHECKPOINT; payload: { checkpointId: string } }
518531
| { type: AIChatMachineEventType.ERROR; payload: { message: string } }
519532
| { type: AIChatMachineEventType.RETRY }
520533
| { type: AIChatMachineEventType.CONNECTOR_GENERATION_REQUESTED; payload: { requestId: string; serviceName?: string; serviceDescription?: string; fromState?: AIChatMachineStateValue } }
@@ -577,6 +590,8 @@ export enum ColorThemeKind {
577590
export interface UIChatHistoryMessage {
578591
role: "user" | "assistant";
579592
content: string;
593+
checkpointId?: string;
594+
messageId?: string;
580595
}
581596

582597
export const aiStateChanged: NotificationType<AIMachineStateValue> = { method: 'aiStateChanged' };
@@ -588,6 +603,12 @@ export const sendAIChatStateEvent: RequestType<AIChatMachineEventType | AIChatMa
588603
export const getAIChatContext: RequestType<void, AIChatMachineContext> = { method: 'getAIChatContext' };
589604
export const getAIChatUIHistory: RequestType<void, UIChatHistoryMessage[]> = { method: 'getAIChatUIHistory' };
590605

606+
export interface CheckpointCapturedPayload {
607+
messageId: string;
608+
checkpointId: string;
609+
}
610+
export const checkpointCaptured: NotificationType<CheckpointCapturedPayload> = { method: 'checkpointCaptured' };
611+
591612
// Connector Generator RPC methods
592613
export interface ConnectorGeneratorResponsePayload {
593614
requestId: string;

workspaces/ballerina/ballerina-extension/package.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,33 @@
233233
"Trace request/response messages only",
234234
"Trace request/response messages with parameters and content"
235235
]
236+
},
237+
"ballerina.copilot.checkpoints.enabled": {
238+
"type": "boolean",
239+
"default": true,
240+
"description": "Enable checkpoint mechanism for copilot chat to restore previous states."
241+
},
242+
"ballerina.copilot.checkpoints.maxCount": {
243+
"type": "number",
244+
"default": 20,
245+
"description": "Maximum number of checkpoints to keep."
246+
},
247+
"ballerina.copilot.checkpoints.ignorePatterns": {
248+
"type": "array",
249+
"default": [
250+
".git/**",
251+
"target/**",
252+
"build/**",
253+
"dist/**",
254+
".vscode/**",
255+
"*.log"
256+
],
257+
"description": "File patterns to ignore when creating checkpoints."
258+
},
259+
"ballerina.copilot.checkpoints.maxSnapshotSize": {
260+
"type": "number",
261+
"default": 52428800,
262+
"description": "Maximum snapshot size in bytes (default: 50MB)."
236263
}
237264
}
238265
},
@@ -1221,6 +1248,7 @@
12211248
"jwt-decode": "^4.0.0",
12221249
"langfuse-vercel": "^3.38.6",
12231250
"lodash": "^4.17.21",
1251+
"minimatch": "^10.1.1",
12241252
"monaco-languageclient": "0.13.1-next.9",
12251253
"node-fetch": "^3.3.2",
12261254
"node-schedule": "^2.1.1",

workspaces/ballerina/ballerina-extension/src/RPCLayer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import { WebviewView, WebviewPanel, window } from 'vscode';
2020
import { Messenger } from 'vscode-messenger';
2121
import { StateMachine } from './stateMachine';
22-
import { stateChanged, getVisualizerLocation, VisualizerLocation, projectContentUpdated, aiStateChanged, sendAIStateEvent, popupStateChanged, getPopupVisualizerState, PopupVisualizerLocation, breakpointChanged, AIMachineEventType, ArtifactData, onArtifactUpdatedNotification, onArtifactUpdatedRequest, currentThemeChanged, AIMachineSendableEvent, aiChatStateChanged, sendAIChatStateEvent, getAIChatContext, getAIChatUIHistory, AIChatMachineEventType, AIChatMachineSendableEvent } from '@wso2/ballerina-core';
22+
import { stateChanged, getVisualizerLocation, VisualizerLocation, projectContentUpdated, aiStateChanged, sendAIStateEvent, popupStateChanged, getPopupVisualizerState, PopupVisualizerLocation, breakpointChanged, AIMachineEventType, ArtifactData, onArtifactUpdatedNotification, onArtifactUpdatedRequest, currentThemeChanged, AIMachineSendableEvent, aiChatStateChanged, sendAIChatStateEvent, getAIChatContext, getAIChatUIHistory, AIChatMachineEventType, AIChatMachineSendableEvent, checkpointCaptured, CheckpointCapturedPayload } from '@wso2/ballerina-core';
2323
import { VisualizerWebview } from './views/visualizer/webview';
2424
import { registerVisualizerRpcHandlers } from './rpc-managers/visualizer/rpc-handler';
2525
import { registerLangClientRpcHandlers } from './rpc-managers/lang-client/rpc-handler';
@@ -190,3 +190,7 @@ export function notifyAiWebview() {
190190
export function notifyBreakpointChange() {
191191
RPCLayer._messenger.sendNotification(breakpointChanged, { type: 'webview', webviewType: VisualizerWebview.viewType }, true);
192192
}
193+
194+
export function notifyCheckpointCaptured(payload: CheckpointCapturedPayload) {
195+
RPCLayer._messenger.sendNotification(checkpointCaptured, { type: 'webview', webviewType: AiPanelWebview.viewType }, payload);
196+
}

workspaces/ballerina/ballerina-extension/src/views/ai-panel/aiChatMachine.ts

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@
2020
import { createMachine, assign, interpret } from 'xstate';
2121
import { extension } from '../../BalExtensionContext';
2222
import * as crypto from 'crypto';
23-
import { AIChatMachineContext, AIChatMachineEventType, AIChatMachineSendableEvent, AIChatMachineStateValue, ChatMessage, Plan, Task, TaskStatus, UIChatHistoryMessage } from '@wso2/ballerina-core/lib/state-machine-types';
23+
import { AIChatMachineContext, AIChatMachineEventType, AIChatMachineSendableEvent, AIChatMachineStateValue, ChatMessage, Plan, Task, TaskStatus, UIChatHistoryMessage, Checkpoint } from '@wso2/ballerina-core/lib/state-machine-types';
2424
import { workspace } from 'vscode';
2525
import { GenerateAgentCodeRequest } from '@wso2/ballerina-core/lib/rpc-types/ai-panel/interfaces';
2626
import { generateDesign } from '../../features/ai/service/design/design';
27+
import { captureWorkspaceSnapshot, restoreWorkspaceSnapshot } from './checkpoint/checkpointUtils';
28+
import { getCheckpointConfig } from './checkpoint/checkpointConfig';
29+
import { notifyCheckpointCaptured } from '../../RPCLayer';
2730

2831
const generateId = () => `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2932

@@ -93,6 +96,67 @@ const updateChatMessage = (
9396
});
9497
};
9598

99+
const cleanupOldCheckpoints = (checkpoints: Checkpoint[]): Checkpoint[] => {
100+
const config = getCheckpointConfig();
101+
if (checkpoints.length <= config.maxCount) {
102+
return checkpoints;
103+
}
104+
return checkpoints.slice(-config.maxCount);
105+
};
106+
107+
const captureCheckpointAction = (context: AIChatMachineContext) => {
108+
const lastMessage = context.chatHistory[context.chatHistory.length - 1];
109+
if (!lastMessage) {
110+
return;
111+
}
112+
113+
captureWorkspaceSnapshot(lastMessage.id).then(checkpoint => {
114+
if (checkpoint) {
115+
lastMessage.checkpointId = checkpoint.id;
116+
const updatedCheckpoints = cleanupOldCheckpoints([...(context.checkpoints || []), checkpoint]);
117+
context.checkpoints = updatedCheckpoints;
118+
saveChatState(context);
119+
120+
// Notify frontend that checkpoint is captured
121+
notifyCheckpointCaptured({
122+
messageId: lastMessage.id,
123+
checkpointId: checkpoint.id
124+
});
125+
}
126+
}).catch(error => {
127+
console.error('[Checkpoint] Failed to capture checkpoint:', error);
128+
});
129+
};
130+
131+
const restoreCheckpointAction = (context: AIChatMachineContext, event: any) => {
132+
const checkpointId = event.payload.checkpointId;
133+
const checkpoint = context.checkpoints?.find(c => c.id === checkpointId);
134+
135+
if (!checkpoint) {
136+
console.error(`[Checkpoint] Checkpoint ${checkpointId} not found`);
137+
return;
138+
}
139+
140+
const messageIndex = context.chatHistory.findIndex(m => m.id === checkpoint.messageId);
141+
const restoredHistory = messageIndex >= 0 ? context.chatHistory.slice(0, messageIndex) : context.chatHistory;
142+
143+
const checkpointIndex = context.checkpoints?.findIndex(c => c.id === checkpointId) || 0;
144+
const restoredCheckpoints = checkpointIndex >= 0 ? (context.checkpoints?.slice(0, checkpointIndex) || []) : (context.checkpoints || []);
145+
146+
context.chatHistory = restoredHistory;
147+
context.checkpoints = restoredCheckpoints;
148+
context.currentPlan = undefined;
149+
context.currentTaskIndex = -1;
150+
151+
saveChatState(context);
152+
153+
restoreWorkspaceSnapshot(checkpoint).then(() => {
154+
155+
}).catch(error => {
156+
console.error('[Checkpoint] Failed to restore workspace snapshot:', error);
157+
});
158+
};
159+
96160
const chatMachine = createMachine<AIChatMachineContext, AIChatMachineSendableEvent>({
97161
/** @xstate-layout N4IgpgJg5mDOIC5QCMCGAbdYBOBLAdqgLSq5EDGAFqgC4B0AwtmLQVAArqr4DEEA9vjB0CAN34BrYeWa0wnbgG0ADAF1EoAA79YuGrkEaQAD0QBmACxm6ZgJzKAbACZbTpwEYnZ97YA0IAE9EAHZg9zoADk8HZQBWW2DHC1iLAF9U-zRMHAJiUgpqeiYWfXwOLl4cbH5sOk0uGgAzGoBbOhkS+QqVdSQQbV19Qz7TBEtrO0cXN09vP0DECIdYuidl4KcLdwdgiPtY9MyMLDxCEjIqWjoAVU0IVjKFXgEhEXxxKToAVzu5J56jAM9AZ8EZRuMbPZnK4PF4fP4gghYhELHQoWYHLZYsEHO5YrEzIcQFkTrlzgUrrd7qVytweFUanUGs1sG0ftSuko1ICdMDhqBwVZIVMYbN4QsEDsIjZ4hYIqFgpZlMo0hlicccmd8pd6ABRYxgchfGkAFVQsAkfEEwjEkmEYANRpoYDNFoBfSBQ1BI3MQsm0JmcPmiKidAstgjtnD9lcZkcRJJmryF0KdH1huNbFdloZtXqtBZbQdGed2fdWl5XrBvomUOmsLmCMQ7mCqKx3nc3giUQcUWCCY1p2TFPoADECGAaF98FnzZaXjb3na6I0J1OhGXuR7KyDq2M-XXRUGm2NcZF4pYsRE1sjVUdskPyTq6OOhOvZxb6dhqnnma0V2u04unO5b9Du-ImDWwoBg24qIms0oWFM3hmBEKSdoSRL4PwEBwEYiaPtqhQ8oMu4+ggRBmNYiSxEkeLuHsSGxCeRArJGEZOPKtF2O4cQDg+ZJEVcACSEBYCRfLegKzadqsDHBLE7jbE42JKSe4ZOHQnacS416Yg4yQHGqBGCSmVzFA8tJSWBpEQaM7ioi44ZmLeURxFiJ44ppyqxF4yhLDETitvxpJamZ9BPAASmAoi4GAADuElVuR6E2FYOIRGYwRQnBiDxIhDjOIqqH2N2d7qgJYUjjcvw0k8SVkdJSK2GiUwWBYQVbBsYQnvEwR0A4ZhOT52L4u4IVJk+qbpk6H4SA1dmIMkdAEvszgMVGjhmCeaErfl14ooNPhWBNhHhXQACC8WkDS1ywDgF3kMlFa2dZoypeMGVZTlwbNteYbZb2sp2IN+KnaZ1WvpO05zQtb1LU4J6ceE2kKVlWy8di4NVc+ACKXxwF6bB3TgcN7iiNhDcoazZT4g2OCeqEOANQ1LCDth7INmH3qFw7Pgw-AtPUk6QGT5EU1RTjUzsth03GDg7fYdDKDxHVxoqnZGTzk1CXq341GLTUfelvbfY4uUIIk-XIjEVEdTe8bpKkQA */
98162
id: 'ballerina-ai-chat',
@@ -110,17 +174,21 @@ const chatMachine = createMachine<AIChatMachineContext, AIChatMachineSendableEve
110174
autoApproveEnabled: false,
111175
previousState: undefined,
112176
currentSpec: undefined,
177+
checkpoints: [],
113178
} as AIChatMachineContext,
114179
on: {
115180
[AIChatMachineEventType.SUBMIT_PROMPT]: {
116181
target: 'Initiating',
117-
actions: assign({
118-
chatHistory: (ctx, event) =>
119-
addUserMessage(ctx.chatHistory, event.payload.prompt),
120-
currentPlan: (_ctx) => undefined,
121-
currentTaskIndex: (_ctx) => -1,
122-
errorMessage: (_ctx) => undefined,
123-
}),
182+
actions: [
183+
assign({
184+
chatHistory: (ctx, event) =>
185+
addUserMessage(ctx.chatHistory, event.payload.prompt),
186+
currentPlan: (_ctx) => undefined,
187+
currentTaskIndex: (_ctx) => -1,
188+
errorMessage: (_ctx) => undefined,
189+
}),
190+
'captureCheckpoint',
191+
],
124192
},
125193
[AIChatMachineEventType.UPDATE_CHAT_MESSAGE]: {
126194
actions: assign({
@@ -151,7 +219,7 @@ const chatMachine = createMachine<AIChatMachineContext, AIChatMachineSendableEve
151219
currentQuestion: (_ctx) => undefined,
152220
errorMessage: (_ctx) => undefined,
153221
sessionId: (_ctx) => undefined,
154-
// Keep projectId to maintain project context
222+
checkpoints: (_ctx) => [],
155223
}),
156224
],
157225
},
@@ -166,6 +234,10 @@ const chatMachine = createMachine<AIChatMachineContext, AIChatMachineSendableEve
166234
sessionId: (_ctx, event) => event.payload.state.sessionId,
167235
}),
168236
},
237+
[AIChatMachineEventType.RESTORE_CHECKPOINT]: {
238+
target: 'Idle',
239+
actions: ['restoreCheckpoint'],
240+
},
169241
[AIChatMachineEventType.ERROR]: {
170242
target: 'Error',
171243
actions: assign({
@@ -547,7 +619,9 @@ const convertChatHistoryToUIMessages = (chatHistory: ChatMessage[]): UIChatHisto
547619

548620
messages.push({
549621
role: 'user',
550-
content: msg.content
622+
content: msg.content,
623+
checkpointId: msg.checkpointId,
624+
messageId: msg.id
551625
});
552626

553627
if (msg.uiResponse) {
@@ -614,6 +688,7 @@ const saveChatState = (context: AIChatMachineContext) => {
614688
currentTaskIndex: context.currentTaskIndex,
615689
sessionId: context.sessionId,
616690
projectId: context.projectId,
691+
checkpoints: context.checkpoints || [],
617692
savedAt: Date.now(),
618693
};
619694

@@ -764,6 +839,8 @@ const chatStateService = interpret(
764839
actions: {
765840
saveChatState: (context) => saveChatState(context),
766841
clearChatState: (context) => clearChatStateAction(context),
842+
captureCheckpoint: (context) => captureCheckpointAction(context),
843+
restoreCheckpoint: (context, event) => restoreCheckpointAction(context, event),
767844
},
768845
})
769846
);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved.
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
import * as vscode from 'vscode';
20+
21+
export interface CheckpointConfig {
22+
enabled: boolean;
23+
maxCount: number;
24+
ignorePatterns: string[];
25+
maxSnapshotSize: number;
26+
}
27+
28+
export const DEFAULT_CHECKPOINT_CONFIG: CheckpointConfig = {
29+
enabled: true,
30+
maxCount: 20,
31+
ignorePatterns: [
32+
'node_modules/**',
33+
'.git/**',
34+
'target/**',
35+
'build/**',
36+
'dist/**',
37+
'.vscode/**',
38+
'*.log',
39+
'.DS_Store'
40+
],
41+
maxSnapshotSize: 52428800
42+
};
43+
44+
export function getCheckpointConfig(): CheckpointConfig {
45+
const config = vscode.workspace.getConfiguration('ballerina.copilot.checkpoints');
46+
47+
return {
48+
enabled: config.get('enabled', DEFAULT_CHECKPOINT_CONFIG.enabled),
49+
maxCount: config.get('maxCount', DEFAULT_CHECKPOINT_CONFIG.maxCount),
50+
ignorePatterns: config.get('ignorePatterns', DEFAULT_CHECKPOINT_CONFIG.ignorePatterns),
51+
maxSnapshotSize: config.get('maxSnapshotSize', DEFAULT_CHECKPOINT_CONFIG.maxSnapshotSize)
52+
};
53+
}

0 commit comments

Comments
 (0)