agentgui 1.0.928 → 1.0.930

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/AGENTS.md CHANGED
@@ -104,3 +104,13 @@ Paired changes:
104
104
  - better-sqlite3 bumped ^12.6.2 → ^12.9.0
105
105
 
106
106
  bun start was already working (bun has native sqlite support via `bun:sqlite`). The node path was broken due to the missing native binding. Only compile from source fixes the node path.
107
+
108
+ ## webjsx applyDiff Array Keying (2026-05-04)
109
+
110
+ **Children arrays that mix keyed VElements with raw text/numbers crash in `vendor/webjsx/applyDiff.js`.**
111
+
112
+ `webjsx` requires all siblings in an array prop (children, right, left, breadcrumbs, etc.) to either ALL be VElements with keys or ALL be non-keyed. Passing `Crumb({ right: [h('span', { key: 'a' }, 'A'), h('span', { key: 'b' }, 'B'), '● connected'] })` crashes on render with `TypeError: Cannot read properties of undefined (reading 'key')` at line 247420.js when the render loop tries to read `.key` on the bare string.
113
+
114
+ Fix: wrap any bare strings as keyed VElements: `h('span', { key: 'dot' }, '● connected')`. Rule: if ANY sibling in an array has a key, ALL siblings must be VElements (no raw strings or numbers).
115
+
116
+ Surfaced 2026-05-04 while validating the chat surface in `site/app/`. Fix applied to crumbRight construction in `site/app/js/app.js` view().
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.928",
3
+ "version": "1.0.930",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -18,9 +18,16 @@ const state = {
18
18
  events: [],
19
19
  searchQ: '',
20
20
  searchHits: null,
21
+ live: { es: null, connected: false, lastEventTs: 0, error: null, eventCount: 0 },
21
22
  };
22
23
 
23
24
  let render;
25
+ let renderScheduled = false;
26
+ function scheduleRender() {
27
+ if (renderScheduled) return;
28
+ renderScheduled = true;
29
+ requestAnimationFrame(() => { renderScheduled = false; render(); });
30
+ }
24
31
 
25
32
  function timeNow() {
26
33
  const d = new Date();
@@ -28,14 +35,74 @@ function timeNow() {
28
35
  }
29
36
 
30
37
  function navTo(tab) {
38
+ const prev = state.tab;
31
39
  state.tab = tab;
32
- if (tab === 'history') refreshHistory();
40
+ if (tab === 'history') {
41
+ refreshHistory();
42
+ openLiveStream();
43
+ } else if (prev === 'history') {
44
+ closeLiveStream();
45
+ }
33
46
  render();
34
47
  }
35
48
 
49
+ function openLiveStream() {
50
+ if (state.live.es) return;
51
+ state.live.error = null;
52
+ state.live.connected = false;
53
+ try {
54
+ state.live.es = B.streamHistory(state.backend, (kind, data) => {
55
+ state.live.lastEventTs = Date.now();
56
+ state.live.eventCount++;
57
+ if (kind === 'hello') {
58
+ state.live.connected = true;
59
+ } else if (kind === 'event' && data) {
60
+ if (state.selectedSid && data.sid === state.selectedSid) {
61
+ state.events.push(data);
62
+ }
63
+ const sess = state.sessions.find(s => s.sid === data.sid);
64
+ if (sess) {
65
+ sess.events = (sess.events || 0) + 1;
66
+ sess.last = data.ts || Date.now();
67
+ if (data.type === 'tool_use') sess.tools = (sess.tools || 0) + 1;
68
+ if (data.isError) sess.errors = (sess.errors || 0) + 1;
69
+ } else {
70
+ refreshHistory();
71
+ return;
72
+ }
73
+ } else if (kind === 'conversation') {
74
+ refreshHistory();
75
+ return;
76
+ } else if (kind === 'error' && data) {
77
+ state.live.error = data.error || 'stream error';
78
+ }
79
+ scheduleRender();
80
+ });
81
+ state.live.es.addEventListener('error', () => {
82
+ state.live.connected = false;
83
+ state.live.error = 'connection lost';
84
+ scheduleRender();
85
+ });
86
+ } catch (e) {
87
+ state.live.error = e.message;
88
+ state.live.es = null;
89
+ }
90
+ }
91
+
92
+ function closeLiveStream() {
93
+ if (!state.live.es) return;
94
+ try { state.live.es.close(); } catch {}
95
+ state.live.es = null;
96
+ state.live.connected = false;
97
+ }
98
+
36
99
  function view() {
37
100
  const ok = state.health.status === 'ok';
38
- const dot = h('span', { key: 'dot' }, ok ? '● connected' : '○ offline');
101
+ const liveActive = state.tab === 'history' && state.live.connected && (Date.now() - state.live.lastEventTs < 30000);
102
+ const dotText = state.tab === 'history'
103
+ ? (state.live.error ? '○ stream offline' : (liveActive ? '● live · ' + state.live.eventCount : (state.live.connected ? '● live' : '◌ connecting…')))
104
+ : (ok ? '● connected' : '○ offline');
105
+ const dot = h('span', { key: 'dot' }, dotText);
39
106
 
40
107
  const topbar = Topbar({
41
108
  brand: 'agentgui',