agentgui 1.0.959 → 1.0.961

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.
@@ -18,6 +18,20 @@ const SUB_AGENT_MAP = {
18
18
  export function register(router, deps) {
19
19
  const { queries, wsOptimizer, broadcastSync, getProviderConfigs, saveProviderConfig, STARTUP_CWD, discoveredAgents, subscriptionIndex, activeChats } = deps;
20
20
 
21
+ // Short-lived per-session terminal-event buffer (finding 35): a turn that
22
+ // completes/errors/cancels while a client ws is down would otherwise be a
23
+ // fire-and-forget broadcast the client never sees, hanging it busy forever.
24
+ // A re-subscribing client gets the buffered terminal frame replayed.
25
+ const TERMINAL_TTL_MS = 60000;
26
+ const terminalEvents = new Map(); // sessionId -> terminal event
27
+ function recordTerminal(sessionId, event) {
28
+ terminalEvents.set(sessionId, event);
29
+ const t = setTimeout(() => {
30
+ if (terminalEvents.get(sessionId) === event) terminalEvents.delete(sessionId);
31
+ }, TERMINAL_TTL_MS);
32
+ if (typeof t.unref === 'function') t.unref();
33
+ }
34
+
21
35
  // --- agents.list: enumerate registered ACP agents + claude-code ---
22
36
  router.handle('agents.list', () => {
23
37
  const agents = registry.list().map(a => ({
@@ -55,7 +69,11 @@ export function register(router, deps) {
55
69
  subscriptionIndex.get(sid).add(ws);
56
70
  ws.subscriptions = ws.subscriptions || new Set();
57
71
  ws.subscriptions.add(sid);
58
- return { subscribed: true, sessionId: sid };
72
+ // Replay a buffered terminal frame (complete/error/cancelled) so a client
73
+ // that re-subscribes after a ws drop learns the turn already ended.
74
+ const term = terminalEvents.get(sid);
75
+ if (term) { try { wsOptimizer.sendToClient(ws, { ...term, replayed: true }); } catch {} }
76
+ return { subscribed: true, sessionId: sid, replayedTerminal: !!term };
59
77
  });
60
78
 
61
79
  // --- chat.sendMessage: start a one-shot streaming chat with an agent.
@@ -88,6 +106,9 @@ export function register(router, deps) {
88
106
 
89
107
  const ctrl = { aborted: false, proc: null, agentId, model, cwd, startedAt: Date.now() };
90
108
  activeChats.set(sessionId, ctrl);
109
+ // Push-driven hint so clients refresh active-session state without
110
+ // waiting for the 3s chat.active poll (finding 48).
111
+ broadcastSync({ type: 'chat_active_changed', reason: 'started', sessionId, agentId, timestamp: Date.now() });
91
112
 
92
113
  // Fire-and-forget. Errors broadcast as streaming_error.
93
114
  (async () => {
@@ -127,11 +148,20 @@ export function register(router, deps) {
127
148
  onPid: () => {}, onProcess: (proc) => { ctrl.proc = proc; },
128
149
  };
129
150
  await runClaudeWithStreaming(content, cwd, agentId, config);
130
- broadcastSync({ type: 'streaming_complete', sessionId, agentId, eventCount, timestamp: Date.now() });
151
+ if (!ctrl.aborted) {
152
+ const ev = { type: 'streaming_complete', sessionId, claudeSessionId: ctrl.claudeSessionId || null, agentId, eventCount, timestamp: Date.now() };
153
+ recordTerminal(sessionId, ev);
154
+ broadcastSync(ev);
155
+ }
131
156
  } catch (e) {
132
- broadcastSync({ type: 'streaming_error', sessionId, agentId, error: e.message || String(e), recoverable: false, timestamp: Date.now() });
157
+ if (!ctrl.aborted) {
158
+ const ev = { type: 'streaming_error', sessionId, claudeSessionId: ctrl.claudeSessionId || null, agentId, error: e.message || String(e), recoverable: false, timestamp: Date.now() };
159
+ recordTerminal(sessionId, ev);
160
+ broadcastSync(ev);
161
+ }
133
162
  } finally {
134
163
  activeChats.delete(sessionId);
164
+ broadcastSync({ type: 'chat_active_changed', reason: 'ended', sessionId, agentId, timestamp: Date.now() });
135
165
  }
136
166
  })();
137
167
 
@@ -142,7 +172,7 @@ export function register(router, deps) {
142
172
  router.handle('chat.active', () => {
143
173
  const sessions = [];
144
174
  for (const [sid, c] of activeChats) {
145
- sessions.push({ sessionId: sid, agentId: c.agentId || null, model: c.model || null, cwd: c.cwd || null, startedAt: c.startedAt || null, pid: c.proc?.pid || null });
175
+ sessions.push({ sessionId: sid, claudeSessionId: c.claudeSessionId || null, agentId: c.agentId || null, model: c.model || null, cwd: c.cwd || null, startedAt: c.startedAt || null, pid: c.proc?.pid || null });
146
176
  }
147
177
  return { sessions };
148
178
  });
@@ -154,8 +184,16 @@ export function register(router, deps) {
154
184
  const ctrl = activeChats.get(sid);
155
185
  if (!ctrl) return { cancelled: false, reason: 'not-found' };
156
186
  ctrl.aborted = true;
187
+ // Broadcast the terminal 'cancelled' frame BEFORE killing the proc so a
188
+ // remote cancellation (other tab, dashboard stop-all) does not read as a
189
+ // normal completion (finding 44). streaming_cancelled is in BROADCAST_TYPES
190
+ // so every connected client sees it; it is also buffered for re-subscribers.
191
+ const ev = { type: 'streaming_cancelled', sessionId: sid, claudeSessionId: ctrl.claudeSessionId || null, agentId: ctrl.agentId || null, cancelled: true, timestamp: Date.now() };
192
+ recordTerminal(sid, ev);
193
+ broadcastSync(ev);
157
194
  try { ctrl.proc?.kill?.(); } catch {}
158
195
  activeChats.delete(sid);
196
+ broadcastSync({ type: 'chat_active_changed', reason: 'cancelled', sessionId: sid, agentId: ctrl.agentId || null, timestamp: Date.now() });
159
197
  return { cancelled: true };
160
198
  });
161
199
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.959",
3
+ "version": "1.0.961",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
package/server.js CHANGED
@@ -70,6 +70,33 @@ const staticDir = path.join(rootDir, 'site', 'app');
70
70
  if (!fs.existsSync(staticDir)) fs.mkdirSync(staticDir, { recursive: true });
71
71
 
72
72
  const expressApp = createExpressApp({ queries, BASE_URL });
73
+
74
+ // ccsniff's /v1/history/sessions/:sid/events returns ALL events with no
75
+ // paging. Shim (mounted BEFORE the ccsniff router; node_modules untouched):
76
+ // when ?limit= is present, slice the most-recent window server-side and
77
+ // report { total, offset } so the client can page older on demand via
78
+ // ?before= (slice end index, exclusive; defaults to total).
79
+ function historyEventsLimitShim(req, res, next) {
80
+ if (!/\/v1\/history\/sessions\/[^/]+\/events$/.test(req.path)) return next();
81
+ const limit = parseInt(req.query.limit, 10);
82
+ if (!Number.isFinite(limit) || limit <= 0) return next();
83
+ const origJson = res.json.bind(res);
84
+ res.json = (body) => {
85
+ try {
86
+ if (body && Array.isArray(body.events)) {
87
+ const total = body.events.length;
88
+ const before = parseInt(req.query.before, 10);
89
+ const end = (Number.isFinite(before) && before >= 0) ? Math.min(before, total) : total;
90
+ const start = Math.max(0, end - limit);
91
+ body = { ...body, events: body.events.slice(start, end), total, offset: start };
92
+ }
93
+ } catch {}
94
+ return origJson(body);
95
+ };
96
+ next();
97
+ }
98
+ expressApp.use(historyEventsLimitShim);
99
+
73
100
  try {
74
101
  const historyRouter = await createHistoryRouter({ projectsDir: process.env.CLAUDE_PROJECTS_DIR });
75
102
  expressApp.use('/', historyRouter);