agentgui 1.0.196 → 1.0.197

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.196",
3
+ "version": "1.0.197",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -5,7 +5,7 @@ import os from 'os';
5
5
  import zlib from 'zlib';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { WebSocketServer } from 'ws';
8
- import { execSync } from 'child_process';
8
+ import { execSync, spawn } from 'child_process';
9
9
  import { createRequire } from 'module';
10
10
  import { queries } from './database.js';
11
11
  import { runClaudeWithStreaming } from './lib/claude-runner.js';
@@ -66,6 +66,7 @@ const fsbrowse = require('fsbrowse');
66
66
  const SYSTEM_PROMPT = `Your output will be spoken aloud by a text-to-speech system. Write ONLY plain conversational sentences that sound natural when read aloud. Never use markdown, bold, italics, headers, bullet points, numbered lists, tables, or any formatting. Never use colons to introduce lists or options. Never use labels like "Option A" or "1." followed by a title. Instead of listing options, describe them conversationally in flowing sentences. For example, instead of "**Option 1**: Do X" say "One approach would be to do X." Keep sentences short and simple. Use transition words like "also", "another option", "or alternatively" to connect ideas. Write as if you are speaking to someone in a casual conversation.`;
67
67
 
68
68
  const activeExecutions = new Map();
69
+ const activeScripts = new Map();
69
70
  const messageQueues = new Map();
70
71
  const rateLimitState = new Map();
71
72
  const STUCK_AGENT_THRESHOLD_MS = 600000;
