agentgui 1.0.535 → 1.0.537
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/database.js +38 -0
- package/lib/ws-handlers-conv.js +6 -0
- package/package.json +1 -1
- package/static/index.html +2 -0
- package/static/js/client.js +6 -4
- package/static/js/conversations.js +35 -0
- package/static/js/event-processor.js +40 -0
- package/static/js/image-loader.js +255 -0
- package/static/js/streaming-renderer.js +79 -0
- package/static/templates/image-display.html +76 -0
package/database.js
CHANGED
|
@@ -1038,6 +1038,44 @@ export const queries = {
|
|
|
1038
1038
|
}
|
|
1039
1039
|
},
|
|
1040
1040
|
|
|
1041
|
+
deleteAllConversations() {
|
|
1042
|
+
try {
|
|
1043
|
+
const conversations = prep('SELECT id, claudeSessionId FROM conversations').all();
|
|
1044
|
+
|
|
1045
|
+
for (const conv of conversations) {
|
|
1046
|
+
if (conv.claudeSessionId) {
|
|
1047
|
+
this.deleteClaudeSessionFile(conv.claudeSessionId);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const deleteAllStmt = db.transaction(() => {
|
|
1052
|
+
const allSessionIds = prep('SELECT id FROM sessions').all().map(r => r.id);
|
|
1053
|
+
|
|
1054
|
+
prep('DELETE FROM stream_updates');
|
|
1055
|
+
prep('DELETE FROM chunks');
|
|
1056
|
+
prep('DELETE FROM events');
|
|
1057
|
+
|
|
1058
|
+
if (allSessionIds.length > 0) {
|
|
1059
|
+
const placeholders = allSessionIds.map(() => '?').join(',');
|
|
1060
|
+
db.prepare(`DELETE FROM stream_updates WHERE sessionId IN (${placeholders})`).run(...allSessionIds);
|
|
1061
|
+
db.prepare(`DELETE FROM chunks WHERE sessionId IN (${placeholders})`).run(...allSessionIds);
|
|
1062
|
+
db.prepare(`DELETE FROM events WHERE sessionId IN (${placeholders})`).run(...allSessionIds);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
prep('DELETE FROM sessions');
|
|
1066
|
+
prep('DELETE FROM messages');
|
|
1067
|
+
prep('DELETE FROM conversations');
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
deleteAllStmt();
|
|
1071
|
+
console.log('[deleteAllConversations] Deleted all conversations and associated Claude Code files');
|
|
1072
|
+
return true;
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
console.error('[deleteAllConversations] Error deleting all conversations:', err.message);
|
|
1075
|
+
return false;
|
|
1076
|
+
}
|
|
1077
|
+
},
|
|
1078
|
+
|
|
1041
1079
|
cleanup() {
|
|
1042
1080
|
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
|
|
1043
1081
|
const now = Date.now();
|
package/lib/ws-handlers-conv.js
CHANGED
|
@@ -43,6 +43,12 @@ export function register(router, deps) {
|
|
|
43
43
|
return { deleted: true };
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
router.handle('conv.del.all', (p) => {
|
|
47
|
+
if (!queries.deleteAllConversations()) fail(500, 'Failed to delete all conversations');
|
|
48
|
+
broadcastSync({ type: 'all_conversations_deleted', timestamp: Date.now() });
|
|
49
|
+
return { deleted: true, message: 'All conversations deleted' };
|
|
50
|
+
});
|
|
51
|
+
|
|
46
52
|
router.handle('conv.full', (p) => {
|
|
47
53
|
const conv = queries.getConversation(p.id);
|
|
48
54
|
if (!conv) notFound();
|
package/package.json
CHANGED
package/static/index.html
CHANGED
|
@@ -3043,6 +3043,7 @@
|
|
|
3043
3043
|
<div class="sidebar-header">
|
|
3044
3044
|
<h2>History</h2>
|
|
3045
3045
|
<div class="sidebar-header-actions">
|
|
3046
|
+
<button id="deleteAllConversationsBtn" class="sidebar-clone-btn" data-delete-all-conversations title="Delete all conversations and Claude Code artifacts">Clear All</button>
|
|
3046
3047
|
<button id="cloneRepoBtn" class="sidebar-clone-btn" data-clone-repo title="Clone a GitHub repo">Clone</button>
|
|
3047
3048
|
<button id="newConversationBtn" class="sidebar-new-btn" data-new-conversation title="Start new conversation">+ New</button>
|
|
3048
3049
|
</div>
|
|
@@ -3246,6 +3247,7 @@
|
|
|
3246
3247
|
</script>
|
|
3247
3248
|
<script defer src="/gm/js/event-processor.js"></script>
|
|
3248
3249
|
<script defer src="/gm/js/streaming-renderer.js"></script>
|
|
3250
|
+
<script defer src="/gm/js/image-loader.js"></script>
|
|
3249
3251
|
<script defer src="/gm/js/kalman-filter.js"></script>
|
|
3250
3252
|
<script defer src="/gm/js/event-consolidator.js"></script>
|
|
3251
3253
|
<script defer src="/gm/js/websocket-manager.js"></script>
|
package/static/js/client.js
CHANGED
|
@@ -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;
|
|
@@ -47,6 +47,7 @@ class ConversationManager {
|
|
|
47
47
|
this.setupWebSocketListener();
|
|
48
48
|
this.setupFolderBrowser();
|
|
49
49
|
this.setupCloneUI();
|
|
50
|
+
this.setupDeleteAllButton();
|
|
50
51
|
|
|
51
52
|
this._pollInterval = setInterval(() => this.loadConversations(), 30000);
|
|
52
53
|
|
|
@@ -245,6 +246,40 @@ class ConversationManager {
|
|
|
245
246
|
}));
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
setupDeleteAllButton() {
|
|
250
|
+
this.deleteAllBtn = document.getElementById('deleteAllConversationsBtn');
|
|
251
|
+
if (!this.deleteAllBtn) return;
|
|
252
|
+
this.deleteAllBtn.addEventListener('click', () => this.confirmDeleteAll());
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async confirmDeleteAll() {
|
|
256
|
+
if (this.conversations.length === 0) {
|
|
257
|
+
window.UIDialog.alert('No conversations to delete', 'Information');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const confirmed = await window.UIDialog.confirm(
|
|
262
|
+
`Delete all ${this.conversations.length} conversation(s) and associated Claude Code artifacts?\n\nThis action cannot be undone.`,
|
|
263
|
+
'Delete All Conversations'
|
|
264
|
+
);
|
|
265
|
+
if (!confirmed) return;
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
this.deleteAllBtn.disabled = true;
|
|
269
|
+
await window.wsClient.rpc('conv.del.all', {});
|
|
270
|
+
console.log('[ConversationManager] Deleted all conversations');
|
|
271
|
+
this.conversations = [];
|
|
272
|
+
this.activeId = null;
|
|
273
|
+
window.dispatchEvent(new CustomEvent('conversation-deselected'));
|
|
274
|
+
this.render();
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error('[ConversationManager] Delete all error:', err);
|
|
277
|
+
window.UIDialog.alert('Failed to delete all conversations: ' + (err.message || 'Unknown error'), 'Error');
|
|
278
|
+
} finally {
|
|
279
|
+
this.deleteAllBtn.disabled = false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
248
283
|
setupCloneUI() {
|
|
249
284
|
this.cloneBtn = document.getElementById('cloneRepoBtn');
|
|
250
285
|
this.cloneBar = document.getElementById('cloneInputBar');
|
|
@@ -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
|
+
}
|
|
@@ -1950,6 +1950,85 @@ class StreamingRenderer {
|
|
|
1950
1950
|
return div;
|
|
1951
1951
|
}
|
|
1952
1952
|
|
|
1953
|
+
/**
|
|
1954
|
+
* Render file read event with image detection
|
|
1955
|
+
*/
|
|
1956
|
+
renderFileRead(event) {
|
|
1957
|
+
const div = document.createElement('div');
|
|
1958
|
+
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';
|
|
1959
|
+
div.dataset.eventId = event.id || '';
|
|
1960
|
+
div.dataset.eventType = 'file_read';
|
|
1961
|
+
|
|
1962
|
+
const filePath = event.path || '';
|
|
1963
|
+
const fileName = pathBasename(filePath);
|
|
1964
|
+
const isImage = this.isImagePath(filePath);
|
|
1965
|
+
|
|
1966
|
+
let html = `
|
|
1967
|
+
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:${isImage ? '0.75rem' : '0'}">
|
|
1968
|
+
<svg viewBox="0 0 20 20" fill="currentColor" style="width:1rem;height:1rem;color:var(--color-info)">
|
|
1969
|
+
<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"/>
|
|
1970
|
+
</svg>
|
|
1971
|
+
<code style="font-size:0.75rem;color:var(--color-text-primary);font-family:'Monaco','Menlo','Ubuntu Mono',monospace">${this.escapeHtml(filePath)}</code>
|
|
1972
|
+
</div>
|
|
1973
|
+
`;
|
|
1974
|
+
|
|
1975
|
+
if (isImage) {
|
|
1976
|
+
html += `<div style="margin-top:0.75rem" class="lazy-image-container" data-image-path="${this.escapeHtml(filePath)}"></div>`;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
div.innerHTML = html;
|
|
1980
|
+
|
|
1981
|
+
if (isImage) {
|
|
1982
|
+
const container = div.querySelector('.lazy-image-container');
|
|
1983
|
+
if (container && window.imageLoader) {
|
|
1984
|
+
const imgElement = window.imageLoader.createImageElement(filePath);
|
|
1985
|
+
container.appendChild(imgElement);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
return div;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
/**
|
|
1993
|
+
* Render file write event
|
|
1994
|
+
*/
|
|
1995
|
+
renderFileWrite(event) {
|
|
1996
|
+
const div = document.createElement('div');
|
|
1997
|
+
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';
|
|
1998
|
+
div.dataset.eventId = event.id || '';
|
|
1999
|
+
div.dataset.eventType = 'file_write';
|
|
2000
|
+
|
|
2001
|
+
const filePath = event.path || '';
|
|
2002
|
+
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>';
|
|
2003
|
+
|
|
2004
|
+
div.innerHTML = `
|
|
2005
|
+
<div style="display:flex;align-items:center;gap:0.5rem">
|
|
2006
|
+
${icon}
|
|
2007
|
+
<code style="font-size:0.75rem;color:var(--color-text-primary);font-family:'Monaco','Menlo','Ubuntu Mono',monospace">${this.escapeHtml(filePath)}</code>
|
|
2008
|
+
</div>
|
|
2009
|
+
`;
|
|
2010
|
+
|
|
2011
|
+
return div;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* Check if a path is an image file
|
|
2016
|
+
*/
|
|
2017
|
+
isImagePath(filePath) {
|
|
2018
|
+
if (!filePath || typeof filePath !== 'string') return false;
|
|
2019
|
+
const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'];
|
|
2020
|
+
const ext = this.getFileExtension(filePath);
|
|
2021
|
+
return imageExts.includes(ext);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
/**
|
|
2025
|
+
* Extract file extension
|
|
2026
|
+
*/
|
|
2027
|
+
getFileExtension(filePath) {
|
|
2028
|
+
const match = filePath.match(/\.([^.]+)$/);
|
|
2029
|
+
return match ? match[1].toLowerCase() : '';
|
|
2030
|
+
}
|
|
2031
|
+
|
|
1953
2032
|
/**
|
|
1954
2033
|
* Render generic event with formatted key-value pairs
|
|
1955
2034
|
*/
|
|
@@ -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>
|