@myrialabs/clopen 0.1.4 → 0.1.6

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.
Files changed (54) hide show
  1. package/backend/lib/chat/stream-manager.ts +8 -0
  2. package/backend/lib/database/migrations/022_add_snapshot_changes_column.ts +35 -0
  3. package/backend/lib/database/migrations/index.ts +7 -0
  4. package/backend/lib/database/queries/snapshot-queries.ts +7 -4
  5. package/backend/lib/files/file-watcher.ts +34 -0
  6. package/backend/lib/project/status-manager.ts +6 -4
  7. package/backend/lib/snapshot/snapshot-service.ts +471 -316
  8. package/backend/lib/terminal/pty-session-manager.ts +1 -32
  9. package/backend/ws/chat/stream.ts +45 -2
  10. package/backend/ws/snapshot/restore.ts +77 -67
  11. package/backend/ws/system/operations.ts +95 -0
  12. package/frontend/App.svelte +24 -7
  13. package/frontend/lib/components/chat/ChatInterface.svelte +14 -14
  14. package/frontend/lib/components/chat/input/ChatInput.svelte +2 -2
  15. package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +1 -1
  16. package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +8 -3
  17. package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +12 -2
  18. package/frontend/lib/components/chat/tools/AskUserQuestionTool.svelte +3 -8
  19. package/frontend/lib/components/checkpoint/TimelineModal.svelte +222 -30
  20. package/frontend/lib/components/common/ConnectionBanner.svelte +55 -0
  21. package/frontend/lib/components/common/MonacoEditor.svelte +14 -0
  22. package/frontend/lib/components/common/UpdateBanner.svelte +88 -0
  23. package/frontend/lib/components/common/xterm/XTerm.svelte +9 -0
  24. package/frontend/lib/components/common/xterm/xterm-service.ts +9 -0
  25. package/frontend/lib/components/git/DiffViewer.svelte +16 -2
  26. package/frontend/lib/components/history/HistoryModal.svelte +8 -4
  27. package/frontend/lib/components/settings/SettingsModal.svelte +2 -2
  28. package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +59 -0
  29. package/frontend/lib/components/settings/general/GeneralSettings.svelte +6 -1
  30. package/frontend/lib/components/settings/general/UpdateSettings.svelte +123 -0
  31. package/frontend/lib/components/terminal/Terminal.svelte +1 -7
  32. package/frontend/lib/components/workspace/DesktopNavigator.svelte +11 -19
  33. package/frontend/lib/components/workspace/MobileNavigator.svelte +4 -15
  34. package/frontend/lib/components/workspace/WorkspaceLayout.svelte +1 -1
  35. package/frontend/lib/components/workspace/panels/FilesPanel.svelte +3 -2
  36. package/frontend/lib/components/workspace/panels/GitPanel.svelte +3 -2
  37. package/frontend/lib/services/notification/global-stream-monitor.ts +56 -16
  38. package/frontend/lib/services/snapshot/snapshot.service.ts +71 -32
  39. package/frontend/lib/stores/core/app.svelte.ts +47 -0
  40. package/frontend/lib/stores/core/presence.svelte.ts +80 -1
  41. package/frontend/lib/stores/core/projects.svelte.ts +10 -2
  42. package/frontend/lib/stores/core/sessions.svelte.ts +15 -2
  43. package/frontend/lib/stores/features/settings.svelte.ts +10 -1
  44. package/frontend/lib/stores/features/terminal.svelte.ts +6 -0
  45. package/frontend/lib/stores/ui/connection.svelte.ts +40 -0
  46. package/frontend/lib/stores/ui/update.svelte.ts +124 -0
  47. package/frontend/lib/stores/ui/workspace.svelte.ts +4 -3
  48. package/frontend/lib/utils/ws.ts +5 -1
  49. package/index.html +1 -1
  50. package/package.json +1 -1
  51. package/shared/types/database/schema.ts +18 -0
  52. package/shared/types/stores/settings.ts +4 -0
  53. package/shared/utils/ws-client.ts +16 -2
  54. package/vite.config.ts +1 -0
