snow-ai 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,171 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { sessionManager } from '../../utils/sessionManager.js';
4
+ export default function SessionListPanel({ onSelectSession, onClose }) {
5
+ const [sessions, setSessions] = useState([]);
6
+ const [loading, setLoading] = useState(true);
7
+ const [selectedIndex, setSelectedIndex] = useState(0);
8
+ const [scrollOffset, setScrollOffset] = useState(0);
9
+ const [markedSessions, setMarkedSessions] = useState(new Set());
10
+ const VISIBLE_ITEMS = 5; // Number of items to show at once
11
+ // Load sessions on mount
12
+ useEffect(() => {
13
+ const loadSessions = async () => {
14
+ setLoading(true);
15
+ try {
16
+ const sessionList = await sessionManager.listSessions();
17
+ setSessions(sessionList);
18
+ }
19
+ catch (error) {
20
+ console.error('Failed to load sessions:', error);
21
+ setSessions([]);
22
+ }
23
+ finally {
24
+ setLoading(false);
25
+ }
26
+ };
27
+ void loadSessions();
28
+ }, []);
29
+ // Format date to relative time
30
+ const formatDate = useCallback((timestamp) => {
31
+ const date = new Date(timestamp);
32
+ const now = new Date();
33
+ const diffMs = now.getTime() - date.getTime();
34
+ const diffMinutes = Math.floor(diffMs / (1000 * 60));
35
+ const diffHours = Math.floor(diffMinutes / 60);
36
+ const diffDays = Math.floor(diffHours / 24);
37
+ if (diffMinutes < 1)
38
+ return 'now';
39
+ if (diffMinutes < 60)
40
+ return `${diffMinutes}m`;
41
+ if (diffHours < 24)
42
+ return `${diffHours}h`;
43
+ if (diffDays < 7)
44
+ return `${diffDays}d`;
45
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
46
+ }, []);
47
+ // Handle keyboard input
48
+ useInput((input, key) => {
49
+ if (loading)
50
+ return;
51
+ if (key.escape) {
52
+ onClose();
53
+ return;
54
+ }
55
+ if (key.upArrow) {
56
+ setSelectedIndex(prev => {
57
+ const newIndex = Math.max(0, prev - 1);
58
+ // Adjust scroll offset if needed
59
+ if (newIndex < scrollOffset) {
60
+ setScrollOffset(newIndex);
61
+ }
62
+ return newIndex;
63
+ });
64
+ return;
65
+ }
66
+ if (key.downArrow) {
67
+ setSelectedIndex(prev => {
68
+ const newIndex = Math.min(sessions.length - 1, prev + 1);
69
+ // Adjust scroll offset if needed
70
+ if (newIndex >= scrollOffset + VISIBLE_ITEMS) {
71
+ setScrollOffset(newIndex - VISIBLE_ITEMS + 1);
72
+ }
73
+ return newIndex;
74
+ });
75
+ return;
76
+ }
77
+ // Space to toggle mark
78
+ if (input === ' ') {
79
+ const currentSession = sessions[selectedIndex];
80
+ if (currentSession) {
81
+ setMarkedSessions(prev => {
82
+ const next = new Set(prev);
83
+ if (next.has(currentSession.id)) {
84
+ next.delete(currentSession.id);
85
+ }
86
+ else {
87
+ next.add(currentSession.id);
88
+ }
89
+ return next;
90
+ });
91
+ }
92
+ return;
93
+ }
94
+ // D to delete marked sessions
95
+ if (input === 'd' || input === 'D') {
96
+ if (markedSessions.size > 0) {
97
+ const deleteMarked = async () => {
98
+ const ids = Array.from(markedSessions);
99
+ await Promise.all(ids.map(id => sessionManager.deleteSession(id)));
100
+ // Reload sessions
101
+ const sessionList = await sessionManager.listSessions();
102
+ setSessions(sessionList);
103
+ setMarkedSessions(new Set());
104
+ // Reset selection if needed
105
+ if (selectedIndex >= sessionList.length && sessionList.length > 0) {
106
+ setSelectedIndex(sessionList.length - 1);
107
+ }
108
+ };
109
+ void deleteMarked();
110
+ }
111
+ return;
112
+ }
113
+ if (key.return && sessions.length > 0) {
114
+ const selectedSession = sessions[selectedIndex];
115
+ if (selectedSession) {
116
+ onSelectSession(selectedSession.id);
117
+ }
118
+ return;
119
+ }
120
+ });
121
+ if (loading) {
122
+ return (React.createElement(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1 },
123
+ React.createElement(Text, { color: "gray", dimColor: true }, "Loading sessions...")));
124
+ }
125
+ if (sessions.length === 0) {
126
+ return (React.createElement(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1 },
127
+ React.createElement(Text, { color: "gray", dimColor: true }, "No conversations found \u2022 Press ESC to close")));
128
+ }
129
+ // Calculate visible sessions based on scroll offset
130
+ const visibleSessions = sessions.slice(scrollOffset, scrollOffset + VISIBLE_ITEMS);
131
+ const hasMore = sessions.length > scrollOffset + VISIBLE_ITEMS;
132
+ const hasPrevious = scrollOffset > 0;
133
+ const currentSession = sessions[selectedIndex];
134
+ return (React.createElement(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column" },
135
+ React.createElement(Box, { flexDirection: "column" },
136
+ React.createElement(Text, { color: "cyan", dimColor: true },
137
+ "Resume (",
138
+ selectedIndex + 1,
139
+ "/",
140
+ sessions.length,
141
+ ")",
142
+ currentSession && ` • ${currentSession.messageCount} msgs`,
143
+ markedSessions.size > 0 && React.createElement(Text, { color: "yellow" },
144
+ " \u2022 ",
145
+ markedSessions.size,
146
+ " marked")),
147
+ React.createElement(Text, { color: "gray", dimColor: true }, "\u2191\u2193 navigate \u2022 Space mark \u2022 D delete \u2022 Enter select \u2022 ESC close")),
148
+ hasPrevious && (React.createElement(Text, { color: "gray", dimColor: true },
149
+ " \u2191 ",
150
+ scrollOffset,
151
+ " more above")),
152
+ visibleSessions.map((session, index) => {
153
+ const actualIndex = scrollOffset + index;
154
+ const isSelected = actualIndex === selectedIndex;
155
+ const isMarked = markedSessions.has(session.id);
156
+ const title = session.title || 'Untitled';
157
+ const timeStr = formatDate(session.updatedAt);
158
+ const truncatedLabel = title.length > 50 ? title.slice(0, 47) + '...' : title;
159
+ return (React.createElement(Box, { key: session.id },
160
+ React.createElement(Text, { color: isMarked ? 'green' : 'gray' }, isMarked ? '✔ ' : ' '),
161
+ React.createElement(Text, { color: isSelected ? 'green' : 'gray' }, isSelected ? '❯ ' : ' '),
162
+ React.createElement(Text, { color: isSelected ? 'cyan' : isMarked ? 'green' : 'white' }, truncatedLabel),
163
+ React.createElement(Text, { color: "gray", dimColor: true },
164
+ " \u2022 ",
165
+ timeStr)));
166
+ }),
167
+ hasMore && (React.createElement(Text, { color: "gray", dimColor: true },
168
+ " \u2193 ",
169
+ sessions.length - scrollOffset - VISIBLE_ITEMS,
170
+ " more below"))));
171
+ }
@@ -5,20 +5,22 @@ import Gradient from 'ink-gradient';
5
5
  import ChatInput from '../components/ChatInput.js';
