agentgui 1.0.77 → 1.0.79

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.77",
3
+ "version": "1.0.79",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -25,7 +25,5 @@
25
25
  "better-sqlite3": "^12.6.2",
26
26
  "ws": "^8.14.2"
27
27
  },
28
- "devDependencies": {
29
- "playwright": "^1.58.1"
30
- }
28
+ "devDependencies": {}
31
29
  }
package/static/index.html CHANGED
@@ -58,6 +58,124 @@
58
58
  gap: 0;
59
59
  }
60
60
 
61
+ .layout-with-sidebar {
62
+ display: grid;
63
+ grid-template-columns: 300px 1fr;
64
+ gap: 0;
65
+ min-height: 0;
66
+ flex: 1;
67
+ overflow: hidden;
68
+ }
69
+
70
+ /* Sidebar */
71
+ .sidebar {
72
+ display: flex;
73
+ flex-direction: column;
74
+ background-color: var(--color-bg-secondary);
75
+ border-right: 1px solid var(--color-border);
76
+ overflow: hidden;
77
+ }
78
+
79
+ .sidebar-header {
80
+ padding: 1rem;
81
+ border-bottom: 1px solid var(--color-border);
82
+ display: flex;
83
+ justify-content: space-between;
84
+ align-items: center;
85
+ flex-shrink: 0;
86
+ }
87
+
88
+ .sidebar-header h2 {
89
+ margin: 0;
90
+ font-size: 0.875rem;
91
+ font-weight: 600;
92
+ text-transform: uppercase;
93
+ letter-spacing: 0.05em;
94
+ color: var(--color-text-secondary);
95
+ }
96
+
97
+ .sidebar-new-btn {
98
+ padding: 0.25rem 0.5rem;
99
+ font-size: 0.75rem;
100
+ background-color: var(--color-primary);
101
+ color: white;
102
+ border: none;
103
+ border-radius: 0.25rem;
104
+ cursor: pointer;
105
+ transition: background-color 0.2s;
106
+ }
107
+
108
+ .sidebar-new-btn:hover {
109
+ background-color: var(--color-primary-dark);
110
+ }
111
+
112
+ .sidebar-list {
113
+ flex: 1;
114
+ overflow-y: auto;
115
+ overflow-x: hidden;
116
+ list-style: none;
117
+ margin: 0;
118
+ padding: 0.5rem 0;
119
+ }
120
+
121
+ .conversation-item {
122
+ padding: 0.75rem 0.5rem;
123
+ margin: 0.25rem 0.5rem;
124
+ border-radius: 0.375rem;
125
+ cursor: pointer;
126
+ transition: background-color 0.2s, transform 0.1s;
127
+ border-left: 3px solid transparent;
128
+ user-select: none;
129
+ }
130
+
131
+ .conversation-item:hover {
132
+ background-color: var(--color-bg-primary);
133
+ }
134
+
135
+ .conversation-item.active {
136
+ background-color: var(--color-primary);
137
+ color: white;
138
+ border-left-color: var(--color-primary-dark);
139
+ }
140
+
141
+ .conversation-item-title {
142
+ font-weight: 500;
143
+ font-size: 0.875rem;
144
+ margin: 0;
145
+ white-space: nowrap;
146
+ overflow: hidden;
147
+ text-overflow: ellipsis;
148
+ }
149
+
150
+ .conversation-item-meta {
151
+ font-size: 0.75rem;
152
+ color: var(--color-text-secondary);
153
+ margin-top: 0.25rem;
154
+ white-space: nowrap;
155
+ overflow: hidden;
156
+ text-overflow: ellipsis;
157
+ }
158
+
159
+ .conversation-item.active .conversation-item-meta {
160
+ color: rgba(255, 255, 255, 0.7);
161
+ }
162
+
163
+ .sidebar-empty {
164
+ padding: 2rem 1rem;
165
+ text-align: center;
166
+ color: var(--color-text-secondary);
167
+ font-size: 0.875rem;
168
+ }
169
+
170
+ /* Main content area - flex column with full height */
171
+ .main-content {
172
+ display: flex;
173
+ flex-direction: column;
174
+ overflow: hidden;
175
+ flex: 1;
176
+ min-height: 0;
177
+ }
178
+
61
179
  header {
62
180
  background-color: var(--color-bg-secondary);
63
181
  border-bottom: 1px solid var(--color-border);
@@ -66,6 +184,8 @@
66
184
  justify-content: space-between;
67
185
  align-items: center;
68
186
  gap: 1rem;
187
+ flex-shrink: 0;
188
+ height: 60px;
69
189
  }
70
190
 
