agentgui 1.0.541 → 1.0.543
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/CLAUDE.md +85 -0
- package/lib/tool-manager.js +13 -0
- package/package.json +1 -1
- package/static/index.html +29 -18
- package/static/js/streaming-renderer.js +5 -0
package/CLAUDE.md
CHANGED
|
@@ -219,6 +219,91 @@ After update/install completes:
|
|
|
219
219
|
3. UI version display updates to show new version
|
|
220
220
|
4. Status reverts to "Installed" or "Up-to-date" accordingly
|
|
221
221
|
|
|
222
|
+
## Base64 Image Rendering in File Read Events
|
|
223
|
+
|
|
224
|
+
### Problem: Images Displaying as Raw Text
|
|
225
|
+
|
|
226
|
+
When an agent reads an image file, the streaming event may not have `type='file_read'`. It can arrive with any type (or fall through to the default case in the renderer switch). Without the `renderGeneric` fallback, the image data displays as raw base64 text instead of an `<img>` element.
|
|
227
|
+
|
|
228
|
+
### Event Structure for Image File Reads
|
|
229
|
+
|
|
230
|
+
Two nested structures are used by different agent versions. Both must be handled:
|
|
231
|
+
|
|
232
|
+
**Structure A** (nested under `source`):
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"type": "<anything>",
|
|
236
|
+
"path": "/path/to/image.png",
|
|
237
|
+
"content": {
|
|
238
|
+
"source": {
|
|
239
|
+
"type": "base64",
|
|
240
|
+
"data": "<base64-string>"
|
|
241
|
+
},
|
|
242
|
+
"media_type": "image/png"
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Structure B** (flat inside `content`):
|
|
248
|
+
```json
|
|
249
|
+
{
|
|
250
|
+
"type": "<anything>",
|
|
251
|
+
"path": "/path/to/image.png",
|
|
252
|
+
"content": {
|
|
253
|
+
"type": "base64",
|
|
254
|
+
"data": "<base64-string>"
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Structure C** (content is raw base64 string, no wrapping object):
|
|
260
|
+
```json
|
|
261
|
+
{
|
|
262
|
+
"type": "<anything>",
|
|
263
|
+
"path": "/path/to/image.png",
|
|
264
|
+
"content": "iVBORw0KGgo..."
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
Structure C is detected by `detectBase64Image()` which checks magic-byte prefixes (PNG: `iVBORw0KGgo`, JPEG: `/9j/4AAQ`, WebP: `UklGRi`, GIF: `R0lGODlh`).
|
|
268
|
+
|
|
269
|
+
### Two Rendering Paths in streaming-renderer.js
|
|
270
|
+
|
|
271
|
+
**Path 1 – Direct dispatch** (`renderEvent` switch statement):
|
|
272
|
+
- `case 'file_read'` routes directly to `renderFileRead(event)`.
|
|
273
|
+
- Handles all three content structures above.
|
|
274
|
+
|
|
275
|
+
**Path 2 – Generic fallback** (`renderGeneric`):
|
|
276
|
+
- Called for any event type not matched by the switch (the `default` case).
|
|
277
|
+
- First thing it does: check for `event.content?.source?.type === 'base64'` OR `event.content?.type === 'base64'` AND `event.path` present.
|
|
278
|
+
- If true, delegates to `renderFileRead(event)` so the image renders correctly.
|
|
279
|
+
- Without this fallback, any file-read event that arrives with an unrecognised type displays as raw key-value text.
|
|
280
|
+
|
|
281
|
+
### MIME Type Resolution in renderFileRead
|
|
282
|
+
|
|
283
|
+
Priority order inside `renderFileRead`:
|
|
284
|
+
1. `event.media_type` field (explicit)
|
|
285
|
+
2. Detected from magic bytes via `detectBase64Image()` → maps `jpeg` → `image/jpeg`, others → `image/<type>`
|
|
286
|
+
3. Falls back to `application/octet-stream` (shows broken image)
|
|
287
|
+
|
|
288
|
+
Always include `media_type` on the event when possible. If absent, magic-byte detection covers PNG/JPEG/WebP/GIF automatically.
|
|
289
|
+
|
|
290
|
+
### Debugging Checklist When Images Show as Text
|
|
291
|
+
|
|
292
|
+
1. `console.log(event)` the raw event object arriving at the renderer — verify `content` structure.
|
|
293
|
+
2. Check `event.type` — if it is not `'file_read'`, the switch default fires `renderGeneric`.
|
|
294
|
+
3. Confirm `renderGeneric` has the base64 fallback guard at the top (search for `content?.source?.type === 'base64'`).
|
|
295
|
+
4. Confirm `renderFileRead` handles both `content.source.data` and `content.data` paths (both exist in the code).
|
|
296
|
+
5. Verify `event.path` is set — the fallback in `renderGeneric` requires `event.path` to delegate correctly.
|
|
297
|
+
6. If `media_type` is missing and content is not PNG/JPEG/WebP/GIF, add it to the event or extend `detectBase64Image` signatures.
|
|
298
|
+
|
|
299
|
+
### Why Two Attempts Failed Before the Fix
|
|
300
|
+
|
|
301
|
+
- Attempt 1: Modified only `renderFileRead` but the event had an unrecognised type so the switch never reached `renderFileRead`.
|
|
302
|
+
- Attempt 2: Added fallback in `renderGeneric` but checked only `event.content?.source?.type === 'base64'` — missed Structure B where data sits directly on `event.content` (no `source` nesting).
|
|
303
|
+
- Fix: `renderGeneric` now checks both structures before falling through to generic key-value rendering.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
222
307
|
## Tool Update Testing & Diagnostics
|
|
223
308
|
|
|
224
309
|
A comprehensive diagnostic page is available at `http://localhost:3000/gm/tool-update-test.html` (`static/tool-update-test.html`) with 7 interactive test sections:
|
package/lib/tool-manager.js
CHANGED
|
@@ -9,6 +9,7 @@ const TOOLS = [
|
|
|
9
9
|
{ id: 'gm-oc', name: 'gm-oc', pkg: 'gm-oc', pluginId: 'gm-oc' },
|
|
10
10
|
{ id: 'gm-gc', name: 'gm-gc', pkg: 'gm-gc', pluginId: 'gm' },
|
|
11
11
|
{ id: 'gm-kilo', name: 'gm-kilo', pkg: 'gm-kilo', pluginId: 'gm-kilo' },
|
|
12
|
+
{ id: 'codex', name: '@openai/codex', pkg: '@openai/codex', pluginId: 'codex' },
|
|
12
13
|
];
|
|
13
14
|
|
|
14
15
|
const statusCache = new Map();
|
|
@@ -76,6 +77,17 @@ const getInstalledVersion = (pkg, pluginId = null) => {
|
|
|
76
77
|
console.warn(`[tool-manager] Failed to parse ${kiloPath}:`, e.message);
|
|
77
78
|
}
|
|
78
79
|
}
|
|
80
|
+
|
|
81
|
+
// Check Codex CLI (stored at ~/.codex)
|
|
82
|
+
const codexPath = path.join(homeDir, '.codex', 'plugin.json');
|
|
83
|
+
if (fs.existsSync(codexPath)) {
|
|
84
|
+
try {
|
|
85
|
+
const pluginJson = JSON.parse(fs.readFileSync(codexPath, 'utf-8'));
|
|
86
|
+
if (pluginJson.version) return pluginJson.version;
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.warn(`[tool-manager] Failed to parse ${codexPath}:`, e.message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
79
91
|
} catch (_) {}
|
|
80
92
|
return null;
|
|
81
93
|
};
|
|
@@ -129,6 +141,7 @@ const checkToolInstalled = (pluginId) => {
|
|
|
129
141
|
if (fs.existsSync(path.join(homeDir, '.config', 'kilo', 'agents', pluginId + '.md'))) return true;
|
|
130
142
|
if (fs.existsSync(path.join(homeDir, '.config', 'opencode', 'agents', 'gm.md'))) return true;
|
|
131
143
|
if (fs.existsSync(path.join(homeDir, '.config', 'kilo', 'agents', 'gm.md'))) return true;
|
|
144
|
+
if (pluginId === 'codex' && fs.existsSync(path.join(homeDir, '.codex', 'plugin.json'))) return true;
|
|
132
145
|
} catch (_) {}
|
|
133
146
|
return false;
|
|
134
147
|
};
|
package/package.json
CHANGED
package/static/index.html
CHANGED
|
@@ -864,9 +864,15 @@
|
|
|
864
864
|
display: inline-block;
|
|
865
865
|
}
|
|
866
866
|
|
|
867
|
+
.message-input-container {
|
|
868
|
+
position: relative;
|
|
869
|
+
flex: 1;
|
|
870
|
+
display: flex;
|
|
871
|
+
}
|
|
872
|
+
|
|
867
873
|
.message-textarea {
|
|
868
874
|
flex: 1;
|
|
869
|
-
padding: 0.625rem 0.875rem;
|
|
875
|
+
padding: 0.625rem 2.75rem 0.625rem 0.875rem;
|
|
870
876
|
border: none;
|
|
871
877
|
border-radius: 0.75rem;
|
|
872
878
|
background-color: var(--color-bg-secondary);
|
|
@@ -1144,11 +1150,14 @@
|
|
|
1144
1150
|
}
|
|
1145
1151
|
|
|
1146
1152
|
.voice-mic-btn {
|
|
1153
|
+
position: absolute;
|
|
1154
|
+
top: 4px;
|
|
1155
|
+
right: 4px;
|
|
1147
1156
|
display: flex;
|
|
1148
1157
|
align-items: center;
|
|
1149
1158
|
justify-content: center;
|
|
1150
|
-
width:
|
|
1151
|
-
height:
|
|
1159
|
+
width: 36px;
|
|
1160
|
+
height: 36px;
|
|
1152
1161
|
background: var(--color-bg-secondary);
|
|
1153
1162
|
color: var(--color-text-secondary);
|
|
1154
1163
|
border: 2px solid var(--color-border);
|
|
@@ -3187,21 +3196,23 @@
|
|
|
3187
3196
|
<select class="agent-selector cli-selector" data-cli-selector title="Select CLI tool"></select>
|
|
3188
3197
|
<select class="agent-selector sub-agent-selector" data-agent-selector title="Select sub-agent" style="display:none"></select>
|
|
3189
3198
|
<select class="model-selector" data-model-selector title="Select model" data-empty="true"></select>
|
|
3190
|
-
<
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
<
|
|
3199
|
-
<
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3199
|
+
<div class="message-input-container">
|
|
3200
|
+
<textarea
|
|
3201
|
+
class="message-textarea"
|
|
3202
|
+
data-message-input
|
|
3203
|
+
placeholder="Message AgentGUI... (Ctrl+Enter to send)"
|
|
3204
|
+
aria-label="Message input"
|
|
3205
|
+
rows="1"
|
|
3206
|
+
></textarea>
|
|
3207
|
+
<button class="voice-mic-btn" id="chatMicBtn" title="Record voice input" aria-label="Voice input">
|
|
3208
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
3209
|
+
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
|
3210
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
|
3211
|
+
<line x1="12" y1="19" x2="12" y2="23"/>
|
|
3212
|
+
<line x1="8" y1="23" x2="16" y2="23"/>
|
|
3213
|
+
</svg>
|
|
3214
|
+
</button>
|
|
3215
|
+
</div>
|
|
3205
3216
|
<button class="inject-btn" id="injectBtn" title="Inject instructions into running agent" aria-label="Inject instructions">
|
|
3206
3217
|
<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
|
|
3207
3218
|
<path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/>
|
|
@@ -1996,6 +1996,11 @@ class StreamingRenderer {
|
|
|
1996
1996
|
* Render generic event with formatted key-value pairs
|
|
1997
1997
|
*/
|
|
1998
1998
|
renderGeneric(event) {
|
|
1999
|
+
// Check if this is actually a file read with base64 image content
|
|
2000
|
+
if ((event.content?.source?.type === 'base64' || event.content?.type === 'base64') && event.path) {
|
|
2001
|
+
return this.renderFileRead(event);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1999
2004
|
const div = document.createElement('div');
|
|
2000
2005
|
div.className = 'event-generic mb-3 p-3 bg-gray-100 dark:bg-gray-800 rounded text-sm';
|
|
2001
2006
|
div.dataset.eventId = event.id || '';
|