6
6
  import PendingMessages from '../components/PendingMessages.js';
7
7
  import MCPInfoScreen from '../components/MCPInfoScreen.js';
8
- import SessionListScreenWrapper from '../components/SessionListScreenWrapper.js';
8
+ import MCPInfoPanel from '../components/MCPInfoPanel.js';
9
+ import SessionListPanel from '../components/SessionListPanel.js';
9
10
  import MarkdownRenderer from '../components/MarkdownRenderer.js';
10
11
  import ToolConfirmation from '../components/ToolConfirmation.js';
11
12
  import DiffViewer from '../components/DiffViewer.js';
12
13
  import ToolResultPreview from '../components/ToolResultPreview.js';
13
14
  import TodoTree from '../components/TodoTree.js';
15
+ import FileRollbackConfirmation from '../components/FileRollbackConfirmation.js';
14
16
  import { getOpenAiConfig } from '../../utils/apiConfig.js';
15
17
  import { sessionManager } from '../../utils/sessionManager.js';
16
18
  import { useSessionSave } from '../../hooks/useSessionSave.js';
17
- import { useSessionManagement } from '../../hooks/useSessionManagement.js';
18
19
  import { useToolConfirmation } from '../../hooks/useToolConfirmation.js';
19
20
  import { handleConversationWithTools } from '../../hooks/useConversation.js';
