agentgui 1.0.38 → 1.0.40
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/CLAUDE.md +0 -0
- package/acp-launcher.js +14 -51
- package/database.js +64 -0
- package/package.json +1 -1
- package/server.js +128 -46
- package/state-validator.js +150 -0
- package/static/sync-manager.js +273 -0
- package/stream-handler.js +101 -0
- package/01-initial-load.png +0 -0
- package/AUTOMATIC_IMPORT.md +0 -284
- package/CONVERSATION_DISPLAY_FIX.md +0 -125
- package/DEBUG_GUIDE.md +0 -136
- package/DIAGNOSTICS.md +0 -61
- package/FINAL_SUMMARY.md +0 -312
- package/IMPLEMENTATION_CHECKLIST.md +0 -287
- package/IMPLEMENTATION_STATUS.md +0 -189
- package/README.md +0 -213
- package/RECENT_UPDATES.md +0 -209
- package/REMOTE_DEBUG_GUIDE.md +0 -225
- package/RESPONSE_ISSUES.md +0 -157
- package/SIDEBAR_FIX_SUMMARY.md +0 -111
- package/STATE_CONSISTENCY_GUARANTEE.md +0 -183
- package/STATE_CONSISTENCY_TEST_INDEX.md +0 -268
- package/STATE_CONSISTENCY_TEST_REPORT.md +0 -256
- package/STATE_MACHINE_SUMMARY.md +0 -172
- package/TEST_README.md +0 -205
- package/TEST_SUMMARY.md +0 -159
- package/test-artifacts/01-window-a-initial.png +0 -0
- package/test-artifacts/01-window-b-initial.png +0 -0
- package/test-artifacts/02-window-a-after-send.png +0 -0
- package/test-artifacts/02-window-b-after-send.png +0 -0
- package/test-artifacts/snapshot-a-1.txt +0 -1
- package/test-artifacts/snapshot-b-1.txt +0 -1
- package/test-state-consistency.cjs +0 -239
- package/test-state-manager.js +0 -55
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Manager - Handles real-time synchronization with automatic reconnection
|
|
3
|
+
* Guarantees: No lost data, perfect recovery, consistent state
|
|
4
|
+
*/
|
|
5
|
+
class SyncManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.ws = null;
|
|
8
|
+
this.clientId = null;
|
|
9
|
+
this.subscriptions = new Map();
|
|
10
|
+
this.reconnectAttempts = 0;
|
|
11
|
+
this.maxReconnectAttempts = 10;
|
|
12
|
+
this.reconnectDelay = 1000;
|
|
13
|
+
this.isConnected = false;
|
|
14
|
+
this.handlers = new Map();
|
|
15
|
+
this.lastCheckpoint = new Map();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Connect to sync server with automatic reconnection
|
|
20
|
+
*/
|
|
21
|
+
connect() {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
24
|
+
const url = `${protocol}//${window.location.host}${window.__BASE_URL || '/gm'}/sync`;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
this.ws = new WebSocket(url);
|
|
28
|
+
|
|
29
|
+
this.ws.onopen = () => {
|
|
30
|
+
console.log('[SyncManager] Connected to server');
|
|
31
|
+
this.isConnected = true;
|
|
32
|
+
this.reconnectAttempts = 0;
|
|
33
|
+
this.emit('connected', { clientId: this.clientId });
|
|
34
|
+
|
|
35
|
+
// Resubscribe to all previously subscribed sessions
|
|
36
|
+
for (const [sessionId, handlers] of this.subscriptions) {
|
|
37
|
+
this.subscribe(sessionId, handlers.onUpdate, handlers.onRecover);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
resolve();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
this.ws.onmessage = (event) => {
|
|
44
|
+
this.handleMessage(JSON.parse(event.data));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
this.ws.onclose = () => {
|
|
48
|
+
console.log('[SyncManager] Disconnected from server');
|
|
49
|
+
this.isConnected = false;
|
|
50
|
+
this.attemptReconnect();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
this.ws.onerror = (error) => {
|
|
54
|
+
console.error('[SyncManager] WebSocket error:', error);
|
|
55
|
+
reject(error);
|
|
56
|
+
};
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error('[SyncManager] Failed to create WebSocket:', err);
|
|
59
|
+
reject(err);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Handle incoming messages
|
|
66
|
+
*/
|
|
67
|
+
handleMessage(message) {
|
|
68
|
+
const { type, sessionId, clientId } = message;
|
|
69
|
+
|
|
70
|
+
if (type === 'sync_connected') {
|
|
71
|
+
this.clientId = message.clientId;
|
|
72
|
+
console.log(`[SyncManager] Assigned client ID: ${this.clientId}`);
|
|
73
|
+
} else if (type === 'state_snapshot') {
|
|
74
|
+
// Received state after subscription
|
|
75
|
+
console.log(`[SyncManager] Received state snapshot for ${sessionId}`);
|
|
76
|
+
this.lastCheckpoint.set(sessionId, message.state.checkpoint);
|
|
77
|
+
|
|
78
|
+
const handlers = this.subscriptions.get(sessionId);
|
|
79
|
+
if (handlers?.onRecover) {
|
|
80
|
+
handlers.onRecover(message.state);
|
|
81
|
+
}
|
|
82
|
+
} else if (type === 'recovery_response') {
|
|
83
|
+
// Received full state recovery
|
|
84
|
+
console.log(`[SyncManager] Received recovery response for ${sessionId}`);
|
|
85
|
+
this.lastCheckpoint.set(sessionId, message.state.checkpoint);
|
|
86
|
+
|
|
87
|
+
const handlers = this.subscriptions.get(sessionId);
|
|
88
|
+
if (handlers?.onRecover) {
|
|
89
|
+
handlers.onRecover(message.state);
|
|
90
|
+
}
|
|
91
|
+
} else if (type === 'stream_update') {
|
|
92
|
+
// Real-time update from server
|
|
93
|
+
this.lastCheckpoint.set(sessionId, message.timestamp);
|
|
94
|
+
|
|
95
|
+
const handlers = this.subscriptions.get(sessionId);
|
|
96
|
+
if (handlers?.onUpdate) {
|
|
97
|
+
try {
|
|
98
|
+
handlers.onUpdate(message);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(`[SyncManager] Error in update handler: ${err.message}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Subscribe to session updates with callbacks
|
|
108
|
+
* @param {string} sessionId
|
|
109
|
+
* @param {Function} onUpdate - Called for each real-time update
|
|
110
|
+
* @param {Function} onRecover - Called with full state on subscribe/reconnect
|
|
111
|
+
*/
|
|
112
|
+
subscribe(sessionId, onUpdate, onRecover) {
|
|
113
|
+
if (!this.subscriptions.has(sessionId)) {
|
|
114
|
+
this.subscriptions.set(sessionId, { onUpdate, onRecover });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
|
|
118
|
+
this.ws.send(JSON.stringify({
|
|
119
|
+
type: 'subscribe',
|
|
120
|
+
sessionId
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Unsubscribe from session
|
|
127
|
+
*/
|
|
128
|
+
unsubscribe(sessionId) {
|
|
129
|
+
this.subscriptions.delete(sessionId);
|
|
130
|
+
this.lastCheckpoint.delete(sessionId);
|
|
131
|
+
|
|
132
|
+
if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
|
|
133
|
+
this.ws.send(JSON.stringify({
|
|
134
|
+
type: 'unsubscribe',
|
|
135
|
+
sessionId
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Request recovery from a specific checkpoint
|
|
142
|
+
* Called when client detects missing data
|
|
143
|
+
*/
|
|
144
|
+
requestRecovery(sessionId) {
|
|
145
|
+
console.log(`[SyncManager] Requesting recovery for ${sessionId}`);
|
|
146
|
+
|
|
147
|
+
if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
|
|
148
|
+
this.ws.send(JSON.stringify({
|
|
149
|
+
type: 'recovery_request',
|
|
150
|
+
sessionId
|
|
151
|
+
}));
|
|
152
|
+
} else {
|
|
153
|
+
// If not connected, recover when connection is restored
|
|
154
|
+
this.connect().then(() => {
|
|
155
|
+
this.ws.send(JSON.stringify({
|
|
156
|
+
type: 'recovery_request',
|
|
157
|
+
sessionId
|
|
158
|
+
}));
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Verify data consistency by querying server
|
|
165
|
+
*/
|
|
166
|
+
async validateSession(sessionId) {
|
|
167
|
+
const baseUrl = window.__BASE_URL || '/gm';
|
|
168
|
+
try {
|
|
169
|
+
const response = await fetch(`${baseUrl}/api/sessions/${sessionId}/validate`);
|
|
170
|
+
const validation = await response.json();
|
|
171
|
+
return validation;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error(`[SyncManager] Validation failed: ${err.message}`);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Fetch full state for recovery
|
|
180
|
+
*/
|
|
181
|
+
async fetchSessionState(sessionId) {
|
|
182
|
+
const baseUrl = window.__BASE_URL || '/gm';
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch(`${baseUrl}/api/sessions/${sessionId}/state-recovery`);
|
|
185
|
+
if (!response.ok) return null;
|
|
186
|
+
return await response.json();
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.error(`[SyncManager] Failed to fetch session state: ${err.message}`);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Automatic reconnection with exponential backoff
|
|
195
|
+
*/
|
|
196
|
+
attemptReconnect() {
|
|
197
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
198
|
+
console.error('[SyncManager] Max reconnection attempts reached');
|
|
199
|
+
this.emit('reconnect_failed');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.reconnectAttempts++;
|
|
204
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
205
|
+
console.log(`[SyncManager] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
206
|
+
|
|
207
|
+
setTimeout(() => {
|
|
208
|
+
this.connect().catch(err => {
|
|
209
|
+
console.error('[SyncManager] Reconnection failed:', err);
|
|
210
|
+
this.attemptReconnect();
|
|
211
|
+
});
|
|
212
|
+
}, delay);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Detect missing updates by checking sequence gaps
|
|
217
|
+
*/
|
|
218
|
+
detectMissingUpdates(updates) {
|
|
219
|
+
const gaps = [];
|
|
220
|
+
for (let i = 0; i < updates.length - 1; i++) {
|
|
221
|
+
if (updates[i + 1].sequence !== updates[i].sequence + 1) {
|
|
222
|
+
gaps.push({
|
|
223
|
+
expected: updates[i].sequence + 1,
|
|
224
|
+
actual: updates[i + 1].sequence
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return gaps;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Register event listener
|
|
233
|
+
*/
|
|
234
|
+
on(event, callback) {
|
|
235
|
+
if (!this.handlers.has(event)) {
|
|
236
|
+
this.handlers.set(event, []);
|
|
237
|
+
}
|
|
238
|
+
this.handlers.get(event).push(callback);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Emit event
|
|
243
|
+
*/
|
|
244
|
+
emit(event, data) {
|
|
245
|
+
const callbacks = this.handlers.get(event) || [];
|
|
246
|
+
for (const callback of callbacks) {
|
|
247
|
+
try {
|
|
248
|
+
callback(data);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error(`[SyncManager] Error in ${event} handler: ${err.message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Close connection gracefully
|
|
257
|
+
*/
|
|
258
|
+
disconnect() {
|
|
259
|
+
this.subscriptions.clear();
|
|
260
|
+
this.lastCheckpoint.clear();
|
|
261
|
+
if (this.ws) {
|
|
262
|
+
this.ws.close();
|
|
263
|
+
this.ws = null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Export as global for browser use
|
|
269
|
+
if (typeof window !== 'undefined') {
|
|
270
|
+
window.SyncManager = SyncManager;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export default SyncManager;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { queries } from './database.js';
|
|
2
|
+
import { StateValidator } from './state-validator.js';
|
|
3
|
+
|
|
4
|
+
export class StreamHandler {
|
|
5
|
+
constructor(sessionId, conversationId, broadcastFn) {
|
|
6
|
+
this.sessionId = sessionId;
|
|
7
|
+
this.conversationId = conversationId;
|
|
8
|
+
this.broadcastFn = broadcastFn;
|
|
9
|
+
this.updateCount = 0;
|
|
10
|
+
this.sequence = -1;
|
|
11
|
+
this.hasText = false;
|
|
12
|
+
this.hasBlocks = false;
|
|
13
|
+
this.blocks = [];
|
|
14
|
+
this.stateCheckpoint = StateValidator.getSessionState(sessionId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
handleUpdate(params, baseUrl) {
|
|
18
|
+
const u = params.update;
|
|
19
|
+
if (!u) return;
|
|
20
|
+
|
|
21
|
+
const kind = u.sessionUpdate;
|
|
22
|
+
if (kind === 'agent_message_chunk' && u.content?.text) {
|
|
23
|
+
this.hasText = true;
|
|
24
|
+
const update = {
|
|
25
|
+
type: 'text',
|
|
26
|
+
content: u.content.text,
|
|
27
|
+
timestamp: Date.now()
|
|
28
|
+
};
|
|
29
|
+
this.persistAndBroadcast('text', update, baseUrl);
|
|
30
|
+
} else if (kind === 'html_content' && u.content?.html) {
|
|
31
|
+
this.hasBlocks = true;
|
|
32
|
+
const update = {
|
|
33
|
+
type: 'html',
|
|
34
|
+
html: u.content.html,
|
|
35
|
+
title: u.content.title,
|
|
36
|
+
id: u.content.id,
|
|
37
|
+
timestamp: Date.now()
|
|
38
|
+
};
|
|
39
|
+
this.blocks.push({ type: 'html', html: u.content.html, title: u.content.title, id: u.content.id });
|
|
40
|
+
this.persistAndBroadcast('html', update, baseUrl);
|
|
41
|
+
} else if (kind === 'image_content' && u.content?.path) {
|
|
42
|
+
this.hasBlocks = true;
|
|
43
|
+
const imageUrl = baseUrl + '/api/image/' + encodeURIComponent(u.content.path);
|
|
44
|
+
const update = {
|
|
45
|
+
type: 'image',
|
|
46
|
+
path: u.content.path,
|
|
47
|
+
url: imageUrl,
|
|
48
|
+
title: u.content.title,
|
|
49
|
+
alt: u.content.alt,
|
|
50
|
+
timestamp: Date.now()
|
|
51
|
+
};
|
|
52
|
+
this.blocks.push({ type: 'image', path: u.content.path, url: imageUrl, title: u.content.title, alt: u.content.alt });
|
|
53
|
+
this.persistAndBroadcast('image', update, baseUrl);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
persistAndBroadcast(updateType, update, baseUrl) {
|
|
58
|
+
try {
|
|
59
|
+
// CRITICAL: Database write MUST complete before broadcast
|
|
60
|
+
// This guarantees database is source of truth
|
|
61
|
+
const persistedUpdate = queries.createStreamUpdate(this.sessionId, this.conversationId, updateType, update);
|
|
62
|
+
this.sequence = persistedUpdate.sequence;
|
|
63
|
+
this.updateCount++;
|
|
64
|
+
|
|
65
|
+
// Validate consistency after write
|
|
66
|
+
const validation = StateValidator.validateSession(this.sessionId);
|
|
67
|
+
if (!validation.valid) {
|
|
68
|
+
console.error(`[StreamHandler] State validation failed after update:`, validation);
|
|
69
|
+
// Log but continue - database is still source of truth
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// CRITICAL: Broadcast happens AFTER database write confirms
|
|
73
|
+
// This ensures clients see data that's already persisted
|
|
74
|
+
this.broadcastFn({
|
|
75
|
+
type: 'stream_update',
|
|
76
|
+
sessionId: this.sessionId,
|
|
77
|
+
conversationId: this.conversationId,
|
|
78
|
+
updateType,
|
|
79
|
+
update: persistedUpdate.content,
|
|
80
|
+
sequence: this.sequence,
|
|
81
|
+
persisted: true,
|
|
82
|
+
timestamp: persistedUpdate.created_at,
|
|
83
|
+
validation: validation.valid ? undefined : { error: validation.error }
|
|
84
|
+
});
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error(`[StreamHandler] Error persisting update: ${err.message}`);
|
|
87
|
+
// On persistence failure, do NOT broadcast - maintain consistency
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getBlocks() {
|
|
93
|
+
return this.blocks;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getUpdateCount() {
|
|
97
|
+
return this.updateCount;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default StreamHandler;
|
package/01-initial-load.png
DELETED
|
Binary file
|
package/AUTOMATIC_IMPORT.md
DELETED
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
# Automatic Continuous Importing Feature
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
AgentGUI now automatically and continuously imports Claude Code conversations **without requiring any user action**. This ensures conversations are always available and up-to-date.
|
|
5
|
-
|
|
6
|
-
## How It Works
|
|
7
|
-
|
|
8
|
-
### Server-Side (Every 30 Seconds)
|
|
9
|
-
```
|
|
10
|
-
Server startup
|
|
11
|
-
↓
|
|
12
|
-
[IMMEDIATE] Import Claude Code conversations (first run)
|
|
13
|
-
↓
|
|
14
|
-
[EVERY 30 SECONDS] Auto-import new conversations
|
|
15
|
-
↓
|
|
16
|
-
If new conversations found:
|
|
17
|
-
• Add them to database
|
|
18
|
-
• Broadcast 'conversations_updated' event to all connected clients
|
|
19
|
-
• Log: "[AUTO-IMPORT] Imported X new Claude Code conversations"
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
### Frontend-Side
|
|
23
|
-
```
|
|
24
|
-
Page loads
|
|
25
|
-
↓
|
|
26
|
-
[IMMEDIATE] Fetch conversations from API
|
|
27
|
-
↓
|
|
28
|
-
[EVERY 10 SECONDS] Fetch conversations again (as fallback)
|
|
29
|
-
↓
|
|
30
|
-
[ON SYNC EVENT] Receive 'conversations_updated' from server
|
|
31
|
-
↓
|
|
32
|
-
Immediately refresh conversation list
|
|
33
|
-
↓
|
|
34
|
-
Users see new conversations appear in real-time
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Key Features
|
|
38
|
-
|
|
39
|
-
✅ **Automatic**: No user action needed
|
|
40
|
-
✅ **Continuous**: Runs every 30 seconds (server) and 10 seconds (client)
|
|
41
|
-
✅ **Real-time**: New conversations appear immediately via WebSocket
|
|
42
|
-
✅ **No Duplicates**: Skips conversations already imported
|
|
43
|
-
✅ **Cross-tab**: Broadcasts via BroadcastChannel API
|
|
44
|
-
✅ **Resilient**: Fallback mechanism if WebSocket fails
|
|
45
|
-
✅ **Logging**: All imports logged for debugging
|
|
46
|
-
|
|
47
|
-
## Where Conversations Come From
|
|
48
|
-
|
|
49
|
-
### Automatically Discovered From:
|
|
50
|
-
1. **Claude Code Projects** (~/.claude/projects/)
|
|
51
|
-
- Scans sessions-index.json files
|
|
52
|
-
- Reads .jsonl message files
|
|
53
|
-
- Imports with "[project] title" format
|
|
54
|
-
|
|
55
|
-
2. **Created in AgentGUI**
|
|
56
|
-
- New conversations created via UI
|
|
57
|
-
- Automatically stored in database
|
|
58
|
-
|
|
59
|
-
## Example Flow
|
|
60
|
-
|
|
61
|
-
### Scenario: User Uses Claude Code, Then Opens AgentGUI
|
|
62
|
-
|
|
63
|
-
```
|
|
64
|
-
11:00:00 - User creates conversation in Claude Code
|
|
65
|
-
11:00:15 - AgentGUI server detects new conversation in ~/.claude/projects/
|
|
66
|
-
11:00:20 - Server imports conversation automatically
|
|
67
|
-
11:00:20 - Server broadcasts 'conversations_updated' event
|
|
68
|
-
11:00:21 - All connected browser tabs receive update
|
|
69
|
-
11:00:21 - Users see new conversation appear in sidebar
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Scenario: Multiple Tabs Open
|
|
73
|
-
|
|
74
|
-
```
|
|
75
|
-
Tab 1 opens AgentGUI
|
|
76
|
-
Tab 2 opens AgentGUI (few seconds later)
|
|
77
|
-
|
|
78
|
-
Tab 1 receives 'conversations_updated' from server
|
|
79
|
-
Tab 1 uses BroadcastChannel to notify Tab 2
|
|
80
|
-
Tab 2 also refreshes conversation list
|
|
81
|
-
Both tabs show latest conversations in sync
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## Server Logs
|
|
85
|
-
|
|
86
|
-
You'll see logs like:
|
|
87
|
-
```
|
|
88
|
-
[AUTO-IMPORT] Imported 2 new Claude Code conversations (42 already exist)
|
|
89
|
-
[AUTO-IMPORT] Imported 1 new Claude Code conversation (43 already exist)
|
|
90
|
-
[AUTO-IMPORT] (nothing new this cycle)
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Client Logs
|
|
94
|
-
|
|
95
|
-
In browser console:
|
|
96
|
-
```
|
|
97
|
-
[SYNC] Server imported 3 new conversations, refreshing...
|
|
98
|
-
[DEBUG] Init: Auto-imported Claude Code conversations
|
|
99
|
-
[DEBUG] Loaded conversations, total: 86
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## Configuration
|
|
103
|
-
|
|
104
|
-
### Import Frequency (Server)
|
|
105
|
-
Current: **30 seconds**
|
|
106
|
-
Location: `server.js` line `setInterval(performAutoImport, 30000);`
|
|
107
|
-
|
|
108
|
-
To change:
|
|
109
|
-
```javascript
|
|
110
|
-
setInterval(performAutoImport, 60000); // 60 seconds
|
|
111
|
-
setInterval(performAutoImport, 5000); // 5 seconds
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### Refresh Frequency (Client)
|
|
115
|
-
Current: **10 seconds**
|
|
116
|
-
Location: `app.js` line `setInterval(() => { ... }, 10000);`
|
|
117
|
-
|
|
118
|
-
To change:
|
|
119
|
-
```javascript
|
|
120
|
-
}, 60000); // 60 seconds
|
|
121
|
-
}, 5000); // 5 seconds
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
## Data Flow Diagram
|
|
125
|
-
|
|
126
|
-
```
|
|
127
|
-
┌─────────────────────────────────────────────────────┐
|
|
128
|
-
│ Claude Code │
|
|
129
|
-
│ ~/.claude/projects/*/sessions-index.json │
|
|
130
|
-
└─────────────────┬───────────────────────────────────┘
|
|
131
|
-
│ (monitors every 30s)
|
|
132
|
-
↓
|
|
133
|
-
┌─────────────────────────────────────────────────────┐
|
|
134
|
-
│ AgentGUI Server │
|
|
135
|
-
│ • queries.importClaudeCodeConversations() │
|
|
136
|
-
│ • Stores in ~/.gmgui/data.db │
|
|
137
|
-
│ • Broadcasts via WebSocket │
|
|
138
|
-
└──────┬──────────────────────────┬────────────────────┘
|
|
139
|
-
│ │
|
|
140
|
-
│ (WebSocket event) │ (HTTP API)
|
|
141
|
-
↓ ↓
|
|
142
|
-
┌─────────────────────────────────────────────────────┐
|
|
143
|
-
│ Browser (Frontend) │
|
|
144
|
-
│ • Listens on sync WebSocket │
|
|
145
|
-
│ • Receives 'conversations_updated' event │
|
|
146
|
-
│ • Calls fetchConversations() │
|
|
147
|
-
│ • Calls renderChatHistory() │
|
|
148
|
-
└──────┬──────────────────────────────────────────────┘
|
|
149
|
-
│
|
|
150
|
-
↓
|
|
151
|
-
┌─────────────────────────────────────────────────────┐
|
|
152
|
-
│ Chat Sidebar │
|
|
153
|
-
│ • Displays conversation list │
|
|
154
|
-
│ • User can click to view conversation │
|
|
155
|
-
└─────────────────────────────────────────────────────┘
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
## Troubleshooting
|
|
159
|
-
|
|
160
|
-
### Conversations Not Appearing
|
|
161
|
-
|
|
162
|
-
**Check 1: Server Auto-Import**
|
|
163
|
-
```bash
|
|
164
|
-
# Look for these logs in server output
|
|
165
|
-
grep "\[AUTO-IMPORT\]" server.log
|
|
166
|
-
|
|
167
|
-
# If no logs, auto-import might not be running
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
**Check 2: Claude Code Availability**
|
|
171
|
-
```bash
|
|
172
|
-
# Check if Claude Code projects exist
|
|
173
|
-
ls -la ~/.claude/projects/
|
|
174
|
-
|
|
175
|
-
# Count projects with conversations
|
|
176
|
-
find ~/.claude/projects -name "sessions-index.json" | wc -l
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
**Check 3: Browser Sync**
|
|
180
|
-
```javascript
|
|
181
|
-
// In browser console
|
|
182
|
-
// Check if WebSocket is connected
|
|
183
|
-
console.log('WebSocket state:', app.syncWs.ws?.readyState);
|
|
184
|
-
|
|
185
|
-
// Try manual refresh
|
|
186
|
-
await app.fetchConversations();
|
|
187
|
-
app.renderChatHistory();
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
**Check 4: Database**
|
|
191
|
-
```bash
|
|
192
|
-
# Check total conversations in DB
|
|
193
|
-
node -e "
|
|
194
|
-
const DB = require('better-sqlite3');
|
|
195
|
-
const db = new DB(process.env.HOME + '/.gmgui/data.db');
|
|
196
|
-
const count = db.prepare('SELECT COUNT(*) as c FROM conversations').get();
|
|
197
|
-
console.log('Database has:', count.c, 'conversations');
|
|
198
|
-
db.close();
|
|
199
|
-
"
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### Too Many Import Logs
|
|
203
|
-
|
|
204
|
-
If server logs are too verbose:
|
|
205
|
-
1. Increase `setInterval` time (30000 → 60000 or more)
|
|
206
|
-
2. Add log level filtering
|
|
207
|
-
|
|
208
|
-
### Conversations Take Too Long to Appear
|
|
209
|
-
|
|
210
|
-
If new conversations take > 1 minute:
|
|
211
|
-
1. Check server auto-import interval (default 30s)
|
|
212
|
-
2. Check client refresh interval (default 10s)
|
|
213
|
-
3. Check network connectivity
|
|
214
|
-
4. Check browser console for errors
|
|
215
|
-
|
|
216
|
-
## Performance Considerations
|
|
217
|
-
|
|
218
|
-
### Import Impact
|
|
219
|
-
- **Minimal**: Import skips existing conversations
|
|
220
|
-
- **Fast**: Only processes new conversations
|
|
221
|
-
- **Efficient**: Uses database transactions
|
|
222
|
-
|
|
223
|
-
### Client Impact
|
|
224
|
-
- **WebSocket**: Real-time updates, low bandwidth
|
|
225
|
-
- **Polling**: Every 10 seconds, minimal traffic
|
|
226
|
-
- **Rendering**: Only updates when conversations change
|
|
227
|
-
|
|
228
|
-
## Security Notes
|
|
229
|
-
|
|
230
|
-
- Only imports from user's local `.claude/projects/`
|
|
231
|
-
- No external network access needed
|
|
232
|
-
- All conversations stored locally
|
|
233
|
-
- Respects filesystem permissions
|
|
234
|
-
|
|
235
|
-
## Future Enhancements
|
|
236
|
-
|
|
237
|
-
Potential improvements:
|
|
238
|
-
- Configurable import interval via UI
|
|
239
|
-
- Import from multiple sources
|
|
240
|
-
- Batch import optimization
|
|
241
|
-
- Import history/logs viewer
|
|
242
|
-
- Import statistics dashboard
|
|
243
|
-
- Per-project import settings
|
|
244
|
-
|
|
245
|
-
## Testing
|
|
246
|
-
|
|
247
|
-
### Manual Test: Add Claude Code Conversation
|
|
248
|
-
```bash
|
|
249
|
-
# 1. Use Claude Code (creates ~/.claude/projects/*/sessions-index.json)
|
|
250
|
-
# 2. Wait up to 30 seconds
|
|
251
|
-
# 3. Check browser - should see new conversation appear
|
|
252
|
-
# 4. Confirm console logs show "[AUTO-IMPORT] Imported X..."
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
### Manual Test: Force Import
|
|
256
|
-
```javascript
|
|
257
|
-
// In browser console
|
|
258
|
-
await fetch('/gm/api/import/claude-code')
|
|
259
|
-
.then(r => r.json())
|
|
260
|
-
.then(d => console.log('Manual import result:', d));
|
|
261
|
-
|
|
262
|
-
// Then refresh
|
|
263
|
-
await app.fetchConversations();
|
|
264
|
-
app.renderChatHistory();
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
## Related Files
|
|
268
|
-
|
|
269
|
-
- `server.js` - Server auto-import implementation
|
|
270
|
-
- `app.js` - Frontend sync handling
|
|
271
|
-
- `database.js` - Query functions
|
|
272
|
-
- `acp-launcher.js` - Claude Code connection
|
|
273
|
-
|
|
274
|
-
## Changelog
|
|
275
|
-
|
|
276
|
-
### Version 1.1.0 (Current)
|
|
277
|
-
- ✅ Added automatic server-side importing every 30 seconds
|
|
278
|
-
- ✅ Added WebSocket broadcast for instant updates
|
|
279
|
-
- ✅ Added client-side sync event handler
|
|
280
|
-
- ✅ Integrated with existing periodic sync
|
|
281
|
-
|
|
282
|
-
### Version 1.0.0 (Previous)
|
|
283
|
-
- Manual import on demand via `/api/import/claude-code`
|
|
284
|
-
- No automatic background importing
|