@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.
- package/backend/lib/chat/stream-manager.ts +8 -0
- package/backend/lib/database/migrations/022_add_snapshot_changes_column.ts +35 -0
- package/backend/lib/database/migrations/index.ts +7 -0
- package/backend/lib/database/queries/snapshot-queries.ts +7 -4
- package/backend/lib/files/file-watcher.ts +34 -0
- package/backend/lib/project/status-manager.ts +6 -4
- package/backend/lib/snapshot/snapshot-service.ts +471 -316
- package/backend/lib/terminal/pty-session-manager.ts +1 -32
- package/backend/ws/chat/stream.ts +45 -2
- package/backend/ws/snapshot/restore.ts +77 -67
- package/backend/ws/system/operations.ts +95 -0
- package/frontend/App.svelte +24 -7
- package/frontend/lib/components/chat/ChatInterface.svelte +14 -14
- package/frontend/lib/components/chat/input/ChatInput.svelte +2 -2
- package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +1 -1
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +8 -3
- package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +12 -2
- package/frontend/lib/components/chat/tools/AskUserQuestionTool.svelte +3 -8
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +222 -30
- package/frontend/lib/components/common/ConnectionBanner.svelte +55 -0
- package/frontend/lib/components/common/MonacoEditor.svelte +14 -0
- package/frontend/lib/components/common/UpdateBanner.svelte +88 -0
- package/frontend/lib/components/common/xterm/XTerm.svelte +9 -0
- package/frontend/lib/components/common/xterm/xterm-service.ts +9 -0
- package/frontend/lib/components/git/DiffViewer.svelte +16 -2
- package/frontend/lib/components/history/HistoryModal.svelte +8 -4
- package/frontend/lib/components/settings/SettingsModal.svelte +2 -2
- package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +59 -0
- package/frontend/lib/components/settings/general/GeneralSettings.svelte +6 -1
- package/frontend/lib/components/settings/general/UpdateSettings.svelte +123 -0
- package/frontend/lib/components/terminal/Terminal.svelte +1 -7
- package/frontend/lib/components/workspace/DesktopNavigator.svelte +11 -19
- package/frontend/lib/components/workspace/MobileNavigator.svelte +4 -15
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +1 -1
- package/frontend/lib/components/workspace/panels/FilesPanel.svelte +3 -2
- package/frontend/lib/components/workspace/panels/GitPanel.svelte +3 -2
- package/frontend/lib/services/notification/global-stream-monitor.ts +56 -16
- package/frontend/lib/services/snapshot/snapshot.service.ts +71 -32
- package/frontend/lib/stores/core/app.svelte.ts +47 -0
- package/frontend/lib/stores/core/presence.svelte.ts +80 -1
- package/frontend/lib/stores/core/projects.svelte.ts +10 -2
- package/frontend/lib/stores/core/sessions.svelte.ts +15 -2
- package/frontend/lib/stores/features/settings.svelte.ts +10 -1
- package/frontend/lib/stores/features/terminal.svelte.ts +6 -0
- package/frontend/lib/stores/ui/connection.svelte.ts +40 -0
- package/frontend/lib/stores/ui/update.svelte.ts +124 -0
- package/frontend/lib/stores/ui/workspace.svelte.ts +4 -3
- package/frontend/lib/utils/ws.ts +5 -1
- package/index.html +1 -1
- package/package.json +1 -1
- package/shared/types/database/schema.ts +18 -0
- package/shared/types/stores/settings.ts +4 -0
- package/shared/utils/ws-client.ts +16 -2
- 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'
|
|
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'
|
|
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
|
)) {
|