@@ -268,6 +268,14 @@ class StreamManager extends EventEmitter {
268
268
  // Track user message ID for stream-end snapshot capture
269
269
  let userMessageId: string | undefined;
270
270
 
271
+ // Initialize session baseline for snapshot system (non-blocking)
272
+ if (requestData.projectPath && requestData.chatSessionId) {
273
+ snapshotService.initializeSessionBaseline(
274
+ requestData.projectPath,
275
+ requestData.chatSessionId
276
+ ).catch(err => debug.error('snapshot', 'Failed to initialize session baseline:', err));
277
+ }
278
+
271
279
  try {
272
280
  const { projectPath, prompt, chatSessionId, engine: engineType = 'claude-code', model, temperature, claudeAccountId } = requestData;
273
281
 
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Migration: Add session_changes column to message_snapshots
3
+ * Purpose: Store session-scoped file changes (old/new hash per file)
4
+ * instead of full project state. This enables:
5
+ * - Session-scoped restore (only undo changes within the session)
6
+ * - Cross-session conflict detection
7
+ * - Dramatically reduced storage (only changed files stored)
8
+ *
9
+ * session_changes JSON format:
10
+ * {
11
+ * "filepath": { "oldHash": "sha256...", "newHash": "sha256..." },
12
+ * ...
13
+ * }
14
+ */
15
+
16
+ import type { DatabaseConnection } from '$shared/types/database/connection';
17
+ import { debug } from '$shared/utils/logger';
18
+
19
+ export const description = 'Add session_changes for session-scoped snapshot deltas';
20
+
21
+ export const up = (db: DatabaseConnection): void => {
22
+ debug.log('migration', 'Adding session_changes column to message_snapshots...');
23
+
24
+ db.exec(`
25
+ ALTER TABLE message_snapshots
26
+ ADD COLUMN session_changes TEXT
27
+ `);
28
+
29
+ debug.log('migration', 'session_changes column added');
30
+ };
31
+
32
+ export const down = (db: DatabaseConnection): void => {
33
+ debug.log('migration', 'Removing session_changes column...');
34
+ debug.warn('migration', 'Rollback not implemented for session_changes (SQLite limitation)');
35
+ };
@@ -20,6 +20,7 @@ import * as migration018 from './018_create_claude_accounts_table';
20
20
  import * as migration019 from './019_add_claude_account_to_sessions';
21
21
  import * as migration020 from './020_add_snapshot_tree_hash';
22
22
  import * as migration021 from './021_drop_prompt_templates_table';
23
+ import * as migration022 from './022_add_snapshot_changes_column';
23
24
 
24
25
  // Export all migrations in order
25
26
  export const migrations = [
@@ -148,6 +149,12 @@ export const migrations = [
148
149
  description: migration021.description,
149
150
  up: migration021.up,
150
151
  down: migration021.down
152
+ },
153
+ {
154
+ id: '022',
155
+ description: migration022.description,
156
+ up: migration022.up,
157
+ down: migration022.down
151
158
  }
152
159
  ];
153
160
 
@@ -27,6 +27,7 @@ export const snapshotQueries = {
27
27
  deletions?: number;
28
28
  branch_id?: string;
29
29
  tree_hash?: string; // Blob store tree hash (new format)
30
+ session_changes?: any; // SessionScopedChanges object
30
31
  }): MessageSnapshot {
31
32
  const db = getDatabase();
32
33
  const id = data.id || `snapshot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
@@ -48,7 +49,8 @@ export const snapshotQueries = {
48
49
  deletions: data.deletions || 0,
49
50
  is_deleted: 0,
50
51
  branch_id: data.branch_id || null,
51
- tree_hash: data.tree_hash || null
52
+ tree_hash: data.tree_hash || null,
53
+ session_changes: data.session_changes ? JSON.stringify(data.session_changes) : null
52
54
  };
53
55
 
54
56
  db.prepare(`
@@ -57,8 +59,8 @@ export const snapshotQueries = {
57
59
  files_snapshot, project_metadata, created_at,
58
60
  snapshot_type, parent_snapshot_id, delta_changes,
59
61
  files_changed, insertions, deletions,
60
- is_deleted, branch_id, tree_hash
61
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
62
+ is_deleted, branch_id, tree_hash, session_changes
63
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?)
62
64
  `).run(
63
65
  snapshot.id,
64
66
  snapshot.message_id,
@@ -74,7 +76,8 @@ export const snapshotQueries = {
74
76
  snapshot.insertions,
75
77
  snapshot.deletions,
76
78
  snapshot.branch_id || null,
77
- snapshot.tree_hash || null
79
+ snapshot.tree_hash || null,
80
+ snapshot.session_changes || null
78
81
  );
79
82
 
80
83
  return snapshot;
@@ -82,6 +82,36 @@ interface ProjectWatcher {
82
82
  class FileWatcherManager {
83
83
  private watchers = new Map<string, ProjectWatcher>();
84
84
 
85
+ /**
86
+ * Per-project dirty file tracking for snapshot system.
87
+ * Accumulates changed file relative paths between snapshot captures.
88
+ */
89
+ private dirtyFiles = new Map<string, Set<string>>();
90
+
91
+ /**
92
+ * Get dirty files accumulated since last clear for a project.
93
+ */
94
+ getDirtyFiles(projectId: string): Set<string> {
95
+ return this.dirtyFiles.get(projectId) || new Set();
96
+ }
97
+
98
+ /**
99
+ * Clear dirty files after snapshot capture.
100
+ */
101
+ clearDirtyFiles(projectId: string): void {
102
+ this.dirtyFiles.delete(projectId);
103
+ }
104
+
105
+ /**
106
+ * Track a file as dirty for snapshot purposes.
107
+ */
108
+ private trackDirtyFile(projectId: string, relativePath: string): void {
109
+ if (!this.dirtyFiles.has(projectId)) {
110
+ this.dirtyFiles.set(projectId, new Set());
111
+ }
112
+ this.dirtyFiles.get(projectId)!.add(relativePath);
113
+ }
114
+
85
115
  /**
86
116
  * Start watching a project directory
87
117
  */
@@ -238,6 +268,10 @@ class FileWatcherManager {
238
268
  }
239
269
  }
240
270
 
271
+ // Track dirty file for snapshot system
272
+ const relativePath = relative(projectPath, fullPath).replace(/\\/g, '/');
273
+ this.trackDirtyFile(projectId, relativePath);
274
+
241
275
  // Create file change object
242
276
  const fileChange: FileChange = {
243
277
  path: fullPath,
@@ -18,11 +18,13 @@ const INTERACTIVE_TOOLS = new Set(['AskUserQuestion']);
18
18
  function detectStreamWaitingInput(stream: StreamState): boolean {
19
19
  if (stream.status !== 'active') return false;
20
20
 
21
+ // SSEEventData.message is SDKMessage: { type, message: { content: [...] } }
22
+ // Content blocks live at msg.message.content, NOT msg.content
21
23
  const answeredToolIds = new Set<string>();
22
24
  for (const event of stream.messages) {
23
25
  const msg = event.message;
24
- if (!msg || (msg as any).type !== 'user' || !(msg as any).content) continue;
25
- const content = Array.isArray((msg as any).content) ? (msg as any).content : [];
26
+ if (!msg || (msg as any).type !== 'user') continue;
27
+ const content = Array.isArray((msg as any).message?.content) ? (msg as any).message.content : [];
26
28
  for (const item of content) {
27
29
  if (item.type === 'tool_result' && item.tool_use_id) {
28
30
  answeredToolIds.add(item.tool_use_id);
@@ -32,8 +34,8 @@ function detectStreamWaitingInput(stream: StreamState): boolean {
32
34
 
33
35
  for (const event of stream.messages) {
34
36
  const msg = event.message;
35
- if (!msg || (msg as any).type !== 'assistant' || !(msg as any).content) continue;
36
- const content = Array.isArray((msg as any).content) ? (msg as any).content : [];
37
+ if (!msg || (msg as any).type !== 'assistant') continue;
38
+ const content = Array.isArray((msg as any).message?.content) ? (msg as any).message.content : [];
37
39
  if (content.some((item: any) =>
38
40
  item.type === 'tool_use' && INTERACTIVE_TOOLS.has(item.name) && item.id && !answeredToolIds.has(item.id)
39
41
  )) {