71
191
  .header-title {
@@ -79,6 +199,7 @@
79
199
  display: flex;
80
200
  gap: 1rem;
81
201
  align-items: center;
202
+ flex-shrink: 0;
82
203
  }
83
204
 
84
205
  .status-badge {
@@ -120,24 +241,34 @@
120
241
  }
121
242
  }
122
243
 
123
- /* Main content */
124
- main {
125
- display: grid;
126
- grid-template-columns: 1fr;
127
- grid-template-rows: auto 1fr;
128
- gap: 0;
244
+ /* Output area - scrollable, fills available space */
245
+ #output-scroll {
246
+ flex: 1;
247
+ overflow-y: auto;
248
+ overflow-x: hidden;
249
+ display: flex;
250
+ flex-direction: column;
129
251
  min-height: 0;
130
- overflow: hidden;
252
+ padding: 1rem;
131
253
  }
132
254
 
255
+ #output {
256
+ display: flex;
257
+ flex-direction: column;
258
+ gap: 0.5rem;
259
+ }
260
+
261
+ /* Execution panel - sticky at bottom */
133
262
  .execution-panel {
134
263
  background-color: var(--color-bg-secondary);
135
- border-bottom: 1px solid var(--color-border);
264
+ border-top: 1px solid var(--color-border);
136
265
  padding: 1.5rem;
137
266
  display: grid;
138
267
  grid-template-columns: 1fr auto;
139
268
  gap: 1rem;
140
269
  align-items: end;
270
+ flex-shrink: 0;
271
+ min-height: auto;
141
272
  }
142
273
 
143
274
  .execution-input-group {
@@ -172,6 +303,7 @@
172
303
  cursor: pointer;
173
304
  transition: all 0.2s;
174
305
  font-size: 0.875rem;
306
+ flex-shrink: 0;
175
307
  }
176
308
 
177
309
  .btn-primary {
@@ -198,23 +330,6 @@
198
330
  cursor: pointer;
199
331
  }
200
332
 
201
- /* Output area */
202
- #output-scroll {
203
- overflow-y: auto;
204
- overflow-x: hidden;
205
- display: flex;
206
- flex-direction: column;
207
- min-height: 0;
208
- }
209
-
210
- #output {
211
- flex: 1;
212
- padding: 1rem;
213
- display: flex;
214
- flex-direction: column;
215
- gap: 0.5rem;
216
- }
217
-
218
333
  /* Event cards */
219
334
  .card {
220
335
  background-color: var(--color-bg-primary);
@@ -246,16 +361,93 @@
246
361
 
247
362
  /* Responsive */
248
363
  @media (max-width: 768px) {
364
+ .layout-with-sidebar {
365
+ grid-template-columns: 1fr;
366
+ }
367
+
368
+ .sidebar {
369
+ display: none;
370
+ position: fixed;
371
+ top: 60px;
372
+ left: 0;
373
+ width: 250px;
374
+ height: calc(100vh - 60px);
375
+ z-index: 1000;
376
+ overflow-y: auto;
377
+ }
378
+
379
+ .sidebar.mobile-visible {
380
+ display: flex;
381
+ }
382
+
249
383
  .execution-panel {
250
384
  grid-template-columns: 1fr;
385
+ padding: 1rem;
251
386
  }
252
387
 
253
388
  .header-controls {
254
389
  flex-wrap: wrap;
390
+ gap: 0.5rem;
255
391
  }
256
392
 
257
393
  .execution-input-group {
258
394
  grid-template-columns: 1fr;
395
+ gap: 0.5rem;
396
+ }
397
+
398
+ #output-scroll {
399
+ padding: 0.75rem;
400
+ }
401
+
402
+ header {
403
+ padding: 0.75rem;
404
+ height: 50px;
405
+ }
406
+
407
+ .header-title {
408
+ font-size: 1.25rem;
409
+ }
410
+ }
411
+
412
+ @media (max-width: 480px) {
413
+ .layout-with-sidebar {
414
+ grid-template-columns: 1fr;
415
+ }
416
+
417
+ .sidebar {
418
+ width: 100%;
419
+ }
420
+
421
+ .execution-panel {
422
+ padding: 0.75rem;
423
+ }
424
+
425
+ .execution-textarea {
426
+ min-height: 2.5rem;
427
+ max-height: 6rem;
428
+ }
429
+
430
+ .btn {
431
+ padding: 0.5rem 1rem;
432
+ font-size: 0.8rem;
433
+ }
434
+
435
+ #output-scroll {
436
+ padding: 0.5rem;
437
+ }
438
+
439
+ header {
440
+ padding: 0.5rem;
441
+ height: 45px;
442
+ }
443
+
444
+ .header-title {
445
+ font-size: 1rem;
446
+ }
447
+
448
+ .status-badge {
449
+ padding: 0.25rem 0.5rem;
450
+ font-size: 0.75rem;
259
451
  }