20
21
  import { parseAndValidateFileReferences, createMessageWithFileInstructions, getSystemInfo } from '../../utils/fileUtils.js';
21
22
  import { compressContext } from '../../utils/contextCompressor.js';
23
+ import { incrementalSnapshotManager } from '../../utils/incrementalSnapshot.js';
22
24
  // Import commands to register them
23
25
  import '../../utils/commands/clear.js';
24
26
  import '../../utils/commands/resume.js';
@@ -68,6 +70,10 @@ export default function ChatScreen({}) {
68
70
  const [editorContext, setEditorContext] = useState({});
69
71
  const [isCompressing, setIsCompressing] = useState(false);
70
72
  const [compressionError, setCompressionError] = useState(null);
73
+ const [showSessionPanel, setShowSessionPanel] = useState(false);
74
+ const [showMcpPanel, setShowMcpPanel] = useState(false);
75
+ const [snapshotFileCount, setSnapshotFileCount] = useState(new Map());
76
+ const [pendingRollback, setPendingRollback] = useState(null);
71
77
  const { stdout } = useStdout();
72
78
  const terminalHeight = stdout?.rows || 24;
73
79
  const workingDirectory = process.cwd();
@@ -81,8 +87,6 @@ export default function ChatScreen({}) {
81
87
  }, [pendingMessages]);
82
88
  // Use tool confirmation hook
83
89
  const { pendingToolConfirmation, requestToolConfirmation, isToolAutoApproved, addMultipleToAlwaysApproved } = useToolConfirmation();
84
- // Use session management hook
85
- const { showSessionList, setShowSessionList, handleSessionSelect, handleBackFromSessionList } = useSessionManagement(setMessages, setPendingMessages, setIsStreaming, setRemountKey, initializeFromSession);
86
90
  // Animation for streaming/saving indicator
87
91
  useEffect(() => {
88
92
  if (!isStreaming && !isSaving)
@@ -171,6 +175,21 @@ export default function ChatScreen({}) {
171
175
  unsubscribe();
172
176
  };
173
177
  }, [vscodeConnectionStatus]);
178
+ // Load snapshot file counts when session changes
179
+ useEffect(() => {
180
+ const loadSnapshotFileCounts = async () => {
181
+ const currentSession = sessionManager.getCurrentSession();
182
+ if (!currentSession)
183
+ return;
184
+ const snapshots = await incrementalSnapshotManager.listSnapshots(currentSession.id);
185
+ const counts = new Map();
186
+ for (const snapshot of snapshots) {
187
+ counts.set(snapshot.messageIndex, snapshot.fileCount);
188
+ }
189
+ setSnapshotFileCount(counts);
190
+ };
191
+ loadSnapshotFileCounts();
192
+ }, [messages.length]); // Reload when messages change
174
193
  // Pending messages are now handled inline during tool execution in useConversation
175
194
  // Auto-send pending messages when streaming completely stops (as fallback)
