agentgui 1.0.65 → 1.0.67

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/static/styles.css CHANGED
@@ -1690,3 +1690,138 @@ p code {
1690
1690
  border-radius: 4px;
1691
1691
  line-height: 1.6;
1692
1692
  }
1693
+
1694
+ /* Claude Execution Blocks - Full message structure rendering */
1695
+ .execution-blocks {
1696
+ display: flex;
1697
+ flex-direction: column;
1698
+ gap: 0.75rem;
1699
+ width: 100%;
1700
+ }
1701
+
1702
+ .message-block {
1703
+ padding: 0.75rem 1rem;
1704
+ border-radius: 0.5rem;
1705
+ background: var(--bg-secondary);
1706
+ border: 1px solid var(--border-color);
1707
+ word-wrap: break-word;
1708
+ }
1709
+
1710
+ .block-text {
1711
+ color: var(--text-primary);
1712
+ white-space: pre-wrap;
1713
+ line-height: 1.5;
1714
+ }
1715
+
1716
+ .block-tool-use {
1717
+ background: rgba(59, 130, 246, 0.05);
1718
+ border: 1px solid rgba(59, 130, 246, 0.2);
1719
+ border-radius: 0.5rem;
1720
+ padding: 0.75rem;
1721
+ }
1722
+
1723
+ .tool-name {
1724
+ color: var(--color-info);
1725
+ font-weight: 600;
1726
+ display: block;
1727
+ margin-bottom: 0.5rem;
1728
+ }
1729
+
1730
+ .tool-input {
1731
+ background: var(--bg-tertiary);
1732
+ border-radius: 0.375rem;
1733
+ padding: 0.5rem;
1734
+ overflow-x: auto;
1735
+ font-size: 0.8rem;
1736
+ font-family: 'Courier New', monospace;
1737
+ }
1738
+
1739
+ .tool-input pre {
1740
+ margin: 0;
1741
+ color: var(--text-secondary);
1742
+ }
1743
+
1744
+ .block-tool-result {
1745
+ background: rgba(16, 185, 129, 0.05);
1746
+ border: 1px solid rgba(16, 185, 129, 0.2);
1747
+ border-radius: 0.5rem;
1748
+ padding: 0.75rem;
1749
+ }
1750
+
1751
+ .block-tool-result strong {
1752
+ color: var(--color-success);
1753
+ display: block;
1754
+ margin-bottom: 0.5rem;
1755
+ }
1756
+
1757
+ .tool-result {
1758
+ background: var(--bg-tertiary);
1759
+ border-radius: 0.375rem;
1760
+ padding: 0.5rem;
1761
+ overflow-x: auto;
1762
+ font-size: 0.8rem;
1763
+ font-family: 'Courier New', monospace;
1764
+ max-height: 300px;
1765
+ overflow-y: auto;
1766
+ }
1767
+
1768
+ .tool-result pre {
1769
+ margin: 0;
1770
+ color: var(--text-secondary);
1771
+ }
1772
+
1773
+ .block-file-op {
1774
+ background: rgba(245, 158, 11, 0.05);
1775
+ border: 1px solid rgba(245, 158, 11, 0.2);
1776
+ border-radius: 0.5rem;
1777
+ padding: 0.75rem;
1778
+ }
1779
+
1780
+ .file-action {
1781
+ color: var(--color-warning);
1782
+ font-weight: 600;
1783
+ display: block;
1784
+ margin-bottom: 0.5rem;
1785
+ }
1786
+
1787
+ .file-path {
1788
+ color: var(--text-secondary);
1789
+ font-size: 0.875rem;
1790
+ font-family: 'Courier New', monospace;
1791
+ margin-bottom: 0.5rem;
1792
+ padding: 0.25rem 0.5rem;
1793
+ background: var(--bg-tertiary);
1794
+ border-radius: 0.25rem;
1795
+ }
1796
+
1797
+ .file-content {
1798
+ background: var(--bg-tertiary);
1799
+ border-radius: 0.375rem;
1800
+ padding: 0.5rem;
1801
+ overflow-x: auto;
1802
+ font-size: 0.8rem;
1803
+ font-family: 'Courier New', monospace;
1804
+ max-height: 200px;
1805
+ overflow-y: auto;
1806
+ }
1807
+
1808
+ .file-content pre {
1809
+ margin: 0;
1810
+ color: var(--text-secondary);
1811
+ }
1812
+
1813
+ .block-unknown {
1814
+ background: var(--bg-tertiary);
1815
+ border-radius: 0.375rem;
1816
+ padding: 0.5rem;
1817
+ overflow-x: auto;
1818
+ font-size: 0.75rem;
1819
+ font-family: 'Courier New', monospace;
1820
+ }
1821
+
1822
+ .message-content {
1823
+ color: var(--text-primary);
1824
+ white-space: pre-wrap;
1825
+ word-wrap: break-word;
1826
+ line-height: 1.5;
1827
+ }
@@ -1,196 +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
- /**
7
- * ConversationSync - Watches for changes to Claude Code conversation files
8
- * and keeps the database synchronized with the latest versions
9
- */
10
- export class ConversationSync {
11
- constructor() {
12
- this.watchers = new Map();
13
- this.lastSync = new Map();
14
- this.syncInterval = 5000; // Check for changes every 5 seconds
15
- this.isRunning = false;
16
- }
17
-
18
- /**
19
- * Start watching for conversation file changes
20
- */
21
- start() {
22
- if (this.isRunning) return;
23
- this.isRunning = true;
24
-
25
- // Watch for changes to sessions-index.json files
26
- this.watchProjectDirectory();
27
-
28
- // Periodic sync check
29
- this.syncCheckInterval = setInterval(() => {
30
- this.checkForUpdates();
31
- }, this.syncInterval);
32
-
33
- console.log('[ConversationSync] Started watching for conversation changes');
34
- }
35
-
36
- /**
37
- * Stop watching for changes
38
- */
39
- stop() {
40
- if (!this.isRunning) return;
41
- this.isRunning = false;
42
-
43
- // Clear all watchers
44
- for (const [, watcher] of this.watchers) {
45
- watcher.close();
46
- }
47
- this.watchers.clear();
48
-
49
- // Clear interval
50
- if (this.syncCheckInterval) {
51
- clearInterval(this.syncCheckInterval);
52
- }
53
-
54
- console.log('[ConversationSync] Stopped watching for conversation changes');
55
- }
56
-
57
- /**
58
- * Watch the .claude/projects directory for changes
59
- */
60
- watchProjectDirectory() {
61
- const projectsDir = path.join(os.homedir(), '.claude', 'projects');
62
- if (!fs.existsSync(projectsDir)) {
63
- console.log('[ConversationSync] Projects directory does not exist:', projectsDir);
64
- return;
65
- }
66
-
67
- try {
68
- const watcher = fs.watch(projectsDir, { recursive: true }, (eventType, filename) => {
69
- if (filename && filename.endsWith('sessions-index.json')) {
70
- this.handleFileChange(projectsDir, filename);
71
- }
72
- });
73
-
74
- this.watchers.set(projectsDir, watcher);
75
- console.log('[ConversationSync] Watching:', projectsDir);
76
- } catch (err) {
77
- console.error('[ConversationSync] Error setting up file watcher:', err.message);
78
- }
79
- }
80
-
81
- /**
82
- * Handle changes to sessions-index.json files
83
- */
84
- handleFileChange(projectsDir, filename) {
85
- const fullPath = path.join(projectsDir, filename);
86
-
87
- // Debounce rapid changes
88
- if (this.lastSync.has(fullPath)) {
89
- const lastTime = this.lastSync.get(fullPath);
90
- if (Date.now() - lastTime < 1000) {
91
- return; // Skip if changed within last second
92
- }
93
- }
94
-
95
- this.lastSync.set(fullPath, Date.now());
96
- this.syncConversationFile(fullPath);
97
- }
98
-
99
- /**
100
- * Sync a specific sessions-index.json file
101
- */
102
- syncConversationFile(indexPath) {
103
- try {
104
- if (!fs.existsSync(indexPath)) return;
105
-
106
- const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
107
- const entries = index.entries || [];
108
- let synced = 0;
109
- let updated = 0;
110
-
111
- for (const entry of entries) {
112
- const existing = queries.getConversationByExternalId('claude-code', entry.sessionId);
113
-
114
- if (existing) {
115
- // Check if conversation has been updated
116
- const existingModified = new Date(existing.modified).getTime();
117
- const newModified = new Date(entry.modified).getTime();
118
-
119
- if (newModified > existingModified) {
120
- // Update with new information
121
- queries.updateConversation(existing.id, {
122
- title: entry.summary || entry.firstPrompt || `Conversation ${entry.sessionId.slice(0, 8)}`,
123
- messageCount: entry.messageCount || 0,
124
- modified: newModified
125
- });
126
- updated++;
127
- }
128
- } else {
129
- // New conversation - import it
130
- const conversation = {
131
- externalId: entry.sessionId,
132
- agentType: 'claude-code',
133
- title: entry.summary || entry.firstPrompt || `Conversation ${entry.sessionId.slice(0, 8)}`,
134
- firstPrompt: entry.firstPrompt,
135
- messageCount: entry.messageCount || 0,
136
- created: new Date(entry.created).getTime(),
137
- modified: new Date(entry.modified).getTime(),
138
- projectPath: entry.projectPath,
139
- gitBranch: entry.gitBranch,
140
- sourcePath: entry.fullPath,
141
- source: 'imported'
142
- };
143
-
144
- queries.createImportedConversation(conversation);
145
- synced++;
146
- }
147
- }
148
-
149
- if (synced > 0 || updated > 0) {
150
- console.log(`[ConversationSync] File: ${path.basename(indexPath)} - synced: ${synced}, updated: ${updated}`);
151
- }
152
- } catch (err) {
153
- console.error('[ConversationSync] Error syncing file:', indexPath, err.message);
154
- }
155
- }
156
-
157
- /**
158
- * Periodic check for any missed updates
159
- */
160
- checkForUpdates() {
161
- try {
162
- const projectsDir = path.join(os.homedir(), '.claude', 'projects');
163
- if (!fs.existsSync(projectsDir)) return;
164
-
165
- const projects = fs.readdirSync(projectsDir);
166
-
167
- for (const projectName of projects) {
168
- const indexPath = path.join(projectsDir, projectName, 'sessions-index.json');
169
- if (fs.existsSync(indexPath)) {
170
- // Check file's modification time
171
- const stat = fs.statSync(indexPath);
172
- const lastModified = stat.mtime.getTime();
173
- const lastSyncTime = this.lastSync.get(indexPath) || 0;
174
-
175
- if (lastModified > lastSyncTime) {
176
- this.syncConversationFile(indexPath);
177
- }
178
- }
179
- }
180
- } catch (err) {
181
- console.error('[ConversationSync] Error during periodic check:', err.message);
182
- }
183
- }
184
- }
185
-
186
- // Singleton instance
187
- let syncInstance = null;
188
-
189
- export function getConversationSync() {
190
- if (!syncInstance) {
191
- syncInstance = new ConversationSync();
192
- }
193
- return syncInstance;
194
- }
195
-
196
- export default ConversationSync;
package/state-manager.js DELETED
@@ -1,360 +0,0 @@
1
- /**
2
- * StateManager - Explicit state machine for all prompt processing
3
- * Ensures predictable, auditable state transitions with no surprises
4
- */
5
-
6
- export class StateManager {
7
- // Valid session states
8
- static STATES = {
9
- PENDING: 'pending',
10
- ACQUIRING_ACP: 'acquiring_acp',
11
- ACP_ACQUIRED: 'acp_acquired',
12
- SENDING_PROMPT: 'sending_prompt',
13
- PROCESSING: 'processing',
14
- COMPLETED: 'completed',
15
- ERROR: 'error',
16
- TIMEOUT: 'timeout',
17
- CANCELLED: 'cancelled'
18
- };
19
-
20
- // Valid state transitions - only these are allowed
21
- static VALID_TRANSITIONS = {
22
- [this.STATES.PENDING]: [
23
- this.STATES.ACQUIRING_ACP,
24
- this.STATES.CANCELLED
25
- ],
26
- [this.STATES.ACQUIRING_ACP]: [
27
- this.STATES.ACP_ACQUIRED,
28
- this.STATES.ERROR,
29
- this.STATES.TIMEOUT,
30
- this.STATES.CANCELLED
31
- ],
32
- [this.STATES.ACP_ACQUIRED]: [
33
- this.STATES.SENDING_PROMPT,
34
- this.STATES.ERROR,
35
- this.STATES.TIMEOUT,
36
- this.STATES.CANCELLED
37
- ],
38
- [this.STATES.SENDING_PROMPT]: [
39
- this.STATES.PROCESSING,
40
- this.STATES.ERROR,
41
- this.STATES.TIMEOUT,
42
- this.STATES.CANCELLED
43
- ],
44
- [this.STATES.PROCESSING]: [
45
- this.STATES.COMPLETED,
46
- this.STATES.ERROR,
47
- this.STATES.TIMEOUT,
48
- this.STATES.CANCELLED
49
- ],
50
- [this.STATES.COMPLETED]: [],
51
- [this.STATES.ERROR]: [],
52
- [this.STATES.TIMEOUT]: [],
53
- [this.STATES.CANCELLED]: []
54
- };
55
-
56
- constructor(sessionId, conversationId, messageId, timeout = 120000) {
57
- this.sessionId = sessionId;
58
- this.conversationId = conversationId;
59
- this.messageId = messageId;
60
- this.timeout = timeout;
61
-
62
- // State tracking
63
- this.state = this.constructor.STATES.PENDING;
64
- this.previousState = null;
65
- this.stateHistory = [{ state: this.state, timestamp: Date.now(), reason: 'initialized' }];
66
-
67
- // Data tracking
68
- this.data = {
69
- acpConnectionTime: null,
70
- promptSentTime: null,
71
- responseReceivedTime: null,
72
- fullText: '',
73
- blocks: [],
74
- error: null,
75
- stackTrace: null
76
- };
77
-
78
- // Promise resolution
79
- this.promiseResolve = null;
80
- this.promiseReject = null;
81
- this.completionPromise = new Promise((resolve, reject) => {
82
- this.promiseResolve = resolve;
83
- this.promiseReject = reject;
84
- });
85
-
86
- // Start timeout
87
- this.startTimeout();
88
-
89
- console.log(`[StateManager] Session ${sessionId} initialized (timeout: ${timeout}ms)`);
90
- }
91
-
92
- /**
93
- * Transition to a new state with validation
94
- * @param {string} newState - Target state
95
- * @param {object} data - State-specific data
96
- * @throws {Error} If transition is invalid
97
- */
98
- transition(newState, data = {}) {
99
- const validTransitions = this.constructor.VALID_TRANSITIONS[this.state] || [];
100
-
101
- if (!validTransitions.includes(newState)) {
102
- const error = `Invalid state transition: ${this.state} → ${newState}. Valid: [${validTransitions.join(', ')}]`;
103
- console.error(`[StateManager] ${error}`);
104
- throw new Error(error);
105
- }
106
-
107
- this.previousState = this.state;
108
- this.state = newState;
109
-
110
- // Record transition
111
- this.stateHistory.push({
112
- state: newState,
113
- timestamp: Date.now(),
114
- reason: data.reason || 'manual transition',
115
- details: data.details || {}
116
- });
117
-
118
- // Update data
119
- Object.assign(this.data, data.data || {});
120
-
121
- // Log transition
122
- const duration = this.stateHistory.length > 1
123
- ? Date.now() - this.stateHistory[this.stateHistory.length - 2].timestamp
124
- : 0;
125
-
126
- console.log(`[StateManager] ${this.sessionId} transitioned: ${this.previousState} → ${newState} (+${duration}ms) | ${data.reason || ''}`);
127
-
128
- // Handle terminal states
129
- if (newState === this.constructor.STATES.COMPLETED) {
130
- this.completeSuccess(data.data);
131
- } else if (newState === this.constructor.STATES.ERROR) {
132
- this.completeError(data.data?.error, data.data?.stackTrace);
133
- } else if (newState === this.constructor.STATES.TIMEOUT) {
134
- this.completeError('Operation timeout', data.data?.stackTrace);
135
- } else if (newState === this.constructor.STATES.CANCELLED) {
136
- this.completeError('Operation cancelled', null);
137
- }
138
- }
139
-
140
- /**
141
- * Start timeout watchdog
142
- */
143
- startTimeout() {
144
- this.timeoutHandle = setTimeout(() => {
145
- if (![
146
- this.constructor.STATES.COMPLETED,
147
- this.constructor.STATES.ERROR,
148
- this.constructor.STATES.CANCELLED,
149
- this.constructor.STATES.TIMEOUT
150
- ].includes(this.state)) {
151
- console.error(`[StateManager] ${this.sessionId} TIMEOUT after ${this.timeout}ms in state: ${this.state}`);
152
- this.transition(this.constructor.STATES.TIMEOUT, {
153
- reason: 'timeout watchdog fired',
154
- data: { error: 'Operation exceeded timeout', timeout: this.timeout }
155
- });
156
- }
157
- }, this.timeout);
158
- }
159
-
160
- /**
161
- * Cancel the timeout
162
- */
163
- cancelTimeout() {
164
- if (this.timeoutHandle) {
165
- clearTimeout(this.timeoutHandle);
166
- this.timeoutHandle = null;
167
- }
168
- }
169
-
170
- /**
171
- * Mark as successfully completed
172
- */
173
- completeSuccess(data) {
174
- this.cancelTimeout();
175
- this.data = { ...this.data, ...data };
176
- if (this.promiseResolve) {
177
- this.promiseResolve({ state: this.state, data: this.data });
178
- }
179
- }
180
-
181
- /**
182
- * Mark as failed
183
- */
184
- completeError(error, stackTrace) {
185
- this.cancelTimeout();
186
- this.data.error = error;
187
- this.data.stackTrace = stackTrace;
188
- if (this.promiseReject) {
189
- this.promiseReject(new Error(`Session failed: ${error}`));
190
- }
191
- }
192
-
193
- /**
194
- * Get current state
195
- */
196
- getState() {
197
- return this.state;
198
- }
199
-
200
- /**
201
- * Get full state history
202
- */
203
- getHistory() {
204
- return this.stateHistory;
205
- }
206
-
207
- /**
208
- * Get human-readable summary
209
- */
210
- getSummary() {
211
- const duration = this.stateHistory[this.stateHistory.length - 1].timestamp - this.stateHistory[0].timestamp;
212
- return {
213
- sessionId: this.sessionId,
214
- conversationId: this.conversationId,
215
- messageId: this.messageId,
216
- state: this.state,
217
- previousState: this.previousState,
218
- duration: `${duration}ms`,
219
- historyLength: this.stateHistory.length,
220
- history: this.stateHistory.map(h => `${h.timestamp - this.stateHistory[0].timestamp}ms: ${h.state} (${h.reason})`),
221
- data: {
222
- fullTextLength: this.data.fullText.length,
223
- blocksCount: this.data.blocks.length,
224
- error: this.data.error,
225
- hasStackTrace: !!this.data.stackTrace
226
- }
227
- };
228
- }
229
-
230
- /**
231
- * Wait for completion
232
- */
233
- async waitForCompletion() {
234
- return this.completionPromise;
235
- }
236
-
237
- /**
238
- * Check if session is in a terminal state
239
- */
240
- isTerminal() {
241
- return [
242
- this.constructor.STATES.COMPLETED,
243
- this.constructor.STATES.ERROR,
244
- this.constructor.STATES.TIMEOUT,
245
- this.constructor.STATES.CANCELLED
246
- ].includes(this.state);
247
- }
248
-
249
- /**
250
- * Check if session is in a running state
251
- */
252
- isRunning() {
253
- return !this.isTerminal();
254
- }
255
-
256
- /**
257
- * Assert session is in specific state
258
- */
259
- assertState(expectedState) {
260
- if (this.state !== expectedState) {
261
- throw new Error(`Expected state ${expectedState}, got ${this.state}`);
262
- }
263
- }
264
-
265
- /**
266
- * Assert session can transition to state
267
- */
268
- assertCanTransition(targetState) {
269
- const validTransitions = this.constructor.VALID_TRANSITIONS[this.state] || [];
270
- if (!validTransitions.includes(targetState)) {
271
- throw new Error(`Cannot transition from ${this.state} to ${targetState}`);
272
- }
273
- }
274
- }
275
-
276
- export class SessionStateStore {
277
- constructor() {
278
- this.sessions = new Map(); // sessionId -> StateManager
279
- }
280
-
281
- create(sessionId, conversationId, messageId, timeout) {
282
- const stateManager = new StateManager(sessionId, conversationId, messageId, timeout);
283
- this.sessions.set(sessionId, stateManager);
284
- return stateManager;
285
- }
286
-
287
- get(sessionId) {
288
- return this.sessions.get(sessionId);
289
- }
290
-
291
- getOrThrow(sessionId) {
292
- const manager = this.sessions.get(sessionId);
293
- if (!manager) {
294
- throw new Error(`Session ${sessionId} not found in state store`);
295
- }
296
- return manager;
297
- }
298
-
299
- remove(sessionId) {
300
- const manager = this.sessions.get(sessionId);
301
- if (manager) {
302
- manager.cancelTimeout();
303
- this.sessions.delete(sessionId);
304
- }
305
- }
306
-
307
- getAll() {
308
- return Array.from(this.sessions.values());
309
- }
310
-
311
- getAllActive() {
312
- return this.getAll().filter(m => m.isRunning());
313
- }
314
-
315
- getAllTerminal() {
316
- return this.getAll().filter(m => m.isTerminal());
317
- }
318
-
319
- /**
320
- * Get diagnostic summary of all sessions
321
- */
322
- getDiagnostics() {
323
- const active = this.getAllActive();
324
- const terminal = this.getAllTerminal();
325
- return {
326
- timestamp: new Date().toISOString(),
327
- activeSessions: active.length,
328
- terminalSessions: terminal.length,
329
- totalSessions: this.sessions.size,
330
- active: active.map(m => ({
331
- sessionId: m.sessionId,
332
- state: m.state,
333
- uptime: Date.now() - m.stateHistory[0].timestamp
334
- })),
335
- recentTerminal: terminal.slice(-5).map(m => m.getSummary())
336
- };
337
- }
338
-
339
- /**
340
- * Cleanup old terminal sessions (older than ttl)
341
- */
342
- cleanup(ttl = 3600000) {
343
- const now = Date.now();
344
- const toDelete = [];
345
-
346
- for (const [sessionId, manager] of this.sessions) {
347
- if (manager.isTerminal()) {
348
- const age = now - manager.stateHistory[manager.stateHistory.length - 1].timestamp;
349
- if (age > ttl) {
350
- toDelete.push(sessionId);
351
- }
352
- }
353
- }
354
-
355
- toDelete.forEach(sessionId => this.remove(sessionId));
356
- if (toDelete.length > 0) {
357
- console.log(`[SessionStateStore] Cleaned up ${toDelete.length} old sessions`);
358
- }
359
- }
360
- }