agentgui 1.0.407 → 1.0.408

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 CHANGED
@@ -559,7 +559,7 @@ export const queries = {
559
559
 
560
560
  getConversationsList() {
561
561
  const stmt = prep(
562
- 'SELECT id, title, agentType, created_at, updated_at, messageCount, workingDirectory, isStreaming, model FROM conversations WHERE status != ? ORDER BY updated_at DESC'
562
+ 'SELECT id, agentId, title, agentType, created_at, updated_at, messageCount, workingDirectory, isStreaming, model FROM conversations WHERE status != ? ORDER BY updated_at DESC'
563
563
  );
564
564
  return stmt.all('deleted');
565
565
  },
@@ -619,13 +619,13 @@ export const queries = {
619
619
  },
620
620
 
621
621
  getStreamingConversations() {
622
- const stmt = prep('SELECT id, title, claudeSessionId, agentType FROM conversations WHERE isStreaming = 1');
622
+ const stmt = prep('SELECT id, title, claudeSessionId, agentId, agentType, model FROM conversations WHERE isStreaming = 1');
623
623
  return stmt.all();
624
624
  },
625
625
 
626
626
  getResumableConversations() {
627
627
  const stmt = prep(
628
- "SELECT id, title, claudeSessionId, agentType, workingDirectory FROM conversations WHERE isStreaming = 1 AND claudeSessionId IS NOT NULL AND claudeSessionId != ''"
628
+ "SELECT id, title, claudeSessionId, agentId, agentType, workingDirectory, model FROM conversations WHERE isStreaming = 1 AND claudeSessionId IS NOT NULL AND claudeSessionId != ''"
629
629
  );
630
630
  return stmt.all();
631
631
  },
@@ -1399,25 +1399,7 @@ export const queries = {
1399
1399
  },
1400
1400
 
1401
1401
  permanentlyDeleteConversation(id) {
1402
- const conv = this.getConversation(id);
1403
- if (!conv) return false;
1404
-
1405
- // Delete associated Claude Code session file if it exists
1406
- if (conv.claudeSessionId) {
1407
- this.deleteClaudeSessionFile(conv.claudeSessionId);
1408
- }
1409
-
1410
- const deleteStmt = db.transaction(() => {
1411
- prep('DELETE FROM stream_updates WHERE conversationId = ?').run(id);
1412
- prep('DELETE FROM chunks WHERE conversationId = ?').run(id);
1413
- prep('DELETE FROM events WHERE conversationId = ?').run(id);
1414
- prep('DELETE FROM sessions WHERE conversationId = ?').run(id);
1415
- prep('DELETE FROM messages WHERE conversationId = ?').run(id);
1416
- prep('DELETE FROM conversations WHERE id = ?').run(id);
1417
- });
1418
-
1419
- deleteStmt();
1420
- return true;
1402
+ return this.deleteConversation(id);
1421
1403
  },
1422
1404
 
1423
1405
  cleanupEmptyConversations() {
@@ -0,0 +1,104 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AgentGUI - Multi-Agent AI Client</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ <link rel="icon" type="image/x-icon" href="https://github.com/AnEntrypoint/agentgui/raw/main/agentgui.ico">
9
+ </head>
10
+ <body>
11
+ <div class="container">
12
+ <header>
13
+ <div class="logo">
14
+ <svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
15
+ <rect width="64" height="64" rx="12" fill="url(#gradient)"/>
16
+ <path d="M32 16L44 32L32 48L20 32L32 16Z" stroke="white" stroke-width="3" fill="none"/>
17
+ <circle cx="32" cy="32" r="8" fill="white"/>
18
+ <defs>
19
+ <linearGradient id="gradient" x1="0" y1="0" x2="64" y2="64">
20
+ <stop offset="0%" stop-color="#6366f1"/>
21
+ <stop offset="100%" stop-color="#8b5cf6"/>
22
+ </linearGradient>
23
+ </defs>
24
+ </svg>
25
+ </div>
26
+ <h1>AgentGUI</h1>
27
+ <p class="subtitle">Multi-agent ACP client with real-time communication</p>
28
+ <div class="stats">
29
+ <div class="stat-item">
30
+ <span class="stat-value" id="weekly-downloads">Loading...</span>
31
+ <span class="stat-label">Weekly Downloads</span>
32
+ </div>
33
+ </div>
34
+ <div class="cta-buttons">
35
+ <a href="https://github.com/AnEntrypoint/agentgui" target="_blank" class="btn btn-primary" id="star-btn">
36
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
37
+ <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
38
+ </svg>
39
+ Star on GitHub
40
+ </a>
41
+ <a href="https://github.com/AnEntrypoint/agentgui#readme" target="_blank" class="btn btn-secondary">
42
+ View Documentation
43
+ </a>
44
+ </div>
45
+ </header>
46
+
47
+ <section class="features">
48
+ <h2>Features</h2>
49
+ <div class="feature-grid">
50
+ <div class="feature-card">
51
+ <div class="feature-icon">⚡</div>
52
+ <h3>Real-Time Streaming</h3>
53
+ <p>Live WebSocket sync with instant updates across multiple agents</p>
54
+ </div>
55
+ <div class="feature-card">
56
+ <div class="feature-icon">🤖</div>
57
+ <h3>Multi-Agent Support</h3>
58
+ <p>Use Claude Code, Gemini CLI, OpenCode, Goose, and more</p>
59
+ </div>
60
+ <div class="feature-card">
61
+ <div class="feature-icon">💾</div>
62
+ <h3>SQLite Persistence</h3>
63
+ <p>All conversations and events safely stored locally</p>
64
+ </div>
65
+ <div class="feature-card">
66
+ <div class="feature-icon">🎤</div>
67
+ <h3>Voice Integration</h3>
68
+ <p>Speech-to-text and text-to-speech with HuggingFace</p>
69
+ </div>
70
+ <div class="feature-card">
71
+ <div class="feature-icon">🔧</div>
72
+ <h3>Portable Executable</h3>
73
+ <p>Windows build with bundled models, no installation needed</p>
74
+ </div>
75
+ <div class="feature-card">
76
+ <div class="feature-icon">🌐</div>
77
+ <h3>Web Interface</h3>
78
+ <p>Modern browser-based UI accessible from any device</p>
79
+ </div>
80
+ </div>
81
+ </section>
82
+
83
+ <section class="quick-start">
84
+ <h2>Quick Start</h2>
85
+ <div class="code-block">
86
+ <code>npm install -g agentgui</code>
87
+ <button class="copy-btn" onclick="copyCode(this)">Copy</button>
88
+ </div>
89
+ <p class="note">Then run <code>agentgui</code> and open <a href="http://localhost:3000/gm/">http://localhost:3000/gm/</a></p>
90
+ </section>
91
+
92
+ <footer>
93
+ <p>Built with ❤️ by the AgentGUI team</p>
94
+ <p class="footer-links">
95
+ <a href="https://github.com/AnEntrypoint/agentgui">GitHub</a> •
96
+ <a href="https://github.com/AnEntrypoint/agentgui/issues">Issues</a> •
97
+ <a href="https://github.com/AnEntrypoint/agentgui#readme">Docs</a>
98
+ </p>
99
+ </footer>
100
+ </div>
101
+
102
+ <script src="script.js"></script>
103
+ </body>
104
+ </html>
package/docs/script.js ADDED
@@ -0,0 +1,38 @@
1
+ document.addEventListener('DOMContentLoaded', async () => {
2
+ const statsElement = document.getElementById('weekly-downloads');
3
+
4
+ try {
5
+ const response = await fetch('stats.json');
6
+ if (!response.ok) {
7
+ throw new Error(`HTTP ${response.status}`);
8
+ }
9
+ const data = await response.json();
10
+ const count = data.downloads ?? data.weeklyDownloads ?? 0;
11
+ statsElement.textContent = formatNumber(count);
12
+ } catch (error) {
13
+ console.warn('Failed to load stats:', error);
14
+ statsElement.textContent = 'N/A';
15
+ }
16
+ });
17
+
18
+ function formatNumber(num) {
19
+ if (num >= 1000000) {
20
+ return (num / 1000000).toFixed(1) + 'M';
21
+ }
22
+ if (num >= 1000) {
23
+ return (num / 1000).toFixed(1) + 'K';
24
+ }
25
+ return num.toLocaleString();
26
+ }
27
+
28
+ function copyCode(button) {
29
+ const codeBlock = button.parentElement;
30
+ const code = codeBlock.querySelector('code').textContent;
31
+ navigator.clipboard.writeText(code).then(() => {
32
+ const originalText = button.textContent;
33
+ button.textContent = 'Copied!';
34
+ setTimeout(() => {
35
+ button.textContent = originalText;
36
+ }, 2000);
37
+ });
38
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "downloads": 18380,
3
+ "lastUpdated": "2026-02-26T12:52:28.883Z"
4
+ }
@@ -0,0 +1,301 @@
1
+ :root {
2
+ --primary: #6366f1;
3
+ --primary-dark: #4f46e5;
4
+ --secondary: #10b981;
5
+ --bg: #0f172a;
6
+ --bg-card: #1e293b;
7
+ --text: #f1f5f9;
8
+ --text-muted: #94a3b8;
9
+ --border: #334155;
10
+ --gradient: linear-gradient(135deg, #6366f1, #8b5cf6);
11
+ }
12
+
13
+ * {
14
+ margin: 0;
15
+ padding: 0;
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ body {
20
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
21
+ background: var(--bg);
22
+ color: var(--text);
23
+ line-height: 1.6;
24
+ min-height: 100vh;
25
+ }
26
+
27
+ .container {
28
+ max-width: 1200px;
29
+ margin: 0 auto;
30
+ padding: 2rem;
31
+ }
32
+
33
+ header {
34
+ text-align: center;
35
+ padding: 3rem 0;
36
+ border-bottom: 1px solid var(--border);
37
+ margin-bottom: 3rem;
38
+ }
39
+
40
+ .logo {
41
+ margin-bottom: 1rem;
42
+ }
43
+
44
+ .logo svg {
45
+ filter: drop-shadow(0 4px 6px rgba(99, 102, 241, 0.3));
46
+ }
47
+
48
+ h1 {
49
+ font-size: 3rem;
50
+ font-weight: 800;
51
+ background: var(--gradient);
52
+ -webkit-background-clip: text;
53
+ -webkit-text-fill-color: transparent;
54
+ background-clip: text;
55
+ margin-bottom: 0.5rem;
56
+ }
57
+
58
+ .subtitle {
59
+ font-size: 1.25rem;
60
+ color: var(--text-muted);
61
+ margin-bottom: 2rem;
62
+ }
63
+
64
+ .stats {
65
+ display: inline-flex;
66
+ gap: 2rem;
67
+ padding: 1.5rem 2rem;
68
+ background: var(--bg-card);
69
+ border: 1px solid var(--border);
70
+ border-radius: 12px;
71
+ margin: 2rem 0;
72
+ }
73
+
74
+ .stat-item {
75
+ display: flex;
76
+ flex-direction: column;
77
+ align-items: center;
78
+ }
79
+
80
+ .stat-value {
81
+ font-size: 2.5rem;
82
+ font-weight: 700;
83
+ color: var(--primary);
84
+ line-height: 1;
85
+ }
86
+
87
+ .stat-label {
88
+ font-size: 0.875rem;
89
+ color: var(--text-muted);
90
+ margin-top: 0.5rem;
91
+ }
92
+
93
+ .cta-buttons {
94
+ display: flex;
95
+ gap: 1rem;
96
+ justify-content: center;
97
+ flex-wrap: wrap;
98
+ margin: 2rem 0;
99
+ }
100
+
101
+ .btn {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ gap: 0.5rem;
105
+ padding: 0.875rem 1.75rem;
106
+ border-radius: 8px;
107
+ font-weight: 600;
108
+ text-decoration: none;
109
+ transition: all 0.2s;
110
+ border: none;
111
+ cursor: pointer;
112
+ font-size: 1rem;
113
+ }
114
+
115
+ .btn-primary {
116
+ background: var(--gradient);
117
+ color: white;
118
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
119
+ }
120
+
121
+ .btn-primary:hover {
122
+ transform: translateY(-2px);
123
+ box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
124
+ }
125
+
126
+ .btn-secondary {
127
+ background: var(--bg-card);
128
+ color: var(--text);
129
+ border: 1px solid var(--border);
130
+ }
131
+
132
+ .btn-secondary:hover {
133
+ border-color: var(--primary);
134
+ color: var(--primary);
135
+ }
136
+
137
+ section {
138
+ margin: 4rem 0;
139
+ }
140
+
141
+ h2 {
142
+ font-size: 2rem;
143
+ margin-bottom: 2rem;
144
+ text-align: center;
145
+ color: var(--text);
146
+ }
147
+
148
+ .feature-grid {
149
+ display: grid;
150
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
151
+ gap: 1.5rem;
152
+ }
153
+
154
+ .feature-card {
155
+ background: var(--bg-card);
156
+ border: 1px solid var(--border);
157
+ border-radius: 12px;
158
+ padding: 1.5rem;
159
+ transition: transform 0.2s, border-color 0.2s;
160
+ }
161
+
162
+ .feature-card:hover {
163
+ transform: translateY(-4px);
164
+ border-color: var(--primary);
165
+ }
166
+
167
+ .feature-icon {
168
+ font-size: 2.5rem;
169
+ margin-bottom: 1rem;
170
+ }
171
+
172
+ .feature-card h3 {
173
+ font-size: 1.25rem;
174
+ margin-bottom: 0.5rem;
175
+ color: var(--text);
176
+ }
177
+
178
+ .feature-card p {
179
+ color: var(--text-muted);
180
+ font-size: 0.95rem;
181
+ }
182
+
183
+ .quick-start {
184
+ background: var(--bg-card);
185
+ border: 1px solid var(--border);
186
+ border-radius: 12px;
187
+ padding: 2rem;
188
+ text-align: center;
189
+ }
190
+
191
+ .code-block {
192
+ position: relative;
193
+ background: var(--bg);
194
+ border: 1px solid var(--border);
195
+ border-radius: 8px;
196
+ padding: 1.25rem;
197
+ margin: 1.5rem auto;
198
+ max-width: 600px;
199
+ font-family: 'Fira Code', 'Consolas', monospace;
200
+ font-size: 1rem;
201
+ }
202
+
203
+ .code-block code {
204
+ color: var(--secondary);
205
+ word-break: break-all;
206
+ }
207
+
208
+ .copy-btn {
209
+ position: absolute;
210
+ top: 0.5rem;
211
+ right: 0.5rem;
212
+ background: var(--border);
213
+ color: var(--text-muted);
214
+ border: none;
215
+ padding: 0.25rem 0.75rem;
216
+ border-radius: 4px;
217
+ font-size: 0.75rem;
218
+ cursor: pointer;
219
+ transition: all 0.2s;
220
+ }
221
+
222
+ .copy-btn:hover {
223
+ background: var(--primary);
224
+ color: white;
225
+ }
226
+
227
+ .note {
228
+ color: var(--text-muted);
229
+ margin-top: 1rem;
230
+ }
231
+
232
+ .note code {
233
+ background: var(--bg);
234
+ padding: 0.125rem 0.375rem;
235
+ border-radius: 4px;
236
+ font-family: 'Fira Code', 'Consolas', monospace;
237
+ color: var(--secondary);
238
+ }
239
+
240
+ .note a {
241
+ color: var(--primary);
242
+ text-decoration: none;
243
+ }
244
+
245
+ .note a:hover {
246
+ text-decoration: underline;
247
+ }
248
+
249
+ footer {
250
+ text-align: center;
251
+ padding-top: 3rem;
252
+ border-top: 1px solid var(--border);
253
+ margin-top: 4rem;
254
+ color: var(--text-muted);
255
+ }
256
+
257
+ .footer-links {
258
+ margin-top: 0.5rem;
259
+ }
260
+
261
+ .footer-links a {
262
+ color: var(--text-muted);
263
+ text-decoration: none;
264
+ margin: 0 0.5rem;
265
+ transition: color 0.2s;
266
+ }
267
+
268
+ .footer-links a:hover {
269
+ color: var(--primary);
270
+ }
271
+
272
+ @media (max-width: 768px) {
273
+ h1 {
274
+ font-size: 2rem;
275
+ }
276
+
277
+ .stats {
278
+ flex-direction: column;
279
+ gap: 1rem;
280
+ padding: 1rem;
281
+ }
282
+
283
+ .stat-value {
284
+ font-size: 2rem;
285
+ }
286
+
287
+ .cta-buttons {
288
+ flex-direction: column;
289
+ align-items: center;
290
+ }
291
+
292
+ .btn {
293
+ width: 100%;
294
+ max-width: 300px;
295
+ justify-content: center;
296
+ }
297
+
298
+ .feature-grid {
299
+ grid-template-columns: 1fr;
300
+ }
301
+ }
@@ -1,4 +1,4 @@
1
- import { spawn } from 'child_process';
1
+ import { spawn, spawnSync } from 'child_process';
2
2
 
3
3
  const isWindows = process.platform === 'win32';
4
4
 
@@ -319,7 +319,7 @@ class AgentRunner {
319
319
  }
320
320
  }
321
321
  };
322
- proc.stdin.on('error', () => {});
322
+ proc.stdin.on('error', () => {});
323
323
  proc.stdin.write(JSON.stringify(initRequest) + '\n');
324
324
 
325
325
  let sessionCreated = false;
@@ -499,8 +499,6 @@ class AgentRegistry {
499
499
  }
500
500
 
501
501
  listACPAvailable() {
502
- const { spawnSync } = require('child_process');
503
- const isWindows = process.platform === 'win32';
504
502
  return this.list().filter(agent => {
505
503
  try {
506
504
  const whichCmd = isWindows ? 'where' : 'which';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.407",
3
+ "version": "1.0.408",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/readme.md CHANGED
@@ -1,6 +1,9 @@
1
- # AgentGUI
2
-
3
- A multi-agent GUI for AI coding assistants. Connects to CLI-based agents (Claude Code, Gemini CLI, OpenCode, Goose, and others) and provides a web interface with real-time streaming output.
1
+ # AgentGUI
2
+
3
+ [![GitHub Pages](https://img.shields.io/badge/GitHub_Pages-Enabled-blue?logo=github)](https://anentrypoint.github.io/agentgui/)
4
+ [![Weekly Downloads](https://img.shields.io/npm/dw/agentgui?color=brightgreen)](https://www.npmjs.com/package/agentgui)
5
+
6
+ A multi-agent GUI for AI coding assistants. Connects to CLI-based agents (Claude Code, Gemini CLI, OpenCode, Goose, and others) and provides a web interface with real-time streaming output.
4
7
 
5
8
  ## Quick Start
6
9
 
@@ -18,7 +21,18 @@ npm install
18
21
  npm run dev
19
22
  ```
20
23
 
21
- Open `http://localhost:3000` in your browser.
24
+ Open `http://localhost:3000` in your browser.
25
+
26
+ ## 📊 Project Stats
27
+
28
+ Check out our **[GitHub Pages site](https://anentrypoint.github.io/agentgui/)** for:
29
+
30
+ - 📈 Live weekly download statistics
31
+ - ⭐ Star the project button
32
+ - ✨ Feature highlights
33
+ - 🚀 Quick start guide
34
+
35
+ The site automatically updates on every push to main with the latest npm download data.
22
36
 
23
37
  ## What It Does
24
38
 
package/server.js CHANGED
@@ -34,6 +34,11 @@ process.on('unhandledRejection', (reason, promise) => {
34
34
  if (reason instanceof Error) console.error(reason.stack);
35
35
  });
36
36
 
37
+ process.on('SIGINT', () => { console.log('[SIGNAL] SIGINT received (ignored - uncrashable)'); });
38
+ process.on('SIGHUP', () => { console.log('[SIGNAL] SIGHUP received (ignored - uncrashable)'); });
39
+ process.on('beforeExit', (code) => { console.log('[PROCESS] beforeExit with code:', code); });
40
+ process.on('exit', (code) => { console.log('[PROCESS] exit with code:', code); });
41
+
37
42
  const ttsTextAccumulators = new Map();
38
43
 
39
44
  let speechModule = null;
@@ -1047,6 +1052,7 @@ const server = http.createServer(async (req, res) => {
1047
1052
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
1048
1053
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
1049
1054
  if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
1055
+ if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') return;
1050
1056
 
1051
1057
  const pathOnly = req.url.split('?')[0];
1052
1058
 
@@ -4044,7 +4050,8 @@ if (watch) {
4044
4050
  } catch (e) { console.error('Watch error:', e.message); }
4045
4051
  }
4046
4052
 
4047
- process.on('SIGTERM', async () => {
4053
+ process.on('SIGTERM', () => {
4054
+ console.log('[SIGNAL] SIGTERM received - graceful shutdown');
4048
4055
  wss.close(() => server.close(() => process.exit(0)));
4049
4056
  });
4050
4057
 
package/static/index.html CHANGED
@@ -491,10 +491,10 @@
491
491
  color: var(--color-text-secondary); user-select: none;
492
492
  }
493
493
 
494
- .terminal-container {
495
- flex: 1; display: flex; flex-direction: column; overflow: hidden; background: #1e1e1e;
496
- }
497
- .terminal-output { flex: 1; overflow: hidden; position: relative; }
494
+ .terminal-container {
495
+ flex: 1; display: flex; flex-direction: column; overflow: hidden; background: #1e1e1e; min-height: 0;
496
+ }
497
+ .terminal-output { flex: 1; overflow: hidden; position: relative; min-height: 0; }
498
498
 
499
499
  /* --- View toggle bar --- */
500
500
  .view-toggle-bar {
@@ -1324,15 +1324,20 @@
1324
1324
  .sidebar-list { contain: strict; content-visibility: auto; }
1325
1325
  .message { contain: layout style; content-visibility: auto; contain-intrinsic-size: auto 80px; }
1326
1326
 
1327
- .voice-block .voice-result-stats {
1328
- font-size: 0.8rem;
1329
- color: var(--color-text-secondary);
1330
- margin-top: 0.5rem;
1331
- padding-top: 0.5rem;
1332
- border-top: 1px solid var(--color-border);
1333
- }
1334
-
1335
- .voice-reread-btn {
1327
+ .voice-block .voice-result-stats {
1328
+ font-size: 0.8rem;
1329
+ color: var(--color-text-secondary);
1330
+ margin-top: 0.5rem;
1331
+ padding-top: 0.5rem;
1332
+ border-top: 1px solid var(--color-border);
1333
+ }
1334
+
1335
+ .voice-block-content {
1336
+ white-space: pre-wrap;
1337
+ display: block;
1338
+ }
1339
+
1340
+ .voice-reread-btn {
1336
1341
  position: absolute;
1337
1342
  top: 0.5rem;
1338
1343
  right: 0.5rem;
@@ -166,7 +166,6 @@ class AgentGUIClient {
166
166
 
167
167
  this.wsManager.on('error', (data) => {
168
168
  console.error('WebSocket error:', data);
169
- this.showError('Connection error: ' + (data.error?.message || 'unknown'));
170
169
  });
171
170
 
172
171
  this.wsManager.on('latency_update', (data) => {
@@ -34,39 +34,38 @@ class EventConsolidator {
34
34
  return { consolidated: result, stats };
35
35
  }
36
36
 
37
- _mergeTextBlocks(chunks, stats) {
38
- const result = [];
39
- let pending = null;
40
- const MAX_MERGE = 50 * 1024;
41
-
42
- for (const c of chunks) {
43
- if (c.block?.type === 'text') {
44
- if (pending) {
45
- const pendingText = pending.block.text || '';
46
- const newText = c.block.text || '';
47
- const combined = pendingText + newText;
48
- if (combined.length <= MAX_MERGE) {
49
- const needsSpace = pendingText.length > 0 && !pendingText.endsWith(' ') && !pendingText.endsWith('\n') && newText.length > 0 && !newText.startsWith(' ') && !newText.startsWith('\n');
50
- pending = {
51
- ...pending,
52
- block: { ...pending.block, text: needsSpace ? pendingText + ' ' + newText : combined },
53
- created_at: c.created_at,
54
- _mergedSequences: [...(pending._mergedSequences || [pending.sequence]), c.sequence]
55
- };
56
- stats.textMerged++;
57
- continue;
58
- }
59
- }
60
- if (pending) result.push(pending);
61
- pending = { ...c, _mergedSequences: [c.sequence] };
62
- } else {
63
- if (pending) { result.push(pending); pending = null; }
64
- result.push(c);
65
- }
66
- }
67
- if (pending) result.push(pending);
68
- return result;
69
- }
37
+ _mergeTextBlocks(chunks, stats) {
38
+ const result = [];
39
+ let pending = null;
40
+ const MAX_MERGE = 50 * 1024;
41
+
42
+ for (const c of chunks) {
43
+ if (c.block?.type === 'text') {
44
+ if (pending) {
45
+ const pendingText = pending.block.text || '';
46
+ const newText = c.block.text || '';
47
+ const combined = pendingText + newText;
48
+ if (combined.length <= MAX_MERGE) {
49
+ pending = {
50
+ ...pending,
51
+ block: { ...pending.block, text: combined },
52
+ created_at: c.created_at,
53
+ _mergedSequences: [...(pending._mergedSequences || [pending.sequence]), c.sequence]
54
+ };
55
+ stats.textMerged++;
56
+ continue;
57
+ }
58
+ }
59
+ if (pending) result.push(pending);
60
+ pending = { ...c, _mergedSequences: [c.sequence] };
61
+ } else {
62
+ if (pending) { result.push(pending); pending = null; }
63
+ result.push(c);
64
+ }
65
+ }
66
+ if (pending) result.push(pending);
67
+ return result;
68
+ }
70
69
 
71
70
  _collapseToolPairs(chunks, stats) {
72
71
  const toolUseMap = {};
@@ -578,9 +578,9 @@
578
578
  }
579
579
  }
580
580
 
581
- function stripHtml(text) {
582
- return text.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
583
- }
581
+ function stripHtml(text) {
582
+ return text.replace(/<[^>]*>/g, '').replace(/[ \t]+/g, ' ').trim();
583
+ }
584
584
 
585
585
  function addVoiceBlock(text, isUser) {
586
586
  var container = document.getElementById('voiceMessages');