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 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:
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.541",
3
+ "version": "1.0.543",
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
@@ -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: 44px;
1151
- height: 44px;
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
- <textarea
3191
- class="message-textarea"
3192
- data-message-input
3193
- placeholder="Message AgentGUI... (Ctrl+Enter to send)"
3194
- aria-label="Message input"
3195
- rows="1"
3196
- ></textarea>
3197
- <button class="voice-mic-btn" id="chatMicBtn" title="Record voice input" aria-label="Voice input">
3198
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
3199
- <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
3200
- <path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
3201
- <line x1="12" y1="19" x2="12" y2="23"/>
3202
- <line x1="8" y1="23" x2="16" y2="23"/>
3203
- </svg>
3204
- </button>
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 || '';