260
452
  }
261
453
 
@@ -440,34 +632,48 @@
440
632
  </div>
441
633
  </header>
442
634
 
443
- <!-- Main Content -->
444
- <main id="app">
445
- <!-- Execution Panel -->
446
- <div class="execution-panel">
447
- <div class="execution-input-group">
448
- <select class="agent-selector" data-agent-selector title="Select agent">
449
- <option value="claude-code">Claude Code</option>
450
- <option value="opencode">OpenCode</option>
451
- </select>
452
-
453
- <textarea
454
- class="execution-textarea"
455
- data-message-input
456
- placeholder="Enter command or prompt... (Ctrl+Enter to submit)"
457
- aria-label="Execution prompt input"
458
- ></textarea>
635
+ <!-- Main Content with Sidebar -->
636
+ <div class="layout-with-sidebar">
637
+ <!-- Sidebar -->
638
+ <aside class="sidebar" data-sidebar>
639
+ <div class="sidebar-header">
640
+ <h2>History</h2>
641
+ <button class="sidebar-new-btn" data-new-conversation title="Start new conversation">+ New</button>
642
+ </div>
643
+ <ul class="sidebar-list" data-conversation-list>
644
+ <li class="sidebar-empty" data-conversation-empty>No conversations yet</li>
645
+ </ul>
646
+ </aside>
647
+
648
+ <!-- Main Content Area -->
649
+ <main id="app" class="main-content">
650
+ <!-- Output Container - Scrollable -->
651
+ <div id="output-scroll" role="region" aria-label="Execution output" aria-live="polite" aria-atomic="false">
652
+ <div id="output" class="output"></div>
459
653
  </div>
460
654
 
461
- <button class="btn btn-primary" data-send-button title="Start execution (Ctrl+Enter)">
462
- Execute
463
- </button>
464
- </div>
465
-
466
- <!-- Output Container -->
467
- <div id="output-scroll" role="region" aria-label="Execution output" aria-live="polite" aria-atomic="false">
468
- <div id="output" class="output"></div>
469
- </div>
470
- </main>
655
+ <!-- Execution Panel - Fixed at bottom -->
656
+ <div class="execution-panel">
657
+ <div class="execution-input-group">
658
+ <select class="agent-selector" data-agent-selector title="Select agent">
659
+ <option value="claude-code">Claude Code</option>
660
+ <option value="opencode">OpenCode</option>
661
+ </select>
662
+
663
+ <textarea
664
+ class="execution-textarea"
665
+ data-message-input
666
+ placeholder="Enter command or prompt... (Ctrl+Enter to submit)"
667
+ aria-label="Execution prompt input"
668
+ ></textarea>
669
+ </div>
670
+
671
+ <button class="btn btn-primary" data-send-button title="Start execution (Ctrl+Enter)">
672
+ Execute
673
+ </button>
674
+ </div>
675
+ </main>
676
+ </div>
471
677
  </div>
472
678
 
473
679
  <!-- Scripts - Order matters! -->
@@ -489,7 +695,10 @@
489
695
  <!-- 6. UI Components (no dependencies) -->
490
696
  <script src="/gm/js/ui-components.js"></script>
491
697
 
492
- <!-- 7. Main Client (depends on all above) -->
698
+ <!-- 7. Conversations Manager (loads conversation history) -->
699
+ <script src="/gm/js/conversations.js"></script>
700
+
701
+ <!-- 8. Main Client (depends on all above) -->
493
702
  <script src="/gm/js/client.js"></script>
494
703
 