@@ -539,6 +540,77 @@ const server = http.createServer(async (req, res) => {
539
540
  return;
540
541
  }
541
542
 
543
+ const scriptsMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/scripts$/);
544
+ if (scriptsMatch && req.method === 'GET') {
545
+ const conv = queries.getConversation(scriptsMatch[1]);
546
+ if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
547
+ const wd = conv.workingDirectory || STARTUP_CWD;
548
+ let hasStart = false, hasDev = false;
549
+ try {
550
+ const pkg = JSON.parse(fs.readFileSync(path.join(wd, 'package.json'), 'utf-8'));
551
+ const scripts = pkg.scripts || {};
552
+ hasStart = !!scripts.start;
553
+ hasDev = !!scripts.dev;
554
+ } catch {}
555
+ const running = activeScripts.has(scriptsMatch[1]);
556
+ const runningScript = running ? activeScripts.get(scriptsMatch[1]).script : null;
557
+ sendJSON(req, res, 200, { hasStart, hasDev, running, runningScript });
558
+ return;
559
+ }
560
+
561
+ const runScriptMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/run-script$/);
562
+ if (runScriptMatch && req.method === 'POST') {
563
+ const conversationId = runScriptMatch[1];
564
+ const conv = queries.getConversation(conversationId);
565
+ if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
566
+ if (activeScripts.has(conversationId)) { sendJSON(req, res, 409, { error: 'Script already running' }); return; }
567
+ const body = await parseBody(req);
568
+ const script = body.script;
569
+ if (script !== 'start' && script !== 'dev') { sendJSON(req, res, 400, { error: 'Invalid script' }); return; }
570
+ const wd = conv.workingDirectory || STARTUP_CWD;
571
+ try {
572
+ const pkg = JSON.parse(fs.readFileSync(path.join(wd, 'package.json'), 'utf-8'));
573
+ if (!pkg.scripts || !pkg.scripts[script]) { sendJSON(req, res, 400, { error: `Script "${script}" not found` }); return; }
574
+ } catch { sendJSON(req, res, 400, { error: 'No package.json' }); return; }
575
+
576
+ const child = spawn('npm', ['run', script], { cwd: wd, stdio: ['ignore', 'pipe', 'pipe'], detached: true, env: { ...process.env, FORCE_COLOR: '1' } });
577
+ activeScripts.set(conversationId, { process: child, script, startTime: Date.now() });
578
+ broadcastSync({ type: 'script_started', conversationId, script, timestamp: Date.now() });
579
+
580
+ const onData = (stream) => (chunk) => {
581
+ broadcastSync({ type: 'script_output', conversationId, data: chunk.toString(), stream, timestamp: Date.now() });
582
+ };
583
+ child.stdout.on('data', onData('stdout'));
584
+ child.stderr.on('data', onData('stderr'));
585
+ child.on('error', (err) => {
586
+ activeScripts.delete(conversationId);
587
+ broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: err.message, timestamp: Date.now() });
588
+ });
589
+ child.on('close', (code) => {
590
+ activeScripts.delete(conversationId);
591
+ broadcastSync({ type: 'script_stopped', conversationId, code: code || 0, timestamp: Date.now() });
592
+ });
593
+ sendJSON(req, res, 200, { ok: true, script, pid: child.pid });
594
+ return;
595
+ }
596
+
597
+ const stopScriptMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/stop-script$/);
598
+ if (stopScriptMatch && req.method === 'POST') {
599
+ const conversationId = stopScriptMatch[1];
600
+ const entry = activeScripts.get(conversationId);
601
+ if (!entry) { sendJSON(req, res, 404, { error: 'No running script' }); return; }
602
+ try { process.kill(-entry.process.pid, 'SIGTERM'); } catch { try { entry.process.kill('SIGTERM'); } catch {} }
603
+ sendJSON(req, res, 200, { ok: true });
604
+ return;
605
+ }
606
+
607
+ const scriptStatusMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/script-status$/);
608
+ if (scriptStatusMatch && req.method === 'GET') {
609
+ const entry = activeScripts.get(scriptStatusMatch[1]);
610
+ sendJSON(req, res, 200, { running: !!entry, script: entry?.script || null });
611
+ return;
612
+ }
613
+
542
614
  if (pathOnly === '/api/agents' && req.method === 'GET') {
543
615
  sendJSON(req, res, 200, { agents: discoveredAgents });
544
616
  return;
@@ -1308,7 +1380,8 @@ const BROADCAST_TYPES = new Set([
1308
1380
  'message_created', 'conversation_created', 'conversation_updated',
1309
1381
  'conversations_updated', 'conversation_deleted', 'queue_status',
1310
1382
  'streaming_start', 'streaming_complete', 'streaming_error',
1311
- 'rate_limit_hit', 'rate_limit_clear'
1383
+ 'rate_limit_hit', 'rate_limit_clear',
1384
+ 'script_started', 'script_stopped', 'script_output'
1312
1385
  ]);
1313
1386
 
1314
1387
  const wsBatchQueues = new Map();
package/static/index.html CHANGED
@@ -15,6 +15,9 @@
15
15
  <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
16
16
  <noscript><link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css" rel="stylesheet"></noscript>
17
17
  <script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
18
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
19
+ <script defer src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
20
+ <script defer src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
18
21
 
19
22
  <style>
20
23
  *, *::before, *::after { box-sizing: border-box; }
@@ -441,6 +444,27 @@
441
444
  color: var(--color-text-primary);
442
445
  }
443
446
 
447
+ .script-buttons { display: flex; gap: 0.25rem; align-items: center; }
448
+ .header-icon-btn {
449
+ display: flex; align-items: center; justify-content: center;
450
+ width: 32px; height: 32px; background: none; border: none;
451
+ border-radius: 0.375rem; cursor: pointer; color: var(--color-text-secondary);
452
+ transition: background-color 0.15s, color 0.15s;
453
+ }
454
+ .header-icon-btn:hover { background-color: var(--color-bg-primary); color: var(--color-text-primary); }
455
+ .header-icon-btn svg { width: 16px; height: 16px; }
456
+ #scriptStartBtn { color: var(--color-success); }
457
+ #scriptStartBtn:hover { background-color: rgba(16,185,129,0.1); color: var(--color-success); }
458
+ .script-dev-btn { color: var(--color-info); }
459
+ .script-dev-btn:hover { background-color: rgba(8,145,178,0.1); color: var(--color-info); }
460
+ .script-stop-btn { color: var(--color-error); }
461
+ .script-stop-btn:hover { background-color: rgba(239,68,68,0.1); color: var(--color-error); }
462
+
463
+ .terminal-container {
464
+ flex: 1; display: flex; flex-direction: column; overflow: hidden; background: #1e1e1e;
465
+ }
466
+ .terminal-output { flex: 1; overflow: hidden; }
467
+
444
468
  /* --- View toggle bar --- */
