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.
@@ -155,11 +155,15 @@ class AgentGUIClient {
155
155
 
156
156
  /**
157
157
  * Router state management: restore conversation from URL
158
- * Format: ?conversation=<id>&session=<id>
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
- const params = new URLSearchParams();
197
- params.set('conversation', conversationId);
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
- params.set('session', sessionId);
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-title">${this.escapeHtml(title)}</div>
237
- <div class="conversation-item-meta">${metaParts.join(' \u2022 ')}</div>
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
 
@@ -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;
@@ -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;