agentgui 1.0.109 → 1.0.111
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/.prd +271 -70
- package/database.js +87 -0
- package/package.json +2 -2
- package/server.js +25 -2
- package/static/index.html +48 -0
- package/static/js/client.js +14 -7
- package/static/js/conversations.js +41 -2
- package/conversation-importer.js +0 -63
- package/hot-reload-manager.js +0 -186
- package/lib/database-service.ts +0 -640
- package/lib/machines.ts +0 -190
- package/lib/schemas.ts +0 -190
- package/lib/sync-service.ts +0 -615
- package/lib/types.ts +0 -413
package/static/js/client.js
CHANGED
|
@@ -155,11 +155,15 @@ class AgentGUIClient {
|
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
157
|
* Router state management: restore conversation from URL
|
|
158
|
-
* Format:
|
|
158
|
+
* Format: /conversations/<conversationId>?session=<sessionId>
|
|
159
159
|
*/
|
|
160
160
|
restoreStateFromUrl() {
|
|
161
|
+
// Parse path-based URL: /conversations/<conversationId>
|
|
162
|
+
const pathMatch = window.location.pathname.match(/\/conversations\/([^\/]+)$/);
|
|
163
|
+
const conversationId = pathMatch ? pathMatch[1] : null;
|
|
164
|
+
|
|
165
|
+
// Session ID still in query params
|
|
161
166
|
const params = new URLSearchParams(window.location.search);
|
|
162
|
-
const conversationId = params.get('conversation');
|
|
163
167
|
const sessionId = params.get('session');
|
|
164
168
|
|
|
165
169
|
if (conversationId && this.isValidId(conversationId)) {
|
|
@@ -184,6 +188,7 @@ class AgentGUIClient {
|
|
|
184
188
|
/**
|
|
185
189
|
* Update URL when conversation is selected
|
|
186
190
|
* Uses History API (pushState) for clean URLs
|
|
191
|
+
* Format: /conversations/<conversationId>?session=<sessionId>
|
|
187
192
|
*/
|
|
188
193
|
updateUrlForConversation(conversationId, sessionId) {
|
|
189
194
|
if (!this.isValidId(conversationId)) return;
|
|
@@ -193,13 +198,15 @@ class AgentGUIClient {
|
|
|
193
198
|
this.routerState.currentSessionId = sessionId;
|
|
194
199
|
}
|
|
195
200
|
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
// Use path-based URL for conversation
|
|
202
|
+
const basePath = window.location.pathname.replace(/\/conversations\/[^\/]+$/, '').replace(/\/$/, '');
|
|
203
|
+
let url = `${basePath}/conversations/${conversationId}`;
|
|
204
|
+
|
|
205
|
+
// Session ID still in query params for optional state
|
|
198
206
|
if (sessionId && this.isValidId(sessionId)) {
|
|
199
|
-
|
|
207
|
+
url += `?session=${sessionId}`;
|
|
200
208
|
}
|
|
201
|
-
|
|
202
|
-
const url = `${window.location.pathname}?${params.toString()}`;
|
|
209
|
+
|
|
203
210
|
window.history.pushState({ conversationId, sessionId }, '', url);
|
|
204
211
|
}
|
|
205
212
|
|
|
@@ -233,14 +233,53 @@ class ConversationManager {
|
|
|
233
233
|
if (wd) metaParts.push(wd);
|
|
234
234
|
|
|
235
235
|
li.innerHTML = `
|
|
236
|
-
<div class="conversation-item-
|
|
237
|
-
|
|
236
|
+
<div class="conversation-item-content">
|
|
237
|
+
<div class="conversation-item-title">${this.escapeHtml(title)}</div>
|
|
238
|
+
<div class="conversation-item-meta">${metaParts.join(' • ')}</div>
|
|
239
|
+
</div>
|
|
240
|
+
<button class="conversation-item-delete" title="Delete conversation" data-delete-conv="${conv.id}">
|
|
241
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
242
|
+
<polyline points="3 6 5 6 21 6"></polyline>
|
|
243
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
|
244
|
+
</svg>
|
|
245
|
+
</button>
|
|
238
246
|
`;
|
|
239
247
|
|
|
248
|
+
// Handle delete button click
|
|
249
|
+
const deleteBtn = li.querySelector('[data-delete-conv]');
|
|
250
|
+
deleteBtn.addEventListener('click', (e) => {
|
|
251
|
+
e.stopPropagation();
|
|
252
|
+
this.confirmDelete(conv.id, title);
|
|
253
|
+
});
|
|
254
|
+
|
|
240
255
|
li.addEventListener('click', () => this.select(conv.id));
|
|
241
256
|
return li;
|
|
242
257
|
}
|
|
243
258
|
|
|
259
|
+
async confirmDelete(convId, title) {
|
|
260
|
+
const confirmed = confirm(`Delete conversation "${title || 'Untitled'}"?\n\nThis will also delete any associated Claude Code session data. This action cannot be undone.`);
|
|
261
|
+
if (!confirmed) return;
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const res = await fetch((window.__BASE_URL || '') + `/api/conversations/${convId}`, {
|
|
265
|
+
method: 'DELETE',
|
|
266
|
+
headers: { 'Content-Type': 'application/json' }
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (res.ok) {
|
|
270
|
+
console.log(`[ConversationManager] Deleted conversation ${convId}`);
|
|
271
|
+
// Remove from local list immediately for responsive UI
|
|
272
|
+
this.deleteConversation(convId);
|
|
273
|
+
} else {
|
|
274
|
+
const error = await res.json().catch(() => ({ error: 'Failed to delete' }));
|
|
275
|
+
alert('Failed to delete conversation: ' + (error.error || 'Unknown error'));
|
|
276
|
+
}
|
|
277
|
+
} catch (err) {
|
|
278
|
+
console.error('[ConversationManager] Delete error:', err);
|
|
279
|
+
alert('Failed to delete conversation: ' + err.message);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
244
283
|
select(convId) {
|
|
245
284
|
this.activeId = convId;
|
|
246
285
|
|
package/conversation-importer.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import { queries } from './database.js';
|
|
5
|
-
|
|
6
|
-
export class ConversationImporter {
|
|
7
|
-
static async importClaudeCodeSessions() {
|
|
8
|
-
const projectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
9
|
-
if (!fs.existsSync(projectsDir)) return [];
|
|
10
|
-
|
|
11
|
-
const imported = [];
|
|
12
|
-
const projects = fs.readdirSync(projectsDir);
|
|
13
|
-
|
|
14
|
-
for (const projectName of projects) {
|
|
15
|
-
const indexPath = path.join(projectsDir, projectName, 'sessions-index.json');
|
|
16
|
-
if (!fs.existsSync(indexPath)) continue;
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
|
|
20
|
-
const entries = index.entries || [];
|
|
21
|
-
|
|
22
|
-
for (const entry of entries) {
|
|
23
|
-
try {
|
|
24
|
-
const existing = queries.getConversationByExternalId('claude-code', entry.sessionId);
|
|
25
|
-
if (existing) continue;
|
|
26
|
-
|
|
27
|
-
const conversation = {
|
|
28
|
-
externalId: entry.sessionId,
|
|
29
|
-
agentType: 'claude-code',
|
|
30
|
-
title: entry.summary || entry.firstPrompt || `Conversation ${entry.sessionId.slice(0, 8)}`,
|
|
31
|
-
firstPrompt: entry.firstPrompt,
|
|
32
|
-
messageCount: entry.messageCount || 0,
|
|
33
|
-
created: new Date(entry.created).getTime(),
|
|
34
|
-
modified: new Date(entry.modified).getTime(),
|
|
35
|
-
projectPath: entry.projectPath,
|
|
36
|
-
gitBranch: entry.gitBranch,
|
|
37
|
-
sourcePath: entry.fullPath,
|
|
38
|
-
source: 'imported'
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
queries.createImportedConversation(conversation);
|
|
42
|
-
imported.push(conversation);
|
|
43
|
-
} catch (err) {
|
|
44
|
-
console.error(`[Importer] Error importing session ${entry.sessionId}:`, err.message);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
} catch (err) {
|
|
48
|
-
console.error(`[Importer] Error reading ${indexPath}:`, err.message);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return imported;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
static async importAll() {
|
|
56
|
-
console.log('[Importer] Starting conversation import...');
|
|
57
|
-
const claudeCode = await this.importClaudeCodeSessions();
|
|
58
|
-
console.log(`[Importer] Imported ${claudeCode.length} Claude Code conversations`);
|
|
59
|
-
return claudeCode;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export default ConversationImporter;
|
package/hot-reload-manager.js
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hot Reload Manager
|
|
3
|
-
* Enables live reloading of client code and graceful server restarts without losing connections
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import fs from 'fs';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
import { fileURLToPath } from 'url';
|
|
9
|
-
|
|
10
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
|
|
12
|
-
export class HotReloadManager {
|
|
13
|
-
constructor(staticDir, options = {}) {
|
|
14
|
-
this.staticDir = staticDir;
|
|
15
|
-
this.watchedFiles = new Map();
|
|
16
|
-
this.hotReloadClients = [];
|
|
17
|
-
this.debounceTimers = new Map();
|
|
18
|
-
this.debounceDelay = options.debounceDelay || 300;
|
|
19
|
-
this.enabled = options.enabled !== false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Start watching files for changes
|
|
24
|
-
*/
|
|
25
|
-
start() {
|
|
26
|
-
if (!this.enabled) return;
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
this.watchDirectory(this.staticDir);
|
|
30
|
-
console.log('[HotReload] Watching for changes:', this.staticDir);
|
|
31
|
-
} catch (e) {
|
|
32
|
-
console.error('[HotReload] Failed to start:', e.message);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Watch directory recursively
|
|
38
|
-
*/
|
|
39
|
-
watchDirectory(dir) {
|
|
40
|
-
try {
|
|
41
|
-
const files = fs.readdirSync(dir, { withFileTypes: true });
|
|
42
|
-
|
|
43
|
-
for (const file of files) {
|
|
44
|
-
const fullPath = path.join(dir, file.name);
|
|
45
|
-
|
|
46
|
-
if (file.isDirectory()) {
|
|
47
|
-
this.watchDirectory(fullPath);
|
|
48
|
-
} else {
|
|
49
|
-
this.watchFile(fullPath);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
} catch (e) {
|
|
53
|
-
console.error('[HotReload] Error watching directory:', e.message);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Watch individual file for changes
|
|
59
|
-
*/
|
|
60
|
-
watchFile(filePath) {
|
|
61
|
-
if (this.watchedFiles.has(filePath)) return;
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
fs.watchFile(filePath, { interval: 100 }, (curr, prev) => {
|
|
65
|
-
if (curr.mtime > prev.mtime) {
|
|
66
|
-
this.onFileChanged(filePath);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
this.watchedFiles.set(filePath, true);
|
|
71
|
-
} catch (e) {
|
|
72
|
-
console.error('[HotReload] Error watching file:', e.message);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Handle file change with debouncing
|
|
78
|
-
*/
|
|
79
|
-
onFileChanged(filePath) {
|
|
80
|
-
// Clear existing timer for this file
|
|
81
|
-
if (this.debounceTimers.has(filePath)) {
|
|
82
|
-
clearTimeout(this.debounceTimers.get(filePath));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Set new debounced timer
|
|
86
|
-
const timer = setTimeout(() => {
|
|
87
|
-
this.debounceTimers.delete(filePath);
|
|
88
|
-
const relPath = path.relative(this.staticDir, filePath);
|
|
89
|
-
|
|
90
|
-
console.log(`[HotReload] File changed: ${relPath}`);
|
|
91
|
-
this.broadcastReload();
|
|
92
|
-
}, this.debounceDelay);
|
|
93
|
-
|
|
94
|
-
this.debounceTimers.set(filePath, timer);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Register a WebSocket client for hot reload
|
|
99
|
-
*/
|
|
100
|
-
registerClient(ws) {
|
|
101
|
-
if (!this.enabled) return;
|
|
102
|
-
this.hotReloadClients.push(ws);
|
|
103
|
-
|
|
104
|
-
ws.on('close', () => {
|
|
105
|
-
const idx = this.hotReloadClients.indexOf(ws);
|
|
106
|
-
if (idx > -1) this.hotReloadClients.splice(idx, 1);
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Broadcast reload signal to all connected clients
|
|
112
|
-
*/
|
|
113
|
-
broadcastReload() {
|
|
114
|
-
const message = JSON.stringify({ type: 'reload', timestamp: Date.now() });
|
|
115
|
-
|
|
116
|
-
for (const ws of this.hotReloadClients) {
|
|
117
|
-
if (ws.readyState === 1) { // WebSocket.OPEN
|
|
118
|
-
try {
|
|
119
|
-
ws.send(message);
|
|
120
|
-
} catch (e) {
|
|
121
|
-
// Client may have disconnected
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Cleanup watchers
|
|
129
|
-
*/
|
|
130
|
-
stop() {
|
|
131
|
-
for (const filePath of this.watchedFiles.keys()) {
|
|
132
|
-
try {
|
|
133
|
-
fs.unwatchFile(filePath);
|
|
134
|
-
} catch (e) {}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
for (const timer of this.debounceTimers.values()) {
|
|
138
|
-
clearTimeout(timer);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
this.watchedFiles.clear();
|
|
142
|
-
this.debounceTimers.clear();
|
|
143
|
-
this.hotReloadClients = [];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get HTML snippet to inject for hot reload
|
|
148
|
-
*/
|
|
149
|
-
getClientScript(baseUrl = '') {
|
|
150
|
-
if (!this.enabled) return '';
|
|
151
|
-
|
|
152
|
-
return `
|
|
153
|
-
<script>
|
|
154
|
-
(function() {
|
|
155
|
-
const baseUrl = '${baseUrl}';
|
|
156
|
-
const ws = new WebSocket((location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + location.host + baseUrl + '/hot-reload');
|
|
157
|
-
|
|
158
|
-
ws.onmessage = function(event) {
|
|
159
|
-
try {
|
|
160
|
-
const data = JSON.parse(event.data);
|
|
161
|
-
if (data.type === 'reload') {
|
|
162
|
-
console.log('[HotReload] Reloading page...');
|
|
163
|
-
location.reload();
|
|
164
|
-
}
|
|
165
|
-
} catch (e) {
|
|
166
|
-
console.error('[HotReload] Error parsing message:', e);
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
ws.onerror = function(e) {
|
|
171
|
-
console.log('[HotReload] WebSocket error:', e);
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
ws.onclose = function() {
|
|
175
|
-
console.log('[HotReload] Connection closed, will attempt to reconnect...');
|
|
176
|
-
setTimeout(function() {
|
|
177
|
-
location.reload();
|
|
178
|
-
}, 2000);
|
|
179
|
-
};
|
|
180
|
-
})();
|
|
181
|
-
</script>
|
|
182
|
-
`;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export default HotReloadManager;
|