495
704
  <!-- Inline initialization -->
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Conversations Module
3
+ * Manages conversation list sidebar with real-time updates
4
+ */
5
+
6
+ class ConversationManager {
7
+ constructor() {
8
+ this.conversations = [];
9
+ this.activeId = null;
10
+ this.listEl = document.querySelector('[data-conversation-list]');
11
+ this.emptyEl = document.querySelector('[data-conversation-empty]');
12
+ this.newBtn = document.querySelector('[data-new-conversation]');
13
+ this.sidebarEl = document.querySelector('[data-sidebar]');
14
+
15
+ if (!this.listEl) return;
16
+
17
+ this.init();
18
+ }
19
+
20
+ async init() {
21
+ this.newBtn?.addEventListener('click', () => this.createNew());
22
+ this.loadConversations();
23
+ this.setupWebSocketListener();
24
+
25
+ // Auto-refresh every 30 seconds
26
+ setInterval(() => this.loadConversations(), 30000);
27
+ }
28
+
29
+ async loadConversations() {
30
+ try {
31
+ const res = await fetch('/gm/api/conversations');
32
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
33
+
34
+ const data = await res.json();
35
+ this.conversations = data.conversations || [];
36
+ this.render();
37
+ } catch (err) {
38
+ console.error('Failed to load conversations:', err);
39
+ this.showEmpty('Failed to load conversations');
40
+ }
41
+ }
42
+
43
+ render() {
44
+ if (!this.listEl) return;
45
+
46
+ if (this.conversations.length === 0) {
47
+ this.showEmpty();
48
+ return;
49
+ }
50
+
51
+ this.listEl.innerHTML = '';
52
+ this.emptyEl.style.display = 'none';
53
+
54
+ // Sort by most recent first
55
+ const sorted = [...this.conversations].sort((a, b) =>
56
+ new Date(b.createdAt || 0) - new Date(a.createdAt || 0)
57
+ );
58
+
59
+ sorted.forEach(conv => {
60
+ const item = this.createConversationItem(conv);
61
+ this.listEl.appendChild(item);
62
+ });
63
+ }
64
+
65
+ createConversationItem(conv) {
66
+ const li = document.createElement('li');
67
+ li.className = 'conversation-item';
68
+ if (conv.id === this.activeId) li.classList.add('active');
69
+
70
+ const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
71
+ const timestamp = conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : 'Unknown';
72
+ const agent = conv.agentId || 'unknown';
73
+
74
+ li.innerHTML = `
75
+ <div class="conversation-item-title">${this.escapeHtml(title)}</div>
76
+ <div class="conversation-item-meta">${agent} • ${timestamp}</div>
77
+ `;
78
+
79
+ li.addEventListener('click', () => this.select(conv.id));
80
+ return li;
81
+ }
82
+
83
+ select(convId) {
84
+ this.activeId = convId;
85
+
86
+ // Update active indicator
87
+ document.querySelectorAll('.conversation-item').forEach(item => {
88
+ item.classList.remove('active');
89
+ });
90
+
91
+ const active = document.querySelector(`[data-conv-id="${convId}"]`);
92
+ if (active) active.classList.add('active');
93
+
94
+ // Emit event for client.js to handle
95
+ window.dispatchEvent(new CustomEvent('conversation-selected', {
96
+ detail: { conversationId: convId }
97
+ }));
98
+ }
99
+
100
+ createNew() {
101
+ window.dispatchEvent(new CustomEvent('create-new-conversation'));
102
+ }
103
+
104
+ showEmpty(message = 'No conversations yet') {
105
+ if (!this.listEl) return;
106
+ this.listEl.innerHTML = '';
107
+ this.emptyEl.textContent = message;
108
+ this.emptyEl.style.display = 'block';
109
+ }
110
+
111
+ addConversation(conv) {
112
+ // Add to beginning (most recent)
113
+ this.conversations.unshift(conv);
114
+ this.render();
115
+ }
116
+
117
+ updateConversation(convId, updates) {
118
+ const conv = this.conversations.find(c => c.id === convId);
119
+ if (conv) {
120
+ Object.assign(conv, updates);
121
+ this.render();
122
+ }
123
+ }
124
+
125
+ deleteConversation(convId) {
126
+ this.conversations = this.conversations.filter(c => c.id !== convId);
127
+ if (this.activeId === convId) this.activeId = null;
128
+ this.render();
129
+ }
130
+
131
+ setupWebSocketListener() {
132
+ window.addEventListener('ws-message', (event) => {
133
+ const msg = event.detail;
134
+
135
+ if (msg.type === 'conversation_created') {
136
+ this.addConversation(msg.conversation);
137
+ } else if (msg.type === 'conversation_updated') {
138
+ this.updateConversation(msg.conversation.id, msg.conversation);
139
+ } else if (msg.type === 'conversation_deleted') {
140
+ this.deleteConversation(msg.conversationId);
141
+ }
142
+ });
143
+ }
144
+
145
+ escapeHtml(text) {
146
+ const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
147
+ return text.replace(/[&<>"']/g, c => map[c]);
148
+ }
149
+ }
150
+
151
+ // Initialize when DOM is ready
152
+ if (document.readyState === 'loading') {
153
+ document.addEventListener('DOMContentLoaded', () => {
154
+ window.conversationManager = new ConversationManager();
155
+ });
156
+ } else {
157
+ window.conversationManager = new ConversationManager();
158
+ }
159
+
160
+ export { ConversationManager };