445
469
  .view-toggle-bar {
446
470
  display: flex;
@@ -2199,6 +2223,17 @@
2199
2223
  <h1 class="header-title">AgentGUI</h1>
2200
2224
 
2201
2225
  <div class="header-controls">
2226
+ <div class="script-buttons" id="scriptButtons" style="display:none;">
2227
+ <button class="header-icon-btn" id="scriptStartBtn" title="Run start script" aria-label="Run start script" style="display:none;">
2228
+ <svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><polygon points="6,3 20,12 6,21"></polygon></svg>
2229
+ </button>
2230
+ <button class="header-icon-btn script-dev-btn" id="scriptDevBtn" title="Run dev script" aria-label="Run dev script" style="display:none;">
2231
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
2232
+ </button>
2233
+ <button class="header-icon-btn script-stop-btn" id="scriptStopBtn" title="Stop running script" aria-label="Stop running script" style="display:none;">
2234
+ <svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="5" y="5" width="14" height="14" rx="1"></rect></svg>
2235
+ </button>
2236
+ </div>
2202
2237
  <div class="status-badge">
2203
2238
  <div class="status-indicator" data-status="disconnected"></div>
2204
2239
  <span id="connectionStatus" data-status-indicator>Disconnected</span>
@@ -2224,6 +2259,7 @@
2224
2259
  <button class="view-toggle-btn active" data-view="chat">Chat</button>
2225
2260
  <button class="view-toggle-btn" data-view="files">Files</button>
2226
2261
  <button class="view-toggle-btn" data-view="voice">Voice</button>
2262
+ <button class="view-toggle-btn" data-view="terminal" id="terminalTabBtn" style="display:none;">Terminal</button>
2227
2263
  </div>
2228
2264
 
2229
2265
  <!-- Messages scroll area -->
@@ -2244,6 +2280,11 @@
2244
2280
  <iframe id="fileBrowserIframe" class="file-browser-iframe"></iframe>
2245
2281
  </div>
2246
2282
 
2283
+ <!-- Terminal output view -->
2284
+ <div id="terminalContainer" class="terminal-container" style="display:none;">
2285
+ <div id="terminalOutput" class="terminal-output"></div>
2286
+ </div>
2287
+
2247
2288
  <!-- Voice STT/TTS view -->
2248
2289
  <div id="voiceContainer" class="voice-container" style="display:none;">
2249
2290
  <div id="voiceScroll" class="voice-scroll">
@@ -2336,6 +2377,7 @@
2336
2377
  <script defer src="/gm/js/client.js"></script>
2337
2378
  <script type="module" src="/gm/js/voice.js"></script>
2338
2379
  <script defer src="/gm/js/features.js"></script>
2380
+ <script defer src="/gm/js/script-runner.js"></script>
2339
2381
 
2340
2382
  <script>
2341
2383
  const savedTheme = localStorage.getItem('theme') || 'light';
@@ -201,6 +201,7 @@
201
201
  var fileBrowser = document.getElementById('fileBrowserContainer');
202
202
  var fileIframe = document.getElementById('fileBrowserIframe');
203
203
  var voiceContainer = document.getElementById('voiceContainer');
204
+ var terminalContainer = document.getElementById('terminalContainer');
204
205
 
205
206
  if (!bar) return;
206
207
 
@@ -212,6 +213,7 @@
212
213
  if (execPanel) execPanel.style.display = view === 'chat' ? '' : 'none';
213
214
  if (fileBrowser) fileBrowser.style.display = view === 'files' ? 'flex' : 'none';
214
215
  if (voiceContainer) voiceContainer.style.display = view === 'voice' ? 'flex' : 'none';
216
+ if (terminalContainer) terminalContainer.style.display = view === 'terminal' ? 'flex' : 'none';
215
217
 
216
218
  if (view === 'files' && fileIframe && currentConversation) {
217
219
  var src = BASE + '/files/' + currentConversation + '/';
@@ -225,6 +227,8 @@
225
227
  } else if (view !== 'voice' && window.voiceModule) {
226
228
  window.voiceModule.deactivate();
227
229
  }
230
+
231
+ window.dispatchEvent(new CustomEvent('view-switched', { detail: { view: view } }));
228
232
  }
229
233
 
