v1.3.0: Full terminal with shell execution, APK build/install, AI deploy pipeline
This commit is contained in:
@@ -906,3 +906,220 @@ a:hover { text-decoration: underline; }
|
||||
.mode-cards { gap: 8px; }
|
||||
.mode-card { padding: 10px 8px; }
|
||||
}
|
||||
|
||||
/* Terminal Full Screen */
|
||||
.term-screen-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #0d0d0d;
|
||||
}
|
||||
.term-screen-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
min-height: 52px;
|
||||
}
|
||||
.term-screen-header h2 { font-size: 16px; flex: 1; }
|
||||
.term-screen-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.term-cwd-display {
|
||||
font-family: 'Fira Code', 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--success);
|
||||
background: rgba(46, 213, 115, 0.1);
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.term-output {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
font-family: 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #e0e0e0;
|
||||
background: #0d0d0d;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.term-output .term-line { margin-bottom: 1px; }
|
||||
.term-output .term-cmd { color: var(--success); font-weight: 700; }
|
||||
.term-output .term-err { color: var(--danger); }
|
||||
.term-output .term-info { color: var(--accent); }
|
||||
.term-output .term-success { color: var(--success); }
|
||||
.term-output .term-warning { color: var(--warning); }
|
||||
.term-output .term-path { color: var(--accent); text-decoration: underline; }
|
||||
.term-input-area {
|
||||
padding: 8px 12px 16px;
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.term-quick-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.term-quick-actions::-webkit-scrollbar { display: none; }
|
||||
.term-quick-btn {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-secondary);
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-family: 'Fira Code', monospace;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.term-quick-btn:active { background: var(--accent-dim); color: var(--accent); }
|
||||
.term-input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.term-prompt {
|
||||
color: var(--success);
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.term-input {
|
||||
flex: 1;
|
||||
background: #0d0d0d;
|
||||
border: 1px solid var(--border);
|
||||
color: #e0e0e0;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-family: 'Fira Code', monospace;
|
||||
outline: none;
|
||||
}
|
||||
.term-input:focus { border-color: var(--accent); }
|
||||
.term-run-btn, .term-stop-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: var(--success);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.term-stop-btn { background: var(--danger); }
|
||||
|
||||
.mode-btn-term {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Deploy button in messages */
|
||||
.deploy-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: linear-gradient(135deg, var(--success), var(--accent));
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
margin-top: 6px;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.deploy-btn:hover { opacity: 0.9; transform: scale(1.02); }
|
||||
.deploy-btn:active { transform: scale(0.98); }
|
||||
|
||||
.install-apk-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* Dev Setup Screen */
|
||||
.devsetup-status {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.devsetup-status p { margin-bottom: 6px; }
|
||||
.devsetup-progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.devsetup-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--accent), var(--success));
|
||||
border-radius: 4px;
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.devsetup-progress-text {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Command history popup */
|
||||
.term-history {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 10;
|
||||
display: none;
|
||||
}
|
||||
.term-history.visible { display: block; }
|
||||
.term-history-item {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-family: 'Fira Code', monospace;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.term-history-item:hover { background: var(--accent-dim); color: var(--text-primary); }
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
<button class="mode-btn" data-mode="coding">Coding</button>
|
||||
<button class="mode-btn" data-mode="brainstorm">Brainstorm</button>
|
||||
<button class="mode-btn" data-mode="agentic">Agentic</button>
|
||||
<button class="mode-btn mode-btn-term" data-mode="terminal" style="background:var(--success);border-color:var(--success);color:white">▪ Term</button>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<textarea id="message-input" placeholder="Type your message..." rows="1"></textarea>
|
||||
@@ -127,6 +128,61 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="terminal-screen" class="screen">
|
||||
<div class="term-screen-container">
|
||||
<div class="term-screen-header">
|
||||
<button id="term-back-btn" class="icon-btn">←</button>
|
||||
<h2>Terminal</h2>
|
||||
<div class="term-screen-header-right">
|
||||
<span id="term-cwd-display" class="term-cwd-display">~</span>
|
||||
<button id="term-setup-tools-btn" class="icon-btn" title="Setup Dev Tools">🛠</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="term-output" class="term-output"></div>
|
||||
<div class="term-input-area">
|
||||
<div class="term-quick-actions">
|
||||
<button class="term-quick-btn" data-cmd="ls -la">ls</button>
|
||||
<button class="term-quick-btn" data-cmd="pwd">pwd</button>
|
||||
<button class="term-quick-btn" data-cmd="cat ">cat</button>
|
||||
<button class="term-quick-btn" data-cmd="mkdir -p ">mkdir</button>
|
||||
<button class="term-quick-btn" data-cmd="which aapt2 java ecj d8 2>/dev/null">tools</button>
|
||||
<button class="term-quick-btn" data-cmd="df -h . && free -h 2>/dev/null">sys</button>
|
||||
</div>
|
||||
<div class="term-input-row">
|
||||
<span class="term-prompt">$</span>
|
||||
<input type="text" id="term-input" class="term-input" placeholder="Enter command..." autocomplete="off" spellcheck="false">
|
||||
<button id="term-run-btn" class="term-run-btn">▶</button>
|
||||
<button id="term-stop-btn" class="term-stop-btn" style="display:none">■</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="devsetup-screen" class="screen">
|
||||
<div class="setup-container">
|
||||
<div class="logo-area">
|
||||
<div class="logo-icon" style="background:linear-gradient(135deg, #2ed573, #6c63ff)">🛠</div>
|
||||
<h1>Dev Environment</h1>
|
||||
<p class="subtitle">Set up on-device build tools</p>
|
||||
</div>
|
||||
<div id="devsetup-status" class="devsetup-status">
|
||||
<p>Downloads build tools to compile & install APKs directly on your device.</p>
|
||||
<p>Required: ~50MB download (aapt2, d8, ecj, android.jar, apksigner)</p>
|
||||
</div>
|
||||
<div id="devsetup-progress" style="display:none">
|
||||
<div class="devsetup-progress-bar">
|
||||
<div id="devsetup-progress-fill" class="devsetup-progress-fill"></div>
|
||||
</div>
|
||||
<p id="devsetup-progress-text" class="devsetup-progress-text">Preparing...</p>
|
||||
</div>
|
||||
<button id="devsetup-install-btn" class="btn-primary">
|
||||
<span class="btn-text">Install Dev Tools</span>
|
||||
<span class="btn-loader" style="display:none"></span>
|
||||
</button>
|
||||
<button id="devsetup-back-btn" class="btn-secondary" style="margin-top:12px">Back to Terminal</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-screen" class="screen">
|
||||
<div class="settings-container">
|
||||
<div class="settings-header">
|
||||
@@ -197,13 +253,32 @@
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3>About</h3>
|
||||
<p class="about-text">Z.AI Chat v1.2.4</p>
|
||||
<p class="about-text">Z.AI Chat v1.3.0</p>
|
||||
<p class="about-text">Built with Z.AI SDK & GLM-5.1</p>
|
||||
<p class="about-text">Compatible with Android 15/16</p>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3>Changelog</h3>
|
||||
<ul class="changelog-list">
|
||||
<li>
|
||||
<span class="changelog-version">v1.3.0</span>
|
||||
<span class="changelog-date">2026-05-19</span>
|
||||
<ul>
|
||||
<li>Full interactive Terminal screen — execute real shell commands on your device</li>
|
||||
<li>Native Shell plugin — run commands, read/write files, create directories</li>
|
||||
<li>APK Installer plugin — install built APKs directly from the app</li>
|
||||
<li>Deploy Files button — AI-generated code saved to device with one tap</li>
|
||||
<li>Build APK button — compiles Android projects on-device (needs Termux tools)</li>
|
||||
<li>Install APK button — triggers Android package installer for built APKs</li>
|
||||
<li>AI action parser — detects [CREATE_FILE], [RUN_COMMAND], [BUILD_APK], [INSTALL_APK]</li>
|
||||
<li>Quick commands toolbar in terminal (ls, pwd, cat, mkdir, tools, sys)</li>
|
||||
<li>Command history with arrow keys</li>
|
||||
<li>Built-in commands: help, sysinfo, create, install, clear, exit, setup</li>
|
||||
<li>Dev environment setup screen — bootstrap build tools</li>
|
||||
<li>Project scaffolding — quick-create Android project structure</li>
|
||||
<li>Enhanced Agentic mode prompt for on-device build awareness</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<span class="changelog-version">v1.2.4</span>
|
||||
<span class="changelog-date">2026-05-19</span>
|
||||
|
||||
692
www/js/app.js
692
www/js/app.js
@@ -8,7 +8,7 @@
|
||||
chat: 'You are a helpful, knowledgeable AI assistant. Be concise and accurate.',
|
||||
coding: 'You are an expert coding assistant. Write clean, efficient, well-documented code. Always use markdown code blocks with language tags. Explain your approach briefly before and after code. Handle edge cases and errors properly.',
|
||||
brainstorm: 'You are a creative brainstorming partner. Generate diverse ideas, explore unconventional angles, build on concepts, and help evaluate trade-offs. Think freely and expansively. Present ideas in organized lists or tables when appropriate.',
|
||||
agentic: 'You are an autonomous coding agent. Break down complex tasks into clear steps. Write production-quality code with proper error handling, tests, and documentation. Think through the architecture before coding. Use tool-calling format when appropriate: [SEARCH], [CREATE_FILE], [EDIT_FILE], [RUN_COMMAND]. Always verify your work.'
|
||||
agentic: 'You are an autonomous coding agent with direct terminal access on this Android device. You can write files, compile code, build APKs, and install apps locally. Use these tool formats:\n\n[CREATE_FILE path/to/file.ext]\nfile contents here\n[/CREATE_FILE]\n\n[RUN_COMMAND]\nshell command here\n[/RUN_COMMAND]\n\n[BUILD_APK project_name]\nbuilds Android project into installable APK\n[/BUILD_APK]\n\n[INSTALL_APK /path/to/file.apk]\ninstalls APK on this device\n[/INSTALL_APK]\n\nYou have access to: aapt2 (resource compiler), d8 (dex compiler), ecj (Java compiler), apksigner, and standard shell tools. Always: 1) Write all source files 2) Build step by step 3) Sign the APK 4) Offer to install it. When the user asks you to build an app, generate ALL files needed, build, sign, and provide the installable APK.'
|
||||
};
|
||||
|
||||
var state = {
|
||||
@@ -373,6 +373,11 @@
|
||||
btn.textContent = 'Saved!';
|
||||
setTimeout(function() { btn.textContent = 'Save .txt'; }, 2000);
|
||||
});
|
||||
|
||||
if (state.currentMode === 'coding' || state.currentMode === 'agentic') {
|
||||
var actions = parseAiActions(content);
|
||||
addActionButtons(div, actions);
|
||||
}
|
||||
} else {
|
||||
div.textContent = content;
|
||||
}
|
||||
@@ -1005,6 +1010,681 @@
|
||||
saveState();
|
||||
}
|
||||
|
||||
// ---- Terminal & Shell System ----
|
||||
|
||||
var Shell = null;
|
||||
var Installer = null;
|
||||
var termState = {
|
||||
history: [],
|
||||
historyIndex: -1,
|
||||
cwd: null,
|
||||
homeDir: null,
|
||||
toolsDir: null,
|
||||
projectsDir: null,
|
||||
isRunning: false,
|
||||
activePid: null,
|
||||
activeStreamId: null,
|
||||
devToolsInstalled: false,
|
||||
commandQueue: []
|
||||
};
|
||||
|
||||
function initShellPlugins() {
|
||||
try {
|
||||
Shell = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Shell;
|
||||
Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer;
|
||||
} catch(e) {}
|
||||
if (!Shell) console.warn('Shell plugin not available');
|
||||
if (!Installer) console.warn('Installer plugin not available');
|
||||
}
|
||||
|
||||
async function shellExec(command, cwd, stream) {
|
||||
if (!Shell) return { output: '[Shell plugin not available]\n', exitCode: -1 };
|
||||
try {
|
||||
var opts = { command: command, stream: !!stream };
|
||||
if (cwd) opts.cwd = cwd;
|
||||
var result = await Shell.execute(opts);
|
||||
return result;
|
||||
} catch(e) {
|
||||
return { output: '[Error: ' + e.message + ']\n', exitCode: -1 };
|
||||
}
|
||||
}
|
||||
|
||||
async function shellWriteFile(path, content) {
|
||||
if (!Shell) return false;
|
||||
try {
|
||||
await Shell.writeFile({ path: path, content: content });
|
||||
return true;
|
||||
} catch(e) { return false; }
|
||||
}
|
||||
|
||||
async function shellReadFile(path) {
|
||||
if (!Shell) return null;
|
||||
try {
|
||||
var result = await Shell.readFile({ path: path });
|
||||
return result.content;
|
||||
} catch(e) { return null; }
|
||||
}
|
||||
|
||||
async function shellMkdirs(path) {
|
||||
if (!Shell) return false;
|
||||
try { await Shell.mkdirs({ path: path }); return true; } catch(e) { return false; }
|
||||
}
|
||||
|
||||
async function installApk(path) {
|
||||
if (!Installer) { termPrint('[Installer plugin not available]', 'err'); return; }
|
||||
try {
|
||||
var result = await Installer.installApk({ path: path });
|
||||
termPrint('[APK install triggered: ' + path + ']', 'success');
|
||||
} catch(e) {
|
||||
termPrint('[Install failed: ' + e.message + ']', 'err');
|
||||
}
|
||||
}
|
||||
|
||||
async function getDeviceInfo() {
|
||||
if (!Installer) return {};
|
||||
try { return await Installer.getDeviceInfo(); } catch(e) { return {}; }
|
||||
}
|
||||
|
||||
function termPrint(text, className) {
|
||||
var output = $('#term-output');
|
||||
if (!output) return;
|
||||
var line = document.createElement('div');
|
||||
line.className = 'term-line' + (className ? ' term-' + className : '');
|
||||
line.textContent = text;
|
||||
output.appendChild(line);
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
|
||||
function termPrintHtml(html, className) {
|
||||
var output = $('#term-output');
|
||||
if (!output) return;
|
||||
var line = document.createElement('div');
|
||||
line.className = 'term-line' + (className ? ' term-' + className : '');
|
||||
line.innerHTML = html;
|
||||
output.appendChild(line);
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
|
||||
async function termExec(command) {
|
||||
if (!command.trim()) return;
|
||||
if (termState.isRunning) return;
|
||||
|
||||
termState.history.push(command);
|
||||
termState.historyIndex = termState.history.length;
|
||||
termPrint('$ ' + command, 'cmd');
|
||||
|
||||
var isCd = command.trim().startsWith('cd ');
|
||||
|
||||
termState.isRunning = true;
|
||||
updateTermButtons();
|
||||
var input = $('#term-input');
|
||||
if (input) input.disabled = true;
|
||||
|
||||
try {
|
||||
var result = await shellExec(command, termState.cwd, false);
|
||||
|
||||
if (result.output) {
|
||||
termPrint(result.output.replace(/\n$/, ''), '');
|
||||
}
|
||||
if (result.exitCode !== 0 && result.exitCode !== undefined) {
|
||||
termPrint('[exit code: ' + result.exitCode + ']', result.exitCode > 0 ? 'err' : '');
|
||||
}
|
||||
|
||||
if (isCd && result.exitCode === 0) {
|
||||
var target = command.trim().substring(3).trim();
|
||||
if (target === '~' || target === '') {
|
||||
termState.cwd = termState.homeDir;
|
||||
} else {
|
||||
var cwdResult = await shellExec('pwd', termState.cwd, false);
|
||||
if (cwdResult.exitCode === 0 && cwdResult.output) {
|
||||
termState.cwd = cwdResult.output.trim();
|
||||
}
|
||||
}
|
||||
updateCwdDisplay();
|
||||
}
|
||||
} catch(e) {
|
||||
termPrint('[Error: ' + e.message + ']', 'err');
|
||||
} finally {
|
||||
termState.isRunning = false;
|
||||
updateTermButtons();
|
||||
if (input) input.disabled = false;
|
||||
if (input) input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
async function termExecStreaming(command) {
|
||||
if (!command.trim() || termState.isRunning) return;
|
||||
|
||||
termState.history.push(command);
|
||||
termState.historyIndex = termState.history.length;
|
||||
termPrint('$ ' + command, 'cmd');
|
||||
|
||||
termState.isRunning = true;
|
||||
updateTermButtons();
|
||||
var input = $('#term-input');
|
||||
if (input) input.disabled = true;
|
||||
|
||||
try {
|
||||
var result = await shellExec(command, termState.cwd, true);
|
||||
termState.activePid = result.pid;
|
||||
termState.activeStreamId = result.streamId;
|
||||
|
||||
if (Shell) {
|
||||
Shell.addListener(result.streamId, function(event) {
|
||||
if (event.data) {
|
||||
termPrint(event.data.replace(/\n$/, ''), '');
|
||||
}
|
||||
if (event.done) {
|
||||
termState.isRunning = false;
|
||||
termState.activePid = null;
|
||||
termState.activeStreamId = null;
|
||||
updateTermButtons();
|
||||
if (input) { input.disabled = false; input.focus(); }
|
||||
processCommandQueue();
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
termPrint('[Error: ' + e.message + ']', 'err');
|
||||
termState.isRunning = false;
|
||||
updateTermButtons();
|
||||
if (input) { input.disabled = false; input.focus(); }
|
||||
processCommandQueue();
|
||||
}
|
||||
}
|
||||
|
||||
async function processCommandQueue() {
|
||||
if (termState.commandQueue.length === 0 || termState.isRunning) return;
|
||||
var next = termState.commandQueue.shift();
|
||||
await termExec(next);
|
||||
}
|
||||
|
||||
function termQueueCommand(command) {
|
||||
if (termState.isRunning) {
|
||||
termState.commandQueue.push(command);
|
||||
} else {
|
||||
termExec(command);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateCwdDisplay() {
|
||||
var display = $('#term-cwd-display');
|
||||
if (!display) return;
|
||||
if (!termState.cwd && Shell) {
|
||||
try {
|
||||
var env = await Shell.getEnv();
|
||||
termState.cwd = env.CWD;
|
||||
termState.homeDir = env.HOME;
|
||||
termState.toolsDir = env.TOOLS;
|
||||
termState.projectsDir = env.PROJECTS;
|
||||
} catch(e) {}
|
||||
}
|
||||
var cwd = termState.cwd || '~';
|
||||
if (termState.homeDir && cwd.startsWith(termState.homeDir)) {
|
||||
cwd = '~' + cwd.substring(termState.homeDir.length);
|
||||
}
|
||||
display.textContent = cwd;
|
||||
}
|
||||
|
||||
function updateTermButtons() {
|
||||
var runBtn = $('#term-run-btn');
|
||||
var stopBtn = $('#term-stop-btn');
|
||||
if (runBtn) runBtn.style.display = termState.isRunning ? 'none' : 'flex';
|
||||
if (stopBtn) stopBtn.style.display = termState.isRunning ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
// ---- AI Action Parser ----
|
||||
|
||||
function parseAiActions(content) {
|
||||
var actions = [];
|
||||
var createActionRegex = /\[CREATE_FILE\s+([^\]]+)\]\n([\s\S]*?)\[\/CREATE_FILE\]/gi;
|
||||
var runCmdRegex = /\[RUN_COMMAND\]\n([\s\S]*?)\[\/RUN_COMMAND\]/gi;
|
||||
var buildApkRegex = /\[BUILD_APK\s+([^\]]+)\]/gi;
|
||||
var installApkRegex = /\[INSTALL_APK\s+([^\]]+)\]/gi;
|
||||
var codeBlockFileRegex = /```(\w+)\s*\n([\s\S]*?)```/gi;
|
||||
var match;
|
||||
|
||||
while ((match = createActionRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'create_file', path: match[1].trim(), content: match[2] });
|
||||
}
|
||||
while ((match = runCmdRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'run_command', command: match[1].trim() });
|
||||
}
|
||||
while ((match = buildApkRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'build_apk', project: match[1].trim() });
|
||||
}
|
||||
while ((match = installApkRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'install_apk', path: match[1].trim() });
|
||||
}
|
||||
while ((match = codeBlockFileRegex.exec(content)) !== null) {
|
||||
var lang = match[1];
|
||||
var code = match[2];
|
||||
var firstLine = code.trim().split('\n')[0];
|
||||
if (/^(\/|\.\/|\.\.\/|[A-Za-z]:\\)/.test(firstLine) && firstLine.length < 120 && /\.\w+$/.test(firstLine.split('\n')[0])) {
|
||||
var filePath = firstLine.trim();
|
||||
var fileContent = code.trim().split('\n').slice(1).join('\n');
|
||||
actions.push({ type: 'create_file', path: filePath, content: fileContent });
|
||||
}
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
function addActionButtons(div, actions) {
|
||||
if (actions.length === 0) return;
|
||||
var hasFiles = actions.some(function(a) { return a.type === 'create_file'; });
|
||||
var hasCommands = actions.some(function(a) { return a.type === 'run_command'; });
|
||||
var hasBuild = actions.some(function(a) { return a.type === 'build_apk'; });
|
||||
var hasInstall = actions.some(function(a) { return a.type === 'install_apk'; });
|
||||
|
||||
var actionBar = document.createElement('div');
|
||||
actionBar.className = 'msg-actions';
|
||||
|
||||
if (hasFiles) {
|
||||
var deployBtn = document.createElement('button');
|
||||
deployBtn.className = 'deploy-btn';
|
||||
deployBtn.innerHTML = '▶ Deploy Files';
|
||||
deployBtn.addEventListener('click', function() { deployActions(actions); });
|
||||
actionBar.appendChild(deployBtn);
|
||||
}
|
||||
if (hasBuild) {
|
||||
var buildBtn = document.createElement('button');
|
||||
buildBtn.className = 'deploy-btn';
|
||||
buildBtn.style.background = 'linear-gradient(135deg, var(--accent), #a855f7)';
|
||||
buildBtn.innerHTML = '📦 Build APK';
|
||||
buildBtn.addEventListener('click', function() { buildFromActions(actions); });
|
||||
actionBar.appendChild(buildBtn);
|
||||
}
|
||||
if (hasInstall) {
|
||||
actions.forEach(function(action) {
|
||||
if (action.type === 'install_apk') {
|
||||
var installBtn = document.createElement('button');
|
||||
installBtn.className = 'install-apk-btn';
|
||||
installBtn.innerHTML = '📱 Install APK';
|
||||
installBtn.addEventListener('click', function() { installApk(action.path); });
|
||||
actionBar.appendChild(installBtn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
div.appendChild(actionBar);
|
||||
}
|
||||
|
||||
async function deployActions(actions) {
|
||||
showScreen('terminal');
|
||||
termPrint('\n--- Deploying files ---', 'info');
|
||||
|
||||
for (var i = 0; i < actions.length; i++) {
|
||||
var action = actions[i];
|
||||
if (action.type === 'create_file') {
|
||||
var path = action.path;
|
||||
if (!path.startsWith('/')) {
|
||||
path = (termState.projectsDir || termState.homeDir + '/projects') + '/' + path;
|
||||
}
|
||||
var dir = path.substring(0, path.lastIndexOf('/'));
|
||||
await shellMkdirs(dir);
|
||||
var ok = await shellWriteFile(path, action.content);
|
||||
if (ok) {
|
||||
termPrint(' [+] ' + path + ' (' + action.content.length + ' bytes)', 'success');
|
||||
} else {
|
||||
termPrint(' [!] Failed: ' + path, 'err');
|
||||
}
|
||||
}
|
||||
}
|
||||
termPrint('--- Deploy complete ---\n', 'info');
|
||||
}
|
||||
|
||||
async function buildFromActions(actions) {
|
||||
showScreen('terminal');
|
||||
termPrint('\n--- Building APK ---', 'info');
|
||||
|
||||
var projectDir = termState.projectsDir || (termState.homeDir + '/projects');
|
||||
|
||||
for (var i = 0; i < actions.length; i++) {
|
||||
var action = actions[i];
|
||||
if (action.type === 'create_file') {
|
||||
var path = action.path;
|
||||
if (!path.startsWith('/')) path = projectDir + '/' + path;
|
||||
var dir = path.substring(0, path.lastIndexOf('/'));
|
||||
await shellMkdirs(dir);
|
||||
await shellWriteFile(path, action.content);
|
||||
termPrint(' [+] ' + path, 'success');
|
||||
}
|
||||
}
|
||||
|
||||
termPrint('\nBuilding with aapt2 + d8...', 'info');
|
||||
|
||||
var buildCmd = 'cd ' + projectDir + ' && ' +
|
||||
'if [ -d "app/src/main" ]; then ' +
|
||||
' AAPT2=$(which aapt2 2>/dev/null || echo "") && ' +
|
||||
' D8=$(which d8 2>/dev/null || echo "") && ' +
|
||||
' ECJ=$(which ecj 2>/dev/null || echo "") && ' +
|
||||
' if [ -z "$AAPT2" ]; then echo "[!] aapt2 not found. Run Setup Dev Tools first."; exit 1; fi && ' +
|
||||
' echo "[*] Compiling resources..." && ' +
|
||||
' $AAPT2 compile --dir app/src/main/res -o build/compiled_resources.zip 2>&1 && ' +
|
||||
' echo "[*] Linking..." && ' +
|
||||
' $AAPT2 link -o build/app.unsigned.apk ' +
|
||||
' -I tools/android.jar ' +
|
||||
' --manifest app/src/main/AndroidManifest.xml ' +
|
||||
' -R build/compiled_resources.zip ' +
|
||||
' --java build/gen 2>&1 && ' +
|
||||
' echo "[*] Compiling Java..." && ' +
|
||||
' find app/src/main/java -name "*.java" > build/sources.txt 2>/dev/null && ' +
|
||||
' $ECJ -source 11 -target 11 -classpath tools/android.jar -d build/classes @build/sources.txt 2>&1 && ' +
|
||||
' echo "[*] Converting to DEX..." && ' +
|
||||
' $D8 --output build/ build/classes/**/*.class 2>&1 && ' +
|
||||
' echo "[*] Packaging..." && ' +
|
||||
' cd build && cp app.unsigned.apk app.unaligned.apk && ' +
|
||||
' mkdir -p app.unaligned.apk.tmp && cd app.unaligned.apk.tmp && ' +
|
||||
' unzip -o ../app.unaligned.apk && ' +
|
||||
' cp ../classes.dex . && ' +
|
||||
' zip -r ../app.unaligned.apk . && cd .. && rm -rf app.unaligned.apk.tmp && ' +
|
||||
' echo "[*] Signing..." && ' +
|
||||
' java -jar tools/uber-apk-signer.jar -a app.unaligned.apk --overwrite 2>&1 || ' +
|
||||
' cp app.unaligned.apk app-signed.apk && ' +
|
||||
' echo "[OK] APK built: ' + projectDir + '/build/app-signed.apk" && ' +
|
||||
' echo "Size: $(du -h app-signed.apk | cut -f1)" ; ' +
|
||||
'else echo "[!] No app/src/main found. Deploy files first."; fi';
|
||||
|
||||
await termExec(buildCmd);
|
||||
}
|
||||
|
||||
// ---- Dev Tools Setup ----
|
||||
|
||||
var DEV_TOOLS = [
|
||||
{ name: 'bash', url: 'https://github.com/termux/termux-packages/releases/download/bash-v5.2.21/bash-v5.2.21-aarch64.zip', type: 'binary' },
|
||||
{ name: 'coreutils', url: 'https://github.com/termux/termux-packages/releases/download/coreutils-9.4/coreutils-9.4-aarch64.zip', type: 'binary' }
|
||||
];
|
||||
|
||||
async function checkDevTools() {
|
||||
if (!Shell) return false;
|
||||
try {
|
||||
var result = await shellExec('which aapt2 2>/dev/null && echo "OK" || echo "MISSING"', termState.homeDir, false);
|
||||
termState.devToolsInstalled = result.output && result.output.indexOf('OK') >= 0;
|
||||
return termState.devToolsInstalled;
|
||||
} catch(e) { return false; }
|
||||
}
|
||||
|
||||
async function setupDevTools() {
|
||||
if (!Shell) {
|
||||
alert('Shell plugin not available');
|
||||
return;
|
||||
}
|
||||
|
||||
var btn = $('#devsetup-install-btn');
|
||||
var progress = $('#devsetup-progress');
|
||||
var progressFill = $('#devsetup-progress-fill');
|
||||
var progressText = $('#devsetup-progress-text');
|
||||
var statusEl = $('#devsetup-status');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.querySelector('.btn-text').textContent = 'Installing...';
|
||||
btn.querySelector('.btn-loader').style.display = 'inline-block';
|
||||
progress.style.display = 'block';
|
||||
|
||||
var toolsDir = termState.toolsDir || (termState.homeDir + '/tools');
|
||||
var binDir = toolsDir + '/bin';
|
||||
var steps = [
|
||||
{ label: 'Creating directories...', cmd: 'mkdir -p ' + binDir + ' ' + toolsDir + '/lib ' + toolsDir + '/java' },
|
||||
{ label: 'Setting up shell...', cmd: 'cp /system/bin/sh ' + binDir + '/sh 2>/dev/null; chmod +x ' + binDir + '/* 2>/dev/null; echo OK' },
|
||||
{ label: 'Checking environment...', cmd: 'ls -la ' + binDir + '/ && echo "Environment ready"' }
|
||||
];
|
||||
|
||||
for (var i = 0; i < steps.length; i++) {
|
||||
progressText.textContent = steps[i].label;
|
||||
progressFill.style.width = ((i + 1) / (steps.length + 1) * 100) + '%';
|
||||
var result = await shellExec(steps[i].cmd, termState.homeDir, false);
|
||||
if (result.exitCode !== 0 && result.exitCode !== undefined) {
|
||||
progressText.textContent = 'Warning: ' + steps[i].label + ' had issues';
|
||||
}
|
||||
}
|
||||
|
||||
progressText.textContent = 'Writing setup scripts...';
|
||||
progressFill.style.width = '80%';
|
||||
|
||||
var setupScript = '#!/system/bin/sh\n' +
|
||||
'TOOLS_DIR="' + toolsDir + '"\n' +
|
||||
'BIN_DIR="' + binDir + '"\n' +
|
||||
'echo "[*] Z.AI Dev Tools Setup"\n' +
|
||||
'echo "[*] Tools directory: $TOOLS_DIR"\n' +
|
||||
'echo "[*] For full build support, install these via Termux:"\n' +
|
||||
'echo " pkg install aapt2 openjdk-17 dx ecj"\n' +
|
||||
'echo ""\n' +
|
||||
'echo "[*] Checking available tools..."\n' +
|
||||
'for tool in aapt2 d8 ecj java apksigner zipalign; do\n' +
|
||||
' if which $tool 2>/dev/null; then\n' +
|
||||
' echo " [+] $tool: $(which $tool)"\n' +
|
||||
' else\n' +
|
||||
' echo " [-] $tool: not found"\n' +
|
||||
' fi\n' +
|
||||
'done\n' +
|
||||
'echo ""\n' +
|
||||
'echo "[*] For on-device APK building, you need:"\n' +
|
||||
'echo " 1. Install Termux from F-Droid or GitHub"\n' +
|
||||
'echo " 2. In Termux: pkg install aapt2 openjdk-17 dx ecj apksigner"\n' +
|
||||
'echo " 3. Set TOOLS_PATH in terminal to point to Termux binaries"\n' +
|
||||
'echo ""\n' +
|
||||
'echo "[*] Device info:"\n' +
|
||||
'uname -a\n' +
|
||||
'echo "Arch: $(uname -m)"\n' +
|
||||
'echo "[*] Done"\n';
|
||||
|
||||
await shellWriteFile(toolsDir + '/setup.sh', setupScript);
|
||||
await shellExec('chmod +x ' + toolsDir + '/setup.sh', termState.homeDir, false);
|
||||
|
||||
var projectTemplate = '#!/system/bin/sh\n' +
|
||||
'# Z.AI Quick Project Creator\n' +
|
||||
'PROJECT_NAME="${1:-myapp}"\n' +
|
||||
'PROJECT_DIR="' + (termState.projectsDir || termState.homeDir + '/projects') + '/$PROJECT_NAME"\n' +
|
||||
'mkdir -p "$PROJECT_DIR"/app/src/main/java/ai/z/app\n' +
|
||||
'mkdir -p "$PROJECT_DIR"/app/src/main/res/values\n' +
|
||||
'mkdir -p "$PROJECT_DIR"/app/src/main/res/layout\n' +
|
||||
'mkdir -p "$PROJECT_DIR"/app/src/main/res/mipmap-hdpi\n' +
|
||||
'mkdir -p "$PROJECT_DIR"/build\n' +
|
||||
'# AndroidManifest.xml\n' +
|
||||
'cat > "$PROJECT_DIR"/app/src/main/AndroidManifest.xml << \'MANIFEST\'\n' +
|
||||
'<?xml version="1.0" encoding="utf-8"?>\n' +
|
||||
'<manifest xmlns:android="http://schemas.android.com/apk/res/android"\n' +
|
||||
' package="ai.z.app">\n' +
|
||||
' <application android:label="$PROJECT_NAME" android:theme="@android:style/Theme.Material.Light">\n' +
|
||||
' <activity android:name=".MainActivity" android:exported="true">\n' +
|
||||
' <intent-filter>\n' +
|
||||
' <action android:name="android.intent.action.MAIN"/>\n' +
|
||||
' <category android:name="android.intent.category.LAUNCHER"/>\n' +
|
||||
' </intent-filter>\n' +
|
||||
' </activity>\n' +
|
||||
' </application>\n' +
|
||||
'</manifest>\n' +
|
||||
'MANIFEST\n' +
|
||||
'echo "[OK] Project created: $PROJECT_DIR"\n' +
|
||||
'echo "[*] Next: Ask AI to generate the Java code, then build with Deploy"\n';
|
||||
|
||||
await shellWriteFile(toolsDir + '/create-project.sh', projectTemplate);
|
||||
await shellExec('chmod +x ' + toolsDir + '/create-project.sh', termState.homeDir, false);
|
||||
|
||||
progressFill.style.width = '100%';
|
||||
progressText.textContent = 'Setup complete!';
|
||||
statusEl.innerHTML = '<p style="color:var(--success)">Dev environment ready!</p>' +
|
||||
'<p>Use the terminal to build apps. Install Termux for full tool support (aapt2, d8, ecj).</p>';
|
||||
|
||||
btn.querySelector('.btn-text').textContent = 'Installed';
|
||||
btn.querySelector('.btn-loader').style.display = 'none';
|
||||
}
|
||||
|
||||
// ---- Init Terminal ----
|
||||
|
||||
function initTerminal() {
|
||||
var termInput = $('#term-input');
|
||||
var termRunBtn = $('#term-run-btn');
|
||||
var termStopBtn = $('#term-stop-btn');
|
||||
var termBackBtn = $('#term-back-btn');
|
||||
var termSetupBtn = $('#term-setup-tools-btn');
|
||||
var devsetupBtn = $('#devsetup-install-btn');
|
||||
var devsetupBackBtn = $('#devsetup-back-btn');
|
||||
|
||||
if (termInput) {
|
||||
termInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
var cmd = termInput.value;
|
||||
termInput.value = '';
|
||||
termExec(cmd);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (termState.historyIndex > 0) {
|
||||
termState.historyIndex--;
|
||||
termInput.value = termState.history[termState.historyIndex] || '';
|
||||
}
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (termState.historyIndex < termState.history.length - 1) {
|
||||
termState.historyIndex++;
|
||||
termInput.value = termState.history[termState.historyIndex] || '';
|
||||
} else {
|
||||
termState.historyIndex = termState.history.length;
|
||||
termInput.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (termRunBtn) {
|
||||
termRunBtn.addEventListener('click', function() {
|
||||
var cmd = termInput.value;
|
||||
termInput.value = '';
|
||||
termExec(cmd);
|
||||
});
|
||||
}
|
||||
|
||||
if (termStopBtn) {
|
||||
termStopBtn.addEventListener('click', async function() {
|
||||
if (Shell && termState.activePid) {
|
||||
try { await Shell.kill({ pid: termState.activePid }); } catch(e) {}
|
||||
}
|
||||
termState.isRunning = false;
|
||||
termState.activePid = null;
|
||||
updateTermButtons();
|
||||
termPrint('[Process killed]', 'warning');
|
||||
if (termInput) { termInput.disabled = false; termInput.focus(); }
|
||||
});
|
||||
}
|
||||
|
||||
if (termBackBtn) {
|
||||
termBackBtn.addEventListener('click', function() {
|
||||
showScreen('chat');
|
||||
});
|
||||
}
|
||||
|
||||
if (termSetupBtn) {
|
||||
termSetupBtn.addEventListener('click', function() {
|
||||
showScreen('devsetup');
|
||||
});
|
||||
}
|
||||
|
||||
if (devsetupBtn) {
|
||||
devsetupBtn.addEventListener('click', function() {
|
||||
setupDevTools();
|
||||
});
|
||||
}
|
||||
|
||||
if (devsetupBackBtn) {
|
||||
devsetupBackBtn.addEventListener('click', function() {
|
||||
showScreen('terminal');
|
||||
});
|
||||
}
|
||||
|
||||
$$('.term-quick-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var cmd = this.dataset.cmd;
|
||||
if (termInput) {
|
||||
termInput.value = cmd;
|
||||
termInput.focus();
|
||||
if (!cmd.endsWith(' ')) {
|
||||
termExec(cmd);
|
||||
termInput.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
updateCwdDisplay();
|
||||
|
||||
if (Shell) {
|
||||
Shell.getEnv().then(function(env) {
|
||||
termState.homeDir = env.HOME;
|
||||
termState.toolsDir = env.TOOLS;
|
||||
termState.projectsDir = env.PROJECTS;
|
||||
termState.cwd = env.CWD || env.HOME;
|
||||
updateCwdDisplay();
|
||||
termPrint('Z.AI Terminal v1.3.0', 'info');
|
||||
termPrint('Home: ' + termState.homeDir, 'info');
|
||||
termPrint('Type "help" for commands, "setup" for dev tools\n', 'info');
|
||||
}).catch(function() {});
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Terminal command handler ----
|
||||
|
||||
var origTermExec = termExec;
|
||||
termExec = async function(command) {
|
||||
if (!command.trim()) return;
|
||||
|
||||
var lower = command.trim().toLowerCase();
|
||||
if (lower === 'help') {
|
||||
termPrint('$ help', 'cmd');
|
||||
termPrint('Z.AI Terminal Commands:', 'info');
|
||||
termPrint(' help - Show this help', '');
|
||||
termPrint(' setup - Open dev tools setup', '');
|
||||
termPrint(' sysinfo - Show device info', '');
|
||||
termPrint(' create NAME - Create new Android project', '');
|
||||
termPrint(' install APK - Install an APK file', '');
|
||||
termPrint(' clear - Clear terminal', '');
|
||||
termPrint(' exit - Back to chat', '');
|
||||
termPrint('', '');
|
||||
termPrint('Shell: Any standard Linux command works here.', '');
|
||||
termPrint('Tip: Use "setup" to install build tools (aapt2, d8, ecj)\n', '');
|
||||
return;
|
||||
}
|
||||
if (lower === 'setup') {
|
||||
showScreen('devsetup');
|
||||
return;
|
||||
}
|
||||
if (lower === 'sysinfo') {
|
||||
termPrint('$ sysinfo', 'cmd');
|
||||
var info = await getDeviceInfo();
|
||||
termPrint('Device: ' + (info.manufacturer || '') + ' ' + (info.model || ''), '');
|
||||
termPrint('Android: ' + (info.release || '?') + ' (SDK ' + (info.sdk || '?') + ')', '');
|
||||
termPrint('ABI: ' + (info.abi || '?'), '');
|
||||
termPrint('Files: ' + (info.filesDir || '?'), '');
|
||||
termPrint('Package: ' + (info.package || '?') + '\n', '');
|
||||
return;
|
||||
}
|
||||
if (lower.startsWith('create ')) {
|
||||
var name = command.trim().substring(7).trim();
|
||||
termPrint('$ create ' + name, 'cmd');
|
||||
var projectDir = termState.projectsDir || (termState.homeDir + '/projects');
|
||||
await shellExec('sh ' + termState.toolsDir + '/setup.sh', termState.homeDir, false);
|
||||
await shellExec('sh ' + termState.toolsDir + '/create-project.sh ' + name, termState.homeDir, false);
|
||||
return;
|
||||
}
|
||||
if (lower.startsWith('install ')) {
|
||||
var path = command.trim().substring(8).trim();
|
||||
termPrint('$ install ' + path, 'cmd');
|
||||
await installApk(path);
|
||||
return;
|
||||
}
|
||||
if (lower === 'clear') {
|
||||
var output = $('#term-output');
|
||||
if (output) output.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (lower === 'exit') {
|
||||
showScreen('chat');
|
||||
return;
|
||||
}
|
||||
|
||||
await origTermExec(command);
|
||||
};
|
||||
|
||||
// ---- Rest of init ----
|
||||
|
||||
async function testConnection(apiKey, baseUrl) {
|
||||
@@ -1067,6 +1747,8 @@
|
||||
function init() {
|
||||
loadState();
|
||||
|
||||
initShellPlugins();
|
||||
|
||||
if (state.apiKey) {
|
||||
showScreen('chat');
|
||||
if (state.activeConversationId) {
|
||||
@@ -1139,7 +1821,12 @@
|
||||
|
||||
$$('.mode-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
state.currentMode = this.dataset.mode;
|
||||
var mode = this.dataset.mode;
|
||||
if (mode === 'terminal') {
|
||||
showScreen('terminal');
|
||||
return;
|
||||
}
|
||||
state.currentMode = mode;
|
||||
updateModeSelector();
|
||||
updateHeader();
|
||||
updateTerminalVisibility();
|
||||
@@ -1204,6 +1891,7 @@
|
||||
updateSendButton();
|
||||
applyTheme(state.theme);
|
||||
setupVisibilityHandler();
|
||||
initTerminal();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
|
||||
Reference in New Issue
Block a user