176
195
  useEffect(() => {
@@ -184,6 +203,24 @@ export default function ChatScreen({}) {
184
203
  }, [isStreaming, pendingMessages.length]);
185
204
  // ESC key handler to interrupt streaming or close overlays
186
205
  useInput((_, key) => {
206
+ if (pendingRollback) {
207
+ if (key.escape) {
208
+ setPendingRollback(null);
209
+ }
210
+ return;
211
+ }
212
+ if (showSessionPanel) {
213
+ if (key.escape) {
214
+ setShowSessionPanel(false);
215
+ }
216
+ return;
217
+ }
218
+ if (showMcpPanel) {
219
+ if (key.escape) {
220
+ setShowMcpPanel(false);
221
+ }
222
+ return;
223
+ }
187
224
  if (showMcpInfo) {
188
225
  if (key.escape) {
189
226
  setShowMcpInfo(false);
@@ -193,7 +230,7 @@ export default function ChatScreen({}) {
193
230
  if (key.escape && isStreaming && abortController) {
194
231
  // Abort the controller
195
232
  abortController.abort();
196
- // Immediately add discontinued message
233
+ // Add discontinued message
197
234
  setMessages(prev => [...prev, {
198
235
  role: 'assistant',
199
236
  content: '',
@@ -293,8 +330,14 @@ export default function ChatScreen({}) {
293
330
  };
294
331
  setMessages([commandMessage]);
295
332
  }
296
- else if (result.success && result.action === 'resume') {
297
- setShowSessionList(true);
333
+ else if (result.success && result.action === 'showSessionPanel') {
334
+ setShowSessionPanel(true);
335
+ const commandMessage = {
336
+ role: 'command',
337
+ content: '',
338
+ commandName: commandName
339
+ };
340
+ setMessages(prev => [...prev, commandMessage]);
298
341
  }
299
342
  else if (result.success && result.action === 'showMcpInfo') {
300
343
  setShowMcpInfo(true);
@@ -306,6 +349,15 @@ export default function ChatScreen({}) {
306
349
  };
307
350
  setMessages(prev => [...prev, commandMessage]);
308
351
  }
352
+ else if (result.success && result.action === 'showMcpPanel') {
353
+ setShowMcpPanel(true);
354
+ const commandMessage = {
355
+ role: 'command',
356
+ content: '',
357
+ commandName: commandName
358
+ };
359
+ setMessages(prev => [...prev, commandMessage]);
360
+ }
309
361
  else if (result.success && result.action === 'goHome') {
310
362
  navigateTo('welcome');
311
363
  }
@@ -330,11 +382,53 @@ export default function ChatScreen({}) {
330
382
  processMessage(result.prompt, undefined, true, true);
331
383
  }
332
384
  };
333
- const handleHistorySelect = (selectedIndex, _message) => {
385
+ const handleHistorySelect = async (selectedIndex, _message) => {
386
+ // Check if there are files to rollback
387
+ const fileCount = snapshotFileCount.get(selectedIndex) || 0;
388
+ if (fileCount > 0) {
389
+ // Show confirmation dialog
390
+ setPendingRollback({ messageIndex: selectedIndex, fileCount });
391
+ }
392
+ else {
393
+ // No files to rollback, just rollback conversation
394
+ performRollback(selectedIndex, false);
395
+ }
396
+ };
397
+ const performRollback = async (selectedIndex, rollbackFiles) => {
398
+ // Rollback workspace to checkpoint if requested
399
+ if (rollbackFiles) {
400
+ const currentSession = sessionManager.getCurrentSession();
401
+ if (currentSession) {
402
+ await incrementalSnapshotManager.rollbackToSnapshot(currentSession.id, selectedIndex);
403
+ }
404
+ }
334
405
  // Truncate messages array to remove the selected user message and everything after it
335
406
  setMessages(prev => prev.slice(0, selectedIndex));
336
407
  clearSavedMessages();
337
408
  setRemountKey(prev => prev + 1);
409
+ // Clear pending rollback dialog
410
+ setPendingRollback(null);
411
+ };
412
+ const handleRollbackConfirm = (rollbackFiles) => {
413
+ if (pendingRollback) {
414
+ performRollback(pendingRollback.messageIndex, rollbackFiles);
415
+ }
416
+ };
417
+ const handleSessionPanelSelect = async (sessionId) => {
418
+ setShowSessionPanel(false);
419
+ try {
420
+ const session = await sessionManager.loadSession(sessionId);
421
+ if (session) {
422
+ initializeFromSession(session.messages);
423
+ setMessages(session.messages);
424
+ setPendingMessages([]);
425
+ setIsStreaming(false);
426
+ setRemountKey(prev => prev + 1);
427
+ }
428
+ }
429
+ catch (error) {
430
+ console.error('Failed to load session:', error);
431
+ }
338
432
  };
339
433
  const handleMessageSubmit = async (message, images) => {
340
434
  // If streaming, add to pending messages instead of sending immediately
@@ -342,6 +436,15 @@ export default function ChatScreen({}) {
342
436
  setPendingMessages(prev => [...prev, message]);
343
437
  return;
344
438
  }
439
+ // Create checkpoint (lightweight, only tracks modifications)
440
+ const currentSession = sessionManager.getCurrentSession();
441
+ if (!currentSession) {
442
+ await sessionManager.createNewSession();
443
+ }
444
+ const session = sessionManager.getCurrentSession();
445
+ if (session) {
446
+ await incrementalSnapshotManager.createSnapshot(session.id, messages.length);
447
+ }
345
448
  // Process the message normally
346
449
  await processMessage(message, images);
347
450
  };
@@ -393,7 +496,8 @@ export default function ChatScreen({}) {
393
496
  setContextUsage,
394
497
  useBasicModel,
395
498
  getPendingMessages: () => pendingMessagesRef.current,
396
- clearPendingMessages: () => setPendingMessages([])
499
+ clearPendingMessages: () => setPendingMessages([]),
500
+ setIsStreaming
397
501
  });
398
502
  }
399
503
  catch (error) {
@@ -455,7 +559,8 @@ export default function ChatScreen({}) {
455
559
  yoloMode,
456
560
  setContextUsage,
457
561
  getPendingMessages: () => pendingMessagesRef.current,
458
- clearPendingMessages: () => setPendingMessages([])
562
+ clearPendingMessages: () => setPendingMessages([]),
563
+ setIsStreaming
459
564
  });
460
565
  }
461
566
  catch (error) {
@@ -477,10 +582,6 @@ export default function ChatScreen({}) {
477
582
  setStreamTokenCount(0);
478
583
  }
479
584
  };
480
- // If showing session list, only render that
481
- if (showSessionList) {
482
- return (React.createElement(SessionListScreenWrapper, { onBack: handleBackFromSessionList, onSelectSession: handleSessionSelect }));
483
- }
484
585
  if (showMcpInfo) {
485
586
  return (React.createElement(MCPInfoScreen, { onClose: () => setShowMcpInfo(false), panelKey: mcpPanelKey }));
486
587
  }
@@ -499,7 +600,7 @@ export default function ChatScreen({}) {
499
600
  React.createElement(Box, { marginTop: 1 },
500
601
  React.createElement(Text, { color: "gray", dimColor: true }, "Please resize your terminal window to continue."))));
501
602
  }
502
- return (React.createElement(Box, { flexDirection: "column" },
603
+ return (React.createElement(Box, { flexDirection: "column", height: "100%" },
503
604
  React.createElement(Static, { key: remountKey, items: [
504
605
  React.createElement(Box, { key: "header", marginX: 1, borderColor: 'cyan', borderStyle: "round", paddingX: 2, paddingY: 1 },
505
606
  React.createElement(Box, { flexDirection: "column" },
@@ -617,11 +718,18 @@ export default function ChatScreen({}) {
617
718
  React.createElement(Box, { marginX: 1 },
618
719
  React.createElement(PendingMessages, { pendingMessages: pendingMessages })),
619
720
  pendingToolConfirmation && (React.createElement(ToolConfirmation, { toolName: pendingToolConfirmation.batchToolNames || pendingToolConfirmation.tool.function.name, onConfirm: pendingToolConfirmation.resolve })),
620
- !pendingToolConfirmation && !isCompressing && (React.createElement(React.Fragment, null,
721
+ showSessionPanel && (React.createElement(Box, { marginX: 1 },
722
+ React.createElement(SessionListPanel, { onSelectSession: handleSessionPanelSelect, onClose: () => setShowSessionPanel(false) }))),
723
+ showMcpPanel && (React.createElement(Box, { marginX: 1, flexDirection: "column" },
724
+ React.createElement(MCPInfoPanel, null),
725
+ React.createElement(Box, { marginTop: 1 },
726
+ React.createElement(Text, { color: "gray", dimColor: true }, "Press ESC to close")))),
727
+ pendingRollback && (React.createElement(FileRollbackConfirmation, { fileCount: pendingRollback.fileCount, onConfirm: handleRollbackConfirm })),
728
+ !pendingToolConfirmation && !isCompressing && !showSessionPanel && !showMcpPanel && !pendingRollback && (React.createElement(React.Fragment, null,
621
729
  React.createElement(ChatInput, { onSubmit: handleMessageSubmit, onCommand: handleCommandExecution, placeholder: "Ask me anything about coding...", disabled: !!pendingToolConfirmation, chatHistory: messages, onHistorySelect: handleHistorySelect, yoloMode: yoloMode, contextUsage: contextUsage ? {
622
730
  inputTokens: contextUsage.prompt_tokens,
623
731
  maxContextTokens: getOpenAiConfig().maxContextTokens || 4000
624
- } : undefined }),
732
+ } : undefined, snapshotFileCount: snapshotFileCount }),
625
733
  vscodeConnectionStatus !== 'disconnected' && (React.createElement(Box, { marginTop: 1 },
626
734
  React.createElement(Text, { color: vscodeConnectionStatus === 'connecting' ? 'yellow' :
627
735
  vscodeConnectionStatus === 'connected' ? 'green' :
@@ -0,0 +1,74 @@
1
+ /**
2
+ * File checkpoint data structure
3
+ */
4
+ export interface FileCheckpoint {
5
+ path: string;
6
+ content: string;
7
+ timestamp: number;
8
+ exists: boolean;
9
+ }
10
+ /**
11
+ * Conversation checkpoint data structure
12
+ */
13
+ export interface ConversationCheckpoint {
14
+ sessionId: string;
15
+ messageCount: number;
16
+ fileSnapshots: FileCheckpoint[];
17
+ timestamp: number;
18
+ }
19
+ /**
20
+ * Checkpoint Manager
21
+ * Manages file snapshots for rollback on ESC interrupt
22
+ */
23
+ declare class CheckpointManager {
24
+ private readonly checkpointsDir;
25
+ private activeCheckpoint;
26
+ constructor();
27
+ /**
28
+ * Ensure checkpoints directory exists
29
+ */
30
+ private ensureCheckpointsDir;
31
+ /**
32
+ * Get checkpoint file path for a session
33
+ */
34
+ private getCheckpointPath;
35
+ /**
36
+ * Create a new checkpoint before AI response
37
+ * @param sessionId - Current session ID
38
+ * @param messageCount - Number of messages before AI response
39
+ */
40
+ createCheckpoint(sessionId: string, messageCount: number): Promise<void>;
41
+ /**
42
+ * Record a file snapshot before modification
43
+ * @param filePath - Absolute path to the file
44
+ */
45
+ recordFileSnapshot(filePath: string): Promise<void>;
46
+ /**
47
+ * Save current checkpoint to disk
48
+ */
49
+ private saveCheckpoint;
50
+ /**
51
+ * Load checkpoint from disk
52
+ */
53
+ loadCheckpoint(sessionId: string): Promise<ConversationCheckpoint | null>;
54
+ /**
55
+ * Rollback files to checkpoint state
56
+ * @param sessionId - Session ID to rollback
57
+ * @returns Number of messages to rollback to, or null if no checkpoint
58
+ */
59
+ rollback(sessionId: string): Promise<number | null>;
60
+ /**
61
+ * Clear checkpoint for a session
62
+ */
63
+ clearCheckpoint(sessionId: string): Promise<void>;
64
+ /**
65
+ * Get active checkpoint
66
+ */
67
+ getActiveCheckpoint(): ConversationCheckpoint | null;
68
+ /**
69
+ * Clear active checkpoint (used when conversation completes successfully)
70
+ */
71
+ commitCheckpoint(): Promise<void>;
72
+ }
73
+ export declare const checkpointManager: CheckpointManager;
74
+ export {};
@@ -0,0 +1,181 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ /**
5
+ * Checkpoint Manager
6
+ * Manages file snapshots for rollback on ESC interrupt
7
+ */
8
+ class CheckpointManager {
9
+ constructor() {
10
+ Object.defineProperty(this, "checkpointsDir", {
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true,
14
+ value: void 0
15
+ });
16
+ Object.defineProperty(this, "activeCheckpoint", {
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ value: null
21
+ });
22
+ this.checkpointsDir = path.join(os.homedir(), '.snow', 'checkpoints');
23
+ }
24
+ /**
25
+ * Ensure checkpoints directory exists
26
+ */
27
+ async ensureCheckpointsDir() {
28
+ try {
29
+ await fs.mkdir(this.checkpointsDir, { recursive: true });
30
+ }
31
+ catch (error) {
32
+ // Directory already exists or other error
33
+ }
34
+ }
35
+ /**
36
+ * Get checkpoint file path for a session
37
+ */
38
+ getCheckpointPath(sessionId) {
39
+ return path.join(this.checkpointsDir, `${sessionId}.json`);
40
+ }
41
+ /**
42
+ * Create a new checkpoint before AI response
43
+ * @param sessionId - Current session ID
44
+ * @param messageCount - Number of messages before AI response
45
+ */
46
+ async createCheckpoint(sessionId, messageCount) {
47
+ await this.ensureCheckpointsDir();
48
+ this.activeCheckpoint = {
49
+ sessionId,
50
+ messageCount,
51
+ fileSnapshots: [],
52
+ timestamp: Date.now()
53
+ };
54
+ // Save checkpoint immediately (will be updated as files are modified)
55
+ await this.saveCheckpoint();
56
+ }
57
+ /**
58
+ * Record a file snapshot before modification
59
+ * @param filePath - Absolute path to the file
60
+ */
61
+ async recordFileSnapshot(filePath) {
62
+ if (!this.activeCheckpoint) {
63
+ return; // No active checkpoint, skip
64
+ }
65
+ // Check if this file already has a snapshot
66
+ const existingSnapshot = this.activeCheckpoint.fileSnapshots.find(snapshot => snapshot.path === filePath);
67
+ if (existingSnapshot) {
68
+ return; // Already recorded, skip
69
+ }
70
+ try {
71
+ // Try to read existing file content
72
+ const content = await fs.readFile(filePath, 'utf-8');
73
+ this.activeCheckpoint.fileSnapshots.push({
74
+ path: filePath,
75
+ content,
76
+ timestamp: Date.now(),
77
+ exists: true
78
+ });
79
+ }
80
+ catch (error) {
81
+ // File doesn't exist, record as non-existent
82
+ this.activeCheckpoint.fileSnapshots.push({
83
+ path: filePath,
84
+ content: '',
85
+ timestamp: Date.now(),
86
+ exists: false
87
+ });
88
+ }
89
+ // Update checkpoint file
90
+ await this.saveCheckpoint();
91
+ }
92
+ /**
93
+ * Save current checkpoint to disk
94
+ */
95
+ async saveCheckpoint() {
96
+ if (!this.activeCheckpoint) {
97
+ return;
98
+ }
99
+ await this.ensureCheckpointsDir();
100
+ const checkpointPath = this.getCheckpointPath(this.activeCheckpoint.sessionId);
101
+ await fs.writeFile(checkpointPath, JSON.stringify(this.activeCheckpoint, null, 2));
102
+ }
103
+ /**
104
+ * Load checkpoint from disk
105
+ */
106
+ async loadCheckpoint(sessionId) {
107
+ try {
108
+ const checkpointPath = this.getCheckpointPath(sessionId);
109
+ const data = await fs.readFile(checkpointPath, 'utf-8');
110
+ return JSON.parse(data);
111
+ }
112
+ catch (error) {
113
+ return null;
114
+ }
115
+ }
116
+ /**
117
+ * Rollback files to checkpoint state
118
+ * @param sessionId - Session ID to rollback
119
+ * @returns Number of messages to rollback to, or null if no checkpoint
120
+ */
121
+ async rollback(sessionId) {
122
+ const checkpoint = await this.loadCheckpoint(sessionId);
123
+ if (!checkpoint) {
124
+ return null;
125
+ }
126
+ // Rollback all file snapshots
127
+ for (const snapshot of checkpoint.fileSnapshots) {
128
+ try {
129
+ if (snapshot.exists) {
130
+ // Restore original file content
131
+ await fs.writeFile(snapshot.path, snapshot.content, 'utf-8');
132
+ }
133
+ else {
134
+ // Delete file that was created
135
+ try {
136
+ await fs.unlink(snapshot.path);
137
+ }
138
+ catch (error) {
139
+ // File may already be deleted, ignore
140
+ }
141
+ }
142
+ }
143
+ catch (error) {
144
+ console.error(`Failed to rollback file ${snapshot.path}:`, error);
145
+ }
146
+ }
147
+ // Clear checkpoint after rollback
148
+ await this.clearCheckpoint(sessionId);
149
+ return checkpoint.messageCount;
150
+ }
151
+ /**
152
+ * Clear checkpoint for a session
153
+ */
154
+ async clearCheckpoint(sessionId) {
155
+ try {
156
+ const checkpointPath = this.getCheckpointPath(sessionId);
157
+ await fs.unlink(checkpointPath);
158
+ }
159
+ catch (error) {
160
+ // Checkpoint may not exist, ignore
161
+ }
162
+ if (this.activeCheckpoint?.sessionId === sessionId) {
163
+ this.activeCheckpoint = null;
164
+ }
165
+ }
166
+ /**
167
+ * Get active checkpoint
168
+ */
169
+ getActiveCheckpoint() {
170
+ return this.activeCheckpoint;
171
+ }
172
+ /**
173
+ * Clear active checkpoint (used when conversation completes successfully)
174
+ */
175
+ async commitCheckpoint() {
176
+ if (this.activeCheckpoint) {
177
+ await this.clearCheckpoint(this.activeCheckpoint.sessionId);
178
+ }
179
+ }
180
+ }
181
+ export const checkpointManager = new CheckpointManager();