agentgui 1.0.536 → 1.0.538

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.536",
3
+ "version": "1.0.538",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/static/index.html CHANGED
@@ -3247,6 +3247,7 @@
3247
3247
  </script>
3248
3248
  <script defer src="/gm/js/event-processor.js"></script>
3249
3249
  <script defer src="/gm/js/streaming-renderer.js"></script>
3250
+ <script defer src="/gm/js/image-loader.js"></script>
3250
3251
  <script defer src="/gm/js/kalman-filter.js"></script>
3251
3252
  <script defer src="/gm/js/event-consolidator.js"></script>
3252
3253
  <script defer src="/gm/js/websocket-manager.js"></script>
@@ -102,6 +102,12 @@ class AgentGUIClient {
102
102
  // Initialize renderer
103
103
  this.renderer.init(this.config.outputContainerId, this.config.scrollContainerId);
104
104
 
105
+ // Initialize image loader
106
+ if (typeof ImageLoader !== 'undefined') {
107
+ window.imageLoader = new ImageLoader();
108
+ console.log('Image loader initialized');
109
+ }
110
+
105
111
  // Setup event listeners
106
112
  this.setupWebSocketListeners();
107
113
  this.setupRendererListeners();
@@ -1649,10 +1655,6 @@ class AgentGUIClient {
1649
1655
  }
1650
1656
 
1651
1657
  let finalPrompt = prompt;
1652
- if (subAgent && agentId === 'claude-code') {
1653
- const displaySubAgent = subAgent.split('-·-')[0];
1654
- finalPrompt = `use ${displaySubAgent} subagent to ${prompt}`;
1655
- }
1656
1658
  const streamBody = { id: conversationId, content: finalPrompt, agentId };
1657
1659
  if (model) streamBody.model = model;
1658
1660
  if (subAgent) streamBody.subAgent = subAgent;
@@ -119,6 +119,20 @@ class EventProcessor {
119
119
  this.stats.transformedEvents++;
120
120
  }
121
121
 
122
+ if (event.type === 'file_read' && event.path && this.isImagePath(event.path)) {
123
+ processed.isImage = true;
124
+ processed.imagePath = event.path;
125
+ this.stats.transformedEvents++;
126
+ }
127
+
128
+ if ((event.type === 'text_block' || event.type === 'command_execute' || event.type === 'streaming_progress') && event.content || event.output) {
129
+ const imagePaths = this.extractImagePaths(event.content || event.output || '');
130
+ if (imagePaths.length > 0) {
131
+ processed.detectedImages = imagePaths;
132
+ this.stats.transformedEvents++;
133
+ }
134
+ }
135
+
122
136
  processed.processTime = performance.now() - startTime;
123
137
  this.stats.processedEvents++;
124
138
 
@@ -444,6 +458,32 @@ class EventProcessor {
444
458
  avgProcessTime: 0
445
459
  };
446
460
  }
461
+
462
+ /**
463
+ * Check if a path is an image file
464
+ */
465
+ isImagePath(filePath) {
466
+ if (!filePath || typeof filePath !== 'string') return false;
467
+ const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'];
468
+ const ext = this.getFileExtension(filePath);
469
+ return imageExts.includes(ext);
470
+ }
471
+
472
+ /**
473
+ * Extract image file paths from text content
474
+ */
475
+ extractImagePaths(content) {
476
+ if (typeof content !== 'string') return [];
477
+ const paths = [];
478
+ const pathPattern = /(?:\/[a-zA-Z0-9_.\-]+)+\/[a-zA-Z0-9_.\-]+\.(?:png|jpg|jpeg|gif|webp|svg)/gi;
479
+ let match;
480
+ while ((match = pathPattern.exec(content)) !== null) {
481
+ if (this.isImagePath(match[0])) {
482
+ paths.push(match[0]);
483
+ }
484
+ }
485
+ return [...new Set(paths)];
486
+ }
447
487
  }
448
488
 
