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 +1 -1
- package/server.js +75 -2
- package/static/index.html +42 -0
- package/static/js/features.js +4 -0
- package/static/js/script-runner.js +219 -0
package/package.json
CHANGED
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';
|
package/static/js/features.js
CHANGED
|
@@ -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
|
+
})();
|