230
234
  function updateViewToggleVisibility() {
@@ -0,0 +1,219 @@
1
+ (function() {
2
+ const BASE = window.__BASE_URL || '';
3
+ let currentConversationId = null;
4
+ let scriptState = { running: false, script: null, hasStart: false, hasDev: false };
5
+ let terminal = null;
6
+ let fitAddon = null;
7
+ let hasTerminalContent = false;
8
+ let resizeObserver = null;
9
+
10
+ function init() {
11
+ setupListeners();
12
+ setupButtons();
13
+ }
14
+
15
+ function setupListeners() {
16
+ window.addEventListener('conversation-selected', function(e) {
17
+ currentConversationId = e.detail.conversationId;
18
+ hasTerminalContent = false;
19
+ if (terminal) terminal.clear();
20
+ hideTerminalTab();
21
+ checkScripts();
22
+ });
23
+
24
+ window.addEventListener('ws-message', function(e) {
25
+ const data = e.detail;
26
+ if (!data || !currentConversationId) return;
27
+ if (data.conversationId !== currentConversationId) return;
28
+
29
+ if (data.type === 'script_started') {
30
+ scriptState.running = true;
31
+ scriptState.script = data.script;
32
+ hasTerminalContent = false;
33
+ if (terminal) terminal.clear();
34
+ updateButtons();
35
+ showTerminalTab();
36
+ } else if (data.type === 'script_stopped') {
37
+ scriptState.running = false;
38
+ const msg = data.error ? data.error : ('exited with code ' + (data.code || 0));
39
+ if (terminal) terminal.writeln('\r\n\x1b[90m[process ' + msg + ']\x1b[0m');
40
+ updateButtons();
41
+ } else if (data.type === 'script_output') {
42
+ hasTerminalContent = true;
43
+ showTerminalTab();
44
+ if (terminal) terminal.write(data.data);
45
+ }
46
+ });
47
+
48
+ window.addEventListener('resize', debounce(fitTerminal, 200));
49
+ }
50
+
51
+ function setupButtons() {
52
+ var startBtn = document.getElementById('scriptStartBtn');
53
+ var devBtn = document.getElementById('scriptDevBtn');
54
+ var stopBtn = document.getElementById('scriptStopBtn');
55
+
56
+ if (startBtn) startBtn.addEventListener('click', function() { runScript('start'); });
57
+ if (devBtn) devBtn.addEventListener('click', function() { runScript('dev'); });
58
+ if (stopBtn) stopBtn.addEventListener('click', function() { stopScript(); });
59
+ }
60
+
61
+ function checkScripts() {
62
+ if (!currentConversationId) return;
63
+ fetch(BASE + '/api/conversations/' + currentConversationId + '/scripts')
64
+ .then(function(r) { return r.json(); })
65
+ .then(function(data) {
66
+ scriptState.hasStart = data.hasStart;
67
+ scriptState.hasDev = data.hasDev;
68
+ scriptState.running = data.running;
69
+ scriptState.script = data.runningScript;
70
+ updateButtons();
71
+ if (data.running || hasTerminalContent) showTerminalTab();
72
+ })
73
+ .catch(function() {
74
+ scriptState.hasStart = false;
75
+ scriptState.hasDev = false;
76
+ updateButtons();
77
+ });
78
+ }
79
+
80
+ function updateButtons() {
81
+ var container = document.getElementById('scriptButtons');
82
+ var startBtn = document.getElementById('scriptStartBtn');
83
+ var devBtn = document.getElementById('scriptDevBtn');
84
+ var stopBtn = document.getElementById('scriptStopBtn');
85
+
86
+ var showAny = scriptState.hasStart || scriptState.hasDev || scriptState.running;
87
+ if (container) container.style.display = showAny ? 'flex' : 'none';
88
+
89
+ if (scriptState.running) {
90
+ if (startBtn) startBtn.style.display = 'none';
91
+ if (devBtn) devBtn.style.display = 'none';
92
+ if (stopBtn) stopBtn.style.display = 'flex';
93
+ } else {
94
+ if (startBtn) startBtn.style.display = scriptState.hasStart ? 'flex' : 'none';
95
+ if (devBtn) devBtn.style.display = scriptState.hasDev ? 'flex' : 'none';
96
+ if (stopBtn) stopBtn.style.display = 'none';
97
+ }
98
+ }
99
+
100
+ function runScript(script) {
101
+ if (!currentConversationId || scriptState.running) return;
102
+ fetch(BASE + '/api/conversations/' + currentConversationId + '/run-script', {
103
+ method: 'POST',
104
+ headers: { 'Content-Type': 'application/json' },
105
+ body: JSON.stringify({ script: script })
106
+ })
107
+ .then(function(r) { return r.json(); })
108
+ .then(function(data) {
109
+ if (data.ok) {
110
+ scriptState.running = true;
111
+ scriptState.script = script;
112
+ hasTerminalContent = false;
113
+ updateButtons();
114
+ showTerminalTab();
115
+ switchToTerminalView();
116
+ ensureTerminal();
117
+ if (terminal) {
118
+ terminal.clear();
119
+ terminal.writeln('\x1b[36m[running npm run ' + script + ']\x1b[0m\r\n');
120
+ }
121
+ }
122
+ })
123
+ .catch(function(err) {
124
+ console.error('Failed to start script:', err);
125
+ });
126
+ }
127
+
128
+ function stopScript() {
129
+ if (!currentConversationId) return;
130
+ fetch(BASE + '/api/conversations/' + currentConversationId + '/stop-script', {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: '{}'
134
+ }).catch(function(err) {
135
+ console.error('Failed to stop script:', err);
136
+ });
137
+ }
138
+
139
+ function showTerminalTab() {
140
+ var btn = document.getElementById('terminalTabBtn');
141
+ if (btn) btn.style.display = '';
142
+ }
143
+
144
+ function hideTerminalTab() {
145
+ var btn = document.getElementById('terminalTabBtn');
146
+ if (btn) btn.style.display = 'none';
147
+ }
148
+
149
+ function switchToTerminalView() {
150
+ var bar = document.getElementById('viewToggleBar');
151
+ if (!bar) return;
152
+ var termBtn = bar.querySelector('[data-view="terminal"]');
153
+ if (termBtn) termBtn.click();
154
+ }
155
+
156
+ function ensureTerminal() {
157
+ if (terminal) return;
158
+ if (typeof window.Terminal === 'undefined') {
159
+ setTimeout(ensureTerminal, 200);
160
+ return;
161
+ }
162
+ var container = document.getElementById('terminalOutput');
163
+ if (!container) return;
164
+
165
+ terminal = new window.Terminal({
166
+ cursorBlink: false,
167
+ scrollback: 10000,
168
+ fontSize: 13,
169
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Menlo, Monaco, 'Courier New', monospace",
170
+ theme: { background: '#1e1e1e', foreground: '#d4d4d4' },
171
+ convertEol: true,
172
+ disableStdin: true
173
+ });
174
+
175
+ if (window.FitAddon) {
176
+ fitAddon = new window.FitAddon.FitAddon();
177
+ terminal.loadAddon(fitAddon);
178
+ }
179
+
180
+ terminal.open(container);
181
+ fitTerminal();
182
+
183
+ if (resizeObserver) resizeObserver.disconnect();
184
+ resizeObserver = new ResizeObserver(debounce(fitTerminal, 100));
185
+ resizeObserver.observe(container);
186
+ }
187
+
188
+ function fitTerminal() {
189
+ if (fitAddon) {
190
+ try { fitAddon.fit(); } catch {}
191
+ }
192
+ }
193
+
194
+ function debounce(fn, ms) {
195
+ var timer;
196
+ return function() {
197
+ clearTimeout(timer);
198
+ timer = setTimeout(fn, ms);
199
+ };
200
+ }
201
+
202
+ window.addEventListener('view-switched', function(e) {
203
+ if (e.detail && e.detail.view === 'terminal') {
204
+ ensureTerminal();
205
+ setTimeout(fitTerminal, 50);
206
+ }
207
+ });
208
+
209
+ if (document.readyState === 'loading') {
210
+ document.addEventListener('DOMContentLoaded', init);
211
+ } else {
212
+ init();
213
+ }
214
+
215
+ window.scriptRunner = {
216
+ getState: function() { return scriptState; },
217
+ getTerminal: function() { return terminal; }
218
+ };
219
+ })();