449
489
  // Export for use in browser
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Image Loader Module
3
+ * Detects image file reads from agent events and manages lazy loading
4
+ * Supports PNG, JPG, JPEG, GIF, WebP, SVG formats
5
+ */
6
+
7
+ class ImageLoader {
8
+ constructor(config = {}) {
9
+ this.config = {
10
+ supportedExts: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'],
11
+ lazyLoadThreshold: config.lazyLoadThreshold || 0.5,
12
+ maxImageDisplaySize: config.maxImageDisplaySize || '600px',
13
+ ...config
14
+ };
15
+
16
+ this.imageCache = new Map();
17
+ this.pendingImages = new Map();
18
+ this.intersectionObserver = null;
19
+ this.drawerObserver = null;
20
+ this.initIntersectionObserver();
21
+ }
22
+
23
+ /**
24
+ * Check if a path is an image file
25
+ */
26
+ isImagePath(filePath) {
27
+ if (!filePath || typeof filePath !== 'string') return false;
28
+ const ext = this.getExtension(filePath).toLowerCase();
29
+ return this.config.supportedExts.includes(ext);
30
+ }
31
+
32
+ /**
33
+ * Extract file extension from path
34
+ */
35
+ getExtension(filePath) {
36
+ const match = filePath.match(/\.([^.]+)$/);
37
+ return match ? match[1] : '';
38
+ }
39
+
40
+ /**
41
+ * Extract image paths from text content
42
+ */
43
+ extractImagePaths(content) {
44
+ if (typeof content !== 'string') return [];
45
+
46
+ const paths = [];
47
+ const pathPattern = /(?:\/[a-zA-Z0-9_.\-]+)+\/[a-zA-Z0-9_.\-]+\.(?:png|jpg|jpeg|gif|webp|svg)/gi;
48
+
49
+ let match;
50
+ while ((match = pathPattern.exec(content)) !== null) {
51
+ if (this.isImagePath(match[0])) {
52
+ paths.push(match[0]);
53
+ }
54
+ }
55
+
56
+ return [...new Set(paths)];
57
+ }
58
+
59
+ /**
60
+ * Register images from event
61
+ */
62
+ registerImagesFromEvent(event) {
63
+ const images = [];
64
+
65
+ if (event.type === 'file_read' && event.path && this.isImagePath(event.path)) {
66
+ images.push({
67
+ path: event.path,
68
+ type: 'file_read',
69
+ eventId: event.id || event.sessionId,
70
+ timestamp: event.timestamp || Date.now()
71
+ });
72
+ }
73
+
74
+ if (event.content && typeof event.content === 'string') {
75
+ const paths = this.extractImagePaths(event.content);
76
+ paths.forEach(path => {
77
+ images.push({
78
+ path,
79
+ type: 'extracted',
80
+ eventId: event.id || event.sessionId,
81
+ timestamp: event.timestamp || Date.now()
82
+ });
83
+ });
84
+ }
85
+
86
+ if (event.output && typeof event.output === 'string') {
87
+ const paths = this.extractImagePaths(event.output);
88
+ paths.forEach(path => {
89
+ images.push({
90
+ path,
91
+ type: 'extracted',
92
+ eventId: event.id || event.sessionId,
93
+ timestamp: event.timestamp || Date.now()
94
+ });
95
+ });
96
+ }
97
+
98
+ images.forEach(img => {
99
+ const key = img.path;
100
+ if (!this.imageCache.has(key)) {
101
+ this.imageCache.set(key, img);
102
+ this.pendingImages.set(key, img);
103
+ }
104
+ });
105
+
106
+ return images;
107
+ }
108
+
109
+ /**
110
+ * Create image element with lazy loading
111
+ */
112
+ createImageElement(imagePath, options = {}) {
113
+ const container = document.createElement('div');
114
+ container.className = 'image-container';
115
+ container.dataset.imagePath = imagePath;
116
+ container.style.cssText = `
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: 0.5rem;
120
+ padding: 0.75rem;
121
+ border-radius: 0.375rem;
122
+ background: var(--color-bg-secondary);
123
+ border: 1px solid var(--color-border);
124
+ `;
125
+
126
+ const placeholder = document.createElement('div');
127
+ placeholder.className = 'image-placeholder';
128
+ placeholder.style.cssText = `
129
+ background: linear-gradient(90deg, var(--color-bg-tertiary) 25%, var(--color-bg-secondary) 50%, var(--color-bg-tertiary) 75%);
130
+ background-size: 200% 100%;
131
+ animation: loading 1.5s infinite;
132
+ border-radius: 0.375rem;
133
+ aspect-ratio: 16/9;
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+ color: var(--color-text-secondary);
138
+ font-size: 0.875rem;
139
+ `;
140
+ placeholder.innerHTML = 'Loading image...';
141
+ placeholder.dataset.path = imagePath;
142
+
143
+ const img = document.createElement('img');
144
+ img.className = 'lazy-image';
145
+ img.alt = imagePath;
146
+ img.style.cssText = `
147
+ max-width: 100%;
148
+ max-height: ${this.config.maxImageDisplaySize};
149
+ border-radius: 0.375rem;
150
+ display: none;
151
+ `;
152
+ img.dataset.src = imagePath;
153
+
154
+ const caption = document.createElement('div');
155
+ caption.className = 'image-caption';
156
+ caption.style.cssText = `
157
+ font-size: 0.75rem;
158
+ color: var(--color-text-secondary);
159
+ word-break: break-all;
160
+ font-family: 'Monaco', 'Menlo', monospace;
161
+ `;
162
+ caption.textContent = imagePath;
163
+
164
+ container.appendChild(placeholder);
165
+ container.appendChild(img);
166
+ container.appendChild(caption);
167
+
168
+ img.addEventListener('load', () => {
169
+ placeholder.style.display = 'none';
170
+ img.style.display = 'block';
171
+ });
172
+
173
+ img.addEventListener('error', () => {
174
+ placeholder.textContent = 'Failed to load image';
175
+ placeholder.style.background = 'var(--color-bg-error)';
176
+ placeholder.style.color = 'var(--color-text-error)';
177
+ });
178
+
179
+ if (this.intersectionObserver) {
180
+ this.intersectionObserver.observe(container);
181
+ }
182
+
183
+ return container;
184
+ }
185
+
186
+ /**
187
+ * Initialize Intersection Observer for lazy loading
188
+ */
189
+ initIntersectionObserver() {
190
+ this.intersectionObserver = new IntersectionObserver(
191
+ (entries) => {
192
+ entries.forEach(entry => {
193
+ if (entry.isIntersecting) {
194
+ const img = entry.target.querySelector('img.lazy-image');
195
+ if (img && img.dataset.src && !img.src) {
196
+ img.src = img.dataset.src;
197
+ this.intersectionObserver.unobserve(entry.target);
198
+ }
199
+ }
200
+ });
201
+ },
202
+ { threshold: this.config.lazyLoadThreshold }
203
+ );
204
+ }
205
+
206
+ /**
207
+ * Setup drawer observer to load images when drawer opens
208
+ */
209
+ setupDrawerObserver(drawerSelector = '.drawer-panel, [role="dialog"]') {
210
+ const drawers = document.querySelectorAll(drawerSelector);
211
+ drawers.forEach(drawer => {
212
+ const observer = new MutationObserver(() => {
213
+ if (drawer.offsetHeight > 0 && drawer.offsetWidth > 0) {
214
+ this.loadVisibleImages(drawer);
215
+ }
216
+ });
217
+
218
+ observer.observe(drawer, { attributes: true, attributeFilter: ['style', 'class'] });
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Load all visible images in a container
224
+ */
225
+ loadVisibleImages(container = document) {
226
+ const images = container.querySelectorAll('img.lazy-image[data-src]');
227
+ images.forEach(img => {
228
+ if (!img.src && img.dataset.src) {
229
+ img.src = img.dataset.src;
230
+ }
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Get cached images for a session/conversation
236
+ */
237
+ getImages(eventId = null) {
238
+ if (!eventId) {
239
+ return Array.from(this.imageCache.values());
240
+ }
241
+ return Array.from(this.imageCache.values()).filter(img => img.eventId === eventId);
242
+ }
243
+
244
+ /**
245
+ * Clear cache
246
+ */
247
+ clear() {
248
+ this.imageCache.clear();
249
+ this.pendingImages.clear();
250
+ }
251
+ }
252
+
253
+ if (typeof module !== 'undefined' && module.exports) {
254
+ module.exports = ImageLoader;
255
+ }
@@ -1626,6 +1626,26 @@ class StreamingRenderer {
1626
1626
  return div;
1627
1627
  }
1628
1628
 
1629
+ /**
1630
+ * Detect if content is a base64-encoded image
1631
+ */
1632
+ detectBase64Image(content) {
1633
+ if (!content || typeof content !== 'string') return null;
1634
+ const trimmed = content.trim();
1635
+ const signatures = {
1636
+ 'png': /^iVBORw0KGgo/,
1637
+ 'jpeg': /^\/9j\/4AAQ/,
1638
+ 'webp': /^UklGRi/,
1639
+ 'gif': /^R0lGODlh/
1640
+ };
1641
+ for (const [type, pattern] of Object.entries(signatures)) {
1642
+ if (pattern.test(trimmed)) {
1643
+ return { type, isBase64: true, data: trimmed };
1644
+ }
1645
+ }
1646
+ return null;
1647
+ }
1648
+
1629
1649
  /**
1630
1650
  * Render file read event
1631
1651
  */
@@ -1651,7 +1671,13 @@ class StreamingRenderer {
1651
1671
  let html = '';
1652
1672
  if (event.path) html += this.renderFilePath(event.path);
1653
1673
  if (event.content) {
1654
- html += `<pre style="background:#1e293b;padding:0.75rem;border-radius:0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;line-height:1.5;color:#e2e8f0;margin:0.5rem 0 0 0"><code class="lazy-hl">${this.escapeHtml(this.truncateContent(event.content, 2000))}</code></pre>`;
1674
+ const imageInfo = this.detectBase64Image(event.content);
1675
+ if (imageInfo) {
1676
+ const mimeType = imageInfo.type === 'jpeg' ? 'image/jpeg' : `image/${imageInfo.type}`;
1677
+ html += `<div style="padding:0.5rem;display:flex;flex-direction:column;gap:0.5rem"><img src="data:${mimeType};base64,${this.escapeHtml(imageInfo.data)}" style="max-width:100%;max-height:600px;border-radius:0.375rem;border:1px solid #334155" loading="lazy"><div style="font-size:0.7rem;color:#64748b;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;word-break:break-all">${this.escapeHtml(event.path)}</div></div>`;
1678
+ } else {
1679
+ html += `<pre style="background:#1e293b;padding:0.75rem;border-radius:0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;line-height:1.5;color:#e2e8f0;margin:0.5rem 0 0 0"><code class="lazy-hl">${this.escapeHtml(this.truncateContent(event.content, 2000))}</code></pre>`;
1680
+ }
1655
1681
  }
1656
1682
  body.innerHTML = html;
1657
1683
  details.appendChild(body);
@@ -1950,6 +1976,85 @@ class StreamingRenderer {
1950
1976
  return div;
1951
1977
  }
1952
1978
 
1979
+ /**
1980
+ * Render file read event with image detection
1981
+ */
1982
+ renderFileRead(event) {
1983
+ const div = document.createElement('div');
1984
+ div.className = 'event-file-read mb-3 p-3 rounded border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900';
1985
+ div.dataset.eventId = event.id || '';
1986
+ div.dataset.eventType = 'file_read';
1987
+
1988
+ const filePath = event.path || '';
1989
+ const fileName = pathBasename(filePath);
1990
+ const isImage = this.isImagePath(filePath);
1991
+
1992
+ let html = `
1993
+ <div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:${isImage ? '0.75rem' : '0'}">
1994
+ <svg viewBox="0 0 20 20" fill="currentColor" style="width:1rem;height:1rem;color:var(--color-info)">
1995
+ <path d="M8 16a2 2 0 100-4 2 2 0 000 4zM9 2a1 1 0 011 1v2h2V3a1 1 0 112 0v2h2V3a1 1 0 112 0v2h2a2 2 0 012 2v2h2a1 1 0 110 2h-2v2h2a1 1 0 110 2h-2v2a2 2 0 01-2 2h-2v2a1 1 0 11-2 0v-2h-2v2a1 1 0 11-2 0v-2H9a2 2 0 01-2-2v-2H5a1 1 0 110-2h2V9H5a1 1 0 010-2h2V5H5a1 1 0 010-2h2V3a1 1 0 011-1z"/>
1996
+ </svg>
1997
+ <code style="font-size:0.75rem;color:var(--color-text-primary);font-family:'Monaco','Menlo','Ubuntu Mono',monospace">${this.escapeHtml(filePath)}</code>
1998
+ </div>
1999
+ `;
2000
+
2001
+ if (isImage) {
2002
+ html += `<div style="margin-top:0.75rem" class="lazy-image-container" data-image-path="${this.escapeHtml(filePath)}"></div>`;
2003
+ }
2004
+
2005
+ div.innerHTML = html;
2006
+
2007
+ if (isImage) {
2008
+ const container = div.querySelector('.lazy-image-container');
2009
+ if (container && window.imageLoader) {
2010
+ const imgElement = window.imageLoader.createImageElement(filePath);
2011
+ container.appendChild(imgElement);
2012
+ }
2013
+ }
2014
+
2015
+ return div;
2016
+ }
2017
+
2018
+ /**
2019
+ * Render file write event
2020
+ */
2021
+ renderFileWrite(event) {
2022
+ const div = document.createElement('div');
2023
+ div.className = 'event-file-write mb-3 p-3 rounded border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900';
2024
+ div.dataset.eventId = event.id || '';
2025
+ div.dataset.eventType = 'file_write';
2026
+
2027
+ const filePath = event.path || '';
2028
+ const icon = '<svg viewBox="0 0 20 20" fill="currentColor" style="width:1rem;height:1rem;color:var(--color-success)"><path d="M11 3a1 1 0 10-2 0v1a1 1 0 102 0V3zM15.657 5.757a1 1 0 00-1.414-1.414l-.707.707a1 1 0 001.414 1.414l.707-.707zM18 10a1 1 0 01-1 1h-1a1 1 0 110-2h1a1 1 0 011 1zM16.243 15.657a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414l.707.707zM10 15a1 1 0 01-1-1v-1a1 1 0 112 0v1a1 1 0 01-1 1zM5.757 16.243a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414l-.707.707zM5 10a1 1 0 01-1-1V8a1 1 0 012 0v1a1 1 0 01-1 1zM5.757 5.757a1 1 0 000-1.414l-.707-.707a1 1 0 00-1.414 1.414l.707.707zM10 5a1 1 0 011-1h1a1 1 0 110 2h-1a1 1 0 01-1-1z"/></svg>';
2029
+
2030
+ div.innerHTML = `
2031
+ <div style="display:flex;align-items:center;gap:0.5rem">
2032
+ ${icon}
2033
+ <code style="font-size:0.75rem;color:var(--color-text-primary);font-family:'Monaco','Menlo','Ubuntu Mono',monospace">${this.escapeHtml(filePath)}</code>
2034
+ </div>
2035
+ `;
2036
+
2037
+ return div;
2038
+ }
2039
+
2040
+ /**
2041
+ * Check if a path is an image file
2042
+ */
2043
+ isImagePath(filePath) {
2044
+ if (!filePath || typeof filePath !== 'string') return false;
2045
+ const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'];
2046
+ const ext = this.getFileExtension(filePath);
2047
+ return imageExts.includes(ext);
2048
+ }
2049
+
2050
+ /**
2051
+ * Extract file extension
2052
+ */
2053
+ getFileExtension(filePath) {
2054
+ const match = filePath.match(/\.([^.]+)$/);
2055
+ return match ? match[1].toLowerCase() : '';
2056
+ }
2057
+
1953
2058
  /**
1954
2059
  * Render generic event with formatted key-value pairs
1955
2060
  */
@@ -0,0 +1,76 @@
1
+ <!-- Image Display Container with Lazy Loading -->
2
+ <div class="image-container" data-image-path="{{ imagePath }}" role="img" aria-label="{{ imagePath }}">
3
+ <!-- Placeholder shown while loading -->
4
+ <div class="image-placeholder" data-path="{{ imagePath }}" style="
5
+ background: linear-gradient(90deg, var(--color-bg-tertiary) 25%, var(--color-bg-secondary) 50%, var(--color-bg-tertiary) 75%);
6
+ background-size: 200% 100%;
7
+ animation: loading 1.5s infinite;
8
+ border-radius: 0.375rem;
9
+ aspect-ratio: 16/9;
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ color: var(--color-text-secondary);
14
+ font-size: 0.875rem;
15
+ ">
16
+ Loading image...
17
+ </div>
18
+
19
+ <!-- Actual image element with lazy loading -->
20
+ <img
21
+ class="lazy-image"
22
+ data-src="{{ imagePath }}"
23
+ alt="{{ imagePath }}"
24
+ loading="lazy"
25
+ style="
26
+ max-width: 100%;
27
+ max-height: 600px;
28
+ border-radius: 0.375rem;
29
+ display: none;
30
+ "
31
+ />
32
+
33
+ <!-- Image path caption -->
34
+ <div class="image-caption" style="
35
+ font-size: 0.75rem;
36
+ color: var(--color-text-secondary);
37
+ word-break: break-all;
38
+ font-family: 'Monaco', 'Menlo', monospace;
39
+ ">
40
+ {{ imagePath }}
41
+ </div>
42
+ </div>
43
+
44
+ <style>
45
+ @keyframes loading {
46
+ 0% {
47
+ background-position: 200% 0;
48
+ }
49
+ 100% {
50
+ background-position: -200% 0;
51
+ }
52
+ }
53
+
54
+ .image-container {
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: 0.5rem;
58
+ padding: 0.75rem;
59
+ border-radius: 0.375rem;
60
+ background: var(--color-bg-secondary);
61
+ border: 1px solid var(--color-border);
62
+ margin: 0.5rem 0;
63
+ }
64
+
65
+ .image-placeholder {
66
+ animation: loading 1.5s infinite;
67
+ }
68
+
69
+ .lazy-image {
70
+ transition: opacity 0.3s ease-in-out;
71
+ }
72
+
73
+ .image-caption {
74
+ white-space: pre-wrap;
75
+ }
76
+ </style>