';
return html;
}
return '';
}
function updateTerminalContent() {
var termBody = $('#terminal-body');
if (!termBody) return;
termBody.innerHTML = '';
var conv = getConversation();
if (!conv) return;
var lastAssistant = null;
for (var i = conv.messages.length - 1; i >= 0; i--) {
if (conv.messages[i].role === 'assistant') {
lastAssistant = conv.messages[i];
break;
}
}
if (!lastAssistant && !state.streamingContent) {
termBody.innerHTML = '
No code output yet. Use Coding or Agentic mode to generate code.
';
return;
}
var content = state.streamingContent || (lastAssistant ? lastAssistant.content : '');
var entries = parseTerminalEntries(content);
if (entries.length === 0) {
termBody.innerHTML = '
No structured code blocks or tool calls detected in response.
';
return;
}
entries.forEach(function(entry) {
termBody.innerHTML += renderTerminalEntry(entry);
});
termBody.querySelectorAll('.term-copy-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var code = this.getAttribute('data-code').replace(/"/g, '"').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
navigator.clipboard.writeText(code).then(function() {
this.textContent = 'Copied!';
setTimeout(function() { this.textContent = 'Copy'; }.bind(this), 2000);
}.bind(this));
});
});
if (state.terminalOpen) {
termBody.scrollTop = termBody.scrollHeight;
}
}
function updateTerminalVisibility() {
var panel = $('#terminal-panel');
var toggleBtn = $('#terminal-toggle');
var fileTreeBtn = $('#file-tree-btn');
if (!panel || !toggleBtn) return;
var isDevMode = (state.currentMode === 'coding' || state.currentMode === 'agentic');
if (isDevMode) {
panel.style.display = 'flex';
toggleBtn.style.display = 'flex';
if (fileTreeBtn) fileTreeBtn.style.display = '';
} else {
panel.style.display = 'none';
toggleBtn.style.display = 'none';
if (fileTreeBtn) fileTreeBtn.style.display = 'none';
state.terminalOpen = false;
closeFileTree();
}
if (state.terminalOpen && isDevMode) {
panel.classList.add('open');
} else {
panel.classList.remove('open');
}
var label = toggleBtn.querySelector('.terminal-label');
if (label) label.textContent = state.terminalOpen ? 'Hide Terminal' : 'Show Terminal';
}
function toggleTerminal() {
state.terminalOpen = !state.terminalOpen;
var panel = $('#terminal-panel');
if (panel) panel.classList.toggle('open', state.terminalOpen);
var label = $('#terminal-toggle .terminal-label');
if (label) label.textContent = state.terminalOpen ? 'Hide Terminal' : 'Show Terminal';
if (state.terminalOpen) updateTerminalContent();
saveState();
}
// ---- Terminal & Shell System ----
var Shell = null;
var Installer = null;
var Wake = null;
var Bootstrap = null;
var termState = {
history: [],
historyIndex: -1,
cwd: null,
homeDir: null,
toolsDir: null,
projectsDir: null,
isRunning: false,
activePid: null,
activeStreamId: null,
devToolsInstalled: false,
javaToolsInstalled: false,
hasProot: false,
prootPath: '',
nativeLibDir: '',
hermesPath: '',
hermesVenv: '',
autoglmEnabled: false,
commandQueue: []
};
var approvalState = null;
function initShellPlugins() {
try {
Shell = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Shell;
Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer;
FileManager = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.FileManager;
Wake = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Wake;
Bootstrap = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Bootstrap;
} catch(e) {}
if (!Shell) console.warn('Shell plugin not available');
if (!Installer) console.warn('Installer plugin not available');
if (!FileManager) console.warn('FileManager plugin not available');
if (!Wake) console.warn('Wake plugin not available');
if (!Bootstrap) console.warn('Bootstrap plugin not available');
}
async function setWakeLock(on) {
if (!Wake) return;
try {
if (on) { await Wake.acquire(); } else { await Wake.release(); }
} catch(e) { console.warn('WakeLock error:', e); }
}
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 };
}
}
function needsApprovalForCommand(command) {
var cmd = (command || '').trim();
return /(^|\s)(ssh|scp|sftp|curl|wget|adb|am\s+start|pm\s+install|install\s+|chmod|chown|rm\s+-rf|mv\s+|cp\s+|python|python3|pip|npm|git\s+push|git\s+pull)/i.test(cmd);
}
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 {
await Installer.installApk({ path: path });
termPrint('[APK install triggered: ' + path + ']', 'success');
} catch(e) {
termPrint('[Install failed: ' + e.message + ']', 'err');
}
}
async function openFile(path, mimeType) {
if (!FileManager) { termPrint('[FileManager plugin not available]', 'err'); return; }
try {
await FileManager.openFile({ path: path, mimeType: mimeType || '' });
termPrint('[Open file triggered: ' + path + ']', 'success');
} catch(e) {
termPrint('[Open failed: ' + e.message + ']', 'err');
}
}
function requestApproval(title, body, onApprove) {
approvalState = { onApprove: onApprove };
var modal = $('#approval-modal');
var titleEl = $('#approval-title');
var bodyEl = $('#approval-body');
if (titleEl) titleEl.textContent = title || 'Approval required';
if (bodyEl) bodyEl.textContent = body || '';
if (modal) modal.style.display = 'flex';
}
function closeApproval() {
var modal = $('#approval-modal');
if (modal) modal.style.display = 'none';
approvalState = null;
}
async function askApproval(title, body, action) {
return new Promise(function(resolve) {
requestApproval(title, body, async function() {
try {
var result = await action();
resolve(result);
} catch(e) {
resolve(false);
} finally {
closeApproval();
}
});
});
}
async function listFiles(path) {
if (!FileManager) return null;
try { return await FileManager.listFiles({ path: path }); } catch(e) { return null; }
}
function isApkPath(path) {
return !!path && path.toLowerCase().endsWith('.apk');
}
function isOpenablePath(path) {
return !!path && /\.(apk|apks|zip|json|txt|log|md|html?|xml|js|java|kt|png|jpg|jpeg|webp)$/i.test(path);
}
async function getDeviceInfo() {
if (!Installer) return {};
try { return await Installer.getDeviceInfo(); } catch(e) { return {}; }
}
async function getWorkspaceContext() {
var ctx = '';
try {
var info = await getDeviceInfo();
ctx += 'Device: ' + (info.manufacturer || '') + ' ' + (info.model || '') + ', Android ' + (info.release || '') + ' (SDK ' + (info.sdk || '') + '), ABI: ' + (info.abi || '') + '\n';
var envResult = await shellExec('echo "HOME=$HOME CWD=$(pwd) TERMUX=${TERMUX_VERSION:-none}"', termState.cwd || termState.homeDir, false);
ctx += 'Environment: ' + (envResult.output || '').trim() + '\n';
var toolCheck = await shellExec('which aapt2 d8 ecj javac apksigner zipalign 2>/dev/null; echo "---"; which tsu su 2>/dev/null || echo "no-root"', termState.homeDir, false);
ctx += 'Available tools: ' + (toolCheck.output || '').trim().replace(/\n---\n/, '\nRoot access: ') + '\n';
var projectDir = termState.projectsDir || (termState.homeDir + '/projects');
var lsResult = await shellExec('find ' + projectDir + ' -type f 2>/dev/null | head -30 || echo "no-projects"', termState.homeDir, false);
if (lsResult.output && lsResult.output.indexOf('no-projects') < 0 && lsResult.output.trim()) {
ctx += 'Project files:\n' + lsResult.output.trim() + '\n';
}
} catch(e) {}
return ctx || null;
}
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;
if (needsApprovalForCommand(command)) {
await askApproval('Run command?', command, function() {
return shellExec(command, termState.cwd, false).then(function(result) {
termPrint('$ ' + command, 'cmd');
if (result.output) termPrint(result.output.replace(/\n$/, ''), '');
if (result.exitCode !== 0 && result.exitCode !== undefined) termPrint('[exit code: ' + result.exitCode + ']', 'err');
return true;
});
});
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 deviceTapRegex = /\[DEVICE_TAP\s+(\d+)\s+(\d+)\]/gi;
var deviceLongPressRegex = /\[DEVICE_LONG_PRESS\s+(\d+)\s+(\d+)\]/gi;
var deviceSwipeRegex = /\[DEVICE_SWIPE\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\]/gi;
var deviceTypeRegex = /\[DEVICE_TYPE\s+([^\]]+)\]/gi;
var devicePressBackRegex = /\[DEVICE_PRESS_BACK\]/gi;
var devicePressHomeRegex = /\[DEVICE_PRESS_HOME\]/gi;
var devicePressRecentsRegex = /\[DEVICE_PRESS_RECENTS\]/gi;
var deviceScreenshotRegex = /\[DEVICE_SCREENSHOT\]/gi;
var deviceUiTreeRegex = /\[DEVICE_UI_TREE\]/gi;
var deviceClickTextRegex = /\[DEVICE_CLICK_TEXT\s+([^\]]+)\]/gi;
var deviceClickIdRegex = /\[DEVICE_CLICK_ID\s+([^\]]+)\]/gi;
var deviceLaunchRegex = /\[DEVICE_LAUNCH\s+([^\]]+)\]/gi;
var deviceCurrentAppRegex = /\[DEVICE_CURRENT_APP\]/gi;
var sshExecRegex = /\[SSH_EXEC\s+([^\s]+)\s+([^\]]+)\]/gi;
var sshUploadRegex = /\[SSH_UPLOAD\s+([^\s]+)\s+([^\]]+)\]/gi;
var sshDownloadRegex = /\[SSH_DOWNLOAD\s+([^\s]+)\s+([^\]]+)\]/gi;
var remoteExecRegex = /\[REMOTE_EXEC\s+([^\s]+)\s+([^\]]+)\]/gi;
var curlExecRegex = /\[CURL_EXEC\s+([^\]]+)\]/gi;
var hermesInstallRegex = /\[HERMES_INSTALL\]/gi;
var hermesExecRegex = /\[HERMES_EXEC\s+([^\]]+)\]/gi;
var venvSetupRegex = /\[VENV_SETUP\]/gi;
var venvPipInstallRegex = /\[VENV_PIP_INSTALL\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 = deviceTapRegex.exec(content)) !== null) {
actions.push({ type: 'device_tap', x: parseInt(match[1]), y: parseInt(match[2]) });
}
while ((match = deviceLongPressRegex.exec(content)) !== null) {
actions.push({ type: 'device_long_press', x: parseInt(match[1]), y: parseInt(match[2]) });
}
while ((match = deviceSwipeRegex.exec(content)) !== null) {
actions.push({ type: 'device_swipe', startX: parseInt(match[1]), startY: parseInt(match[2]), endX: parseInt(match[3]), endY: parseInt(match[4]) });
}
while ((match = deviceTypeRegex.exec(content)) !== null) {
actions.push({ type: 'device_type', text: match[1].trim() });
}
while ((match = devicePressBackRegex.exec(content)) !== null) {
actions.push({ type: 'device_press_back' });
}
while ((match = devicePressHomeRegex.exec(content)) !== null) {
actions.push({ type: 'device_press_home' });
}
while ((match = devicePressRecentsRegex.exec(content)) !== null) {
actions.push({ type: 'device_press_recents' });
}
while ((match = deviceScreenshotRegex.exec(content)) !== null) {
actions.push({ type: 'device_screenshot' });
}
while ((match = deviceUiTreeRegex.exec(content)) !== null) {
actions.push({ type: 'device_ui_tree' });
}
while ((match = deviceClickTextRegex.exec(content)) !== null) {
actions.push({ type: 'device_click_text', text: match[1].trim() });
}
while ((match = deviceClickIdRegex.exec(content)) !== null) {
actions.push({ type: 'device_click_id', viewId: match[1].trim() });
}
while ((match = deviceLaunchRegex.exec(content)) !== null) {
actions.push({ type: 'device_launch', pkg: match[1].trim() });
}
while ((match = deviceCurrentAppRegex.exec(content)) !== null) {
actions.push({ type: 'device_current_app' });
}
while ((match = hermesInstallRegex.exec(content)) !== null) {
actions.push({ type: 'hermes_install' });
}
while ((match = hermesExecRegex.exec(content)) !== null) {
actions.push({ type: 'hermes_exec', command: match[1].trim() });
}
while ((match = venvSetupRegex.exec(content)) !== null) {
actions.push({ type: 'venv_setup' });
}
while ((match = venvPipInstallRegex.exec(content)) !== null) {
actions.push({ type: 'venv_pip_install', packages: match[1].trim() });
}
while ((match = sshExecRegex.exec(content)) !== null) {
actions.push({ type: 'ssh_exec', host: match[1].trim(), command: match[2].trim() });
}
while ((match = sshUploadRegex.exec(content)) !== null) {
actions.push({ type: 'ssh_upload', localPath: match[1].trim(), remotePath: match[2].trim() });
}
while ((match = sshDownloadRegex.exec(content)) !== null) {
actions.push({ type: 'ssh_download', remotePath: match[1].trim(), localPath: match[2].trim() });
}
while ((match = remoteExecRegex.exec(content)) !== null) {
actions.push({ type: 'remote_exec', host: match[1].trim(), command: match[2].trim() });
}
while ((match = curlExecRegex.exec(content)) !== null) {
actions.push({ type: 'curl_exec', url: 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 hasDevice = actions.some(function(a) { return a.type && a.type.indexOf('device_') === 0; });
var hasHermes = actions.some(function(a) { return a.type && a.type.indexOf('hermes_') === 0; });
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');
}
function showStatusToast(message, type) {
var existing = $('#status-toast');
if (existing) existing.remove();
var toast = document.createElement('div');
toast.id = 'status-toast';
toast.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:1000;' +
'padding:10px 20px;border-radius:20px;font-size:13px;font-weight:600;max-width:90%;' +
'text-align:center;pointer-events:none;opacity:0;transition:opacity 0.3s;' +
'background:' + (type === 'success' ? 'var(--success)' : type === 'err' ? 'var(--danger)' : 'var(--accent)') +
';color:white;box-shadow:0 4px 12px rgba(0,0,0,0.3)';
toast.textContent = message;
document.body.appendChild(toast);
requestAnimationFrame(function() { toast.style.opacity = '1'; });
setTimeout(function() {
toast.style.opacity = '0';
setTimeout(function() { if (toast.parentElement) toast.remove(); }, 300);
}, 3000);
}
var _agenticRetryCount = 0;
function getMaxRetries() {
return state.maxRetries || 10;
}
function getMaxAutoContinue() {
return state.maxAutoContinue || 5;
}
function getFileLanguage(path) {
var ext = (path || '').split('.').pop().toLowerCase();
var map = {
'java': 'java', 'xml': 'xml', 'html': 'html', 'css': 'css',
'js': 'javascript', 'json': 'json', 'md': 'markdown',
'py': 'python', 'kt': 'kotlin', 'gradle': 'groovy',
'properties': 'properties', 'txt': 'text', 'sh': 'shell',
'yaml': 'yaml', 'yml': 'yaml', 'toml': 'toml',
'c': 'c', 'cpp': 'cpp', 'h': 'c', 'rs': 'rust',
'sql': 'sql', 'svg': 'svg', 'png': 'image', 'jpg': 'image'
};
return map[ext] || ext;
}
function trackFile(conv, path, content) {
if (!conv || !path) return;
if (!conv.files) conv.files = [];
var lang = getFileLanguage(path);
var idx = -1;
for (var i = 0; i < conv.files.length; i++) {
if (conv.files[i].path === path) { idx = i; break; }
}
var entry = { path: path, content: content, language: lang, timestamp: Date.now() };
if (idx >= 0) {
conv.files[idx] = entry;
} else {
conv.files.push(entry);
}
saveState();
}
function trackFilesFromActions(actions, conv) {
if (!conv) return;
for (var i = 0; i < actions.length; i++) {
if (actions[i].type === 'create_file') {
trackFile(conv, actions[i].path, actions[i].content);
}
}
}
function resolveFilePath(path) {
if (!path) return '';
if (path.startsWith('/')) return path;
return (termState.projectsDir || termState.homeDir + '/projects') + '/' + path;
}
function getLastAssistantContent(conv) {
if (!conv || !conv.messages) return '';
for (var i = conv.messages.length - 1; i >= 0; i--) {
if (conv.messages[i].role === 'assistant' && !conv.messages[i]._streaming) {
return conv.messages[i].content;
}
}
return '';
}
function isTaskComplete(content) {
if (!content) return true;
if (content.indexOf('[TASK_COMPLETE]') >= 0) return true;
var hasAction = content.indexOf('[CREATE_FILE') >= 0 ||
content.indexOf('[RUN_COMMAND]') >= 0 ||
content.indexOf('[BUILD_APK') >= 0 ||
content.indexOf('[INSTALL_APK') >= 0 ||
content.indexOf('[DEVICE_') >= 0 ||
content.indexOf('[HERMES_') >= 0 ||
content.indexOf('[VENV_') >= 0 ||
content.indexOf('[SSH_') >= 0 ||
content.indexOf('[REMOTE_EXEC') >= 0 ||
content.indexOf('[CURL_EXEC') >= 0;
var hasCodeBlock = content.indexOf('```') >= 0;
if (!hasCodeBlock && !hasAction && content.length < 300) return true;
if ((content.match(/```/g) || []).length % 2 !== 0) return false;
if ((content.match(/\[CREATE_FILE/g) || []).length > (content.match(/\[\/CREATE_FILE\]/g) || []).length) return false;
if ((content.match(/\[RUN_COMMAND\]/g) || []).length > (content.match(/\[\/RUN_COMMAND\]/g) || []).length) return false;
if (hasAction) return false;
return true;
}
async function autoContinueIfNeeded(conv) {
if (!state.autoContinue) return;
if (state.currentMode !== 'coding' && state.currentMode !== 'agentic') return;
if (!conv || !state.apiKey) return;
var maxCont = getMaxAutoContinue();
var count = 0;
while (count < maxCont) {
var last = getLastAssistantContent(conv);
if (isTaskComplete(last)) break;
count++;
termPrint('\n[>] Auto-continuing (' + count + '/' + maxCont + ')...', 'info');
showStatusToast('Auto-continuing (' + count + '/' + maxCont + ')...', 'info');
var contMsg = 'Your previous response was cut off or the task is not fully complete. ' +
'Continue from EXACTLY where you left off. Complete ALL remaining files, commands, builds, and installations. ' +
'When the ENTIRE task is fully done, output [TASK_COMPLETE] on its own line.';
conv.messages.push({ role: 'user', content: contMsg });
saveState();
appendMessage('user', contMsg);
state.isGenerating = true;
state.streamingConvId = conv.id;
state.streamingContent = '';
updateSendButton();
if (state.keepAwake) setWakeLock(true);
var contError = false;
try {
var sysPrompt = MODE_PROMPTS[state.currentMode] || MODE_PROMPTS.chat;
if (Shell && (state.currentMode === 'agentic' || state.currentMode === 'coding')) {
var wsCtx = await getWorkspaceContext();
if (wsCtx) sysPrompt += '\n\n## Current Device Context:\n' + wsCtx;
}
var apiMsgs = [{ role: 'system', content: sysPrompt }];
conv.messages.forEach(function(m) {
if (m.role === 'user' || (m.role === 'assistant' && !m._streaming)) {
apiMsgs.push({ role: m.role, content: m.content });
}
});
var reqBody = {
model: state.model,
messages: apiMsgs,
temperature: state.temperature,
max_tokens: state.maxTokens,
stream: state.streaming
};
var respDiv = appendMessage('assistant', '');
state.streamingResponseDiv = respDiv;
if (state.streaming) {
await streamResponseWithRetry(reqBody, respDiv, conv);
} else {
var res = await apiRequestWithRetry(reqBody);
var cont = res.choices[0].message.content;
updateStreamingMessage(respDiv, cont);
state.streamingContent = cont;
conv.messages.push({ role: 'assistant', content: cont });
}
} catch(err) {
if (state.streamingContent) {
conv.messages.push({ role: 'assistant', content: state.streamingContent });
}
if (err.name !== 'AbortError') {
termPrint('[!] Auto-continue error: ' + (err.message || err), 'err');
}
contError = true;
}
state.isGenerating = false;
state.abortController = null;
state.streamingConvId = null;
state.streamingResponseDiv = null;
updateSendButton();
saveState();
if (state.keepAwake) setWakeLock(false);
if (contError) break;
var contContent = state.streamingContent || '';
state.streamingContent = '';
if (state.autoDeploy && contContent) {
var contActions = parseAiActions(contContent);
if (contActions.length > 0) {
trackFilesFromActions(contActions, conv);
await autoExecuteActions(contActions, conv);
}
}
renderFileTree();
}
if (count > 0) {
var finalCheck = getLastAssistantContent(conv);
if (isTaskComplete(finalCheck)) {
termPrint('\n[v] Task completed after ' + count + ' auto-continue(s)', 'success');
showStatusToast('Task completed!', 'success');
} else if (count >= maxCont) {
termPrint('\n[!] Max auto-continues reached (' + maxCont + ')', 'warning');
}
}
}
function buildFileTreeData(files) {
var root = { name: '', children: {}, files: [] };
for (var f = 0; f < files.length; f++) {
var parts = files[f].path.split('/').filter(Boolean);
var cur = root;
for (var i = 0; i < parts.length - 1; i++) {
if (!cur.children[parts[i]]) {
cur.children[parts[i]] = { name: parts[i], children: {}, files: [] };
}
cur = cur.children[parts[i]];
}
cur.files.push({
name: parts[parts.length - 1],
path: files[f].path,
language: files[f].language,
timestamp: files[f].timestamp
});
}
return root;
}
function renderTreeHtml(node, convId, depth) {
var html = '';
var indent = depth * 20;
var dirs = Object.keys(node.children).sort();
for (var d = 0; d < dirs.length; d++) {
var child = node.children[dirs[d]];
html += '
';
html += '
';
html += '▶';
html += '' + escapeHtml(dirs[d]) + '';
html += '
';
html += '
';
html += renderTreeHtml(child, convId, depth + 1);
html += '
';
}
for (var fi = 0; fi < node.files.length; fi++) {
var file = node.files[fi];
html += '
';
html += '' + escapeHtml(file.language || '?') + '';
html += '' + escapeHtml(file.name) + '';
if (isApkPath(file.path)) {
html += 'APK';
}
html += '
';
}
return html;
}
function renderFileTree() {
var body = $('#file-tree-body');
if (!body) return;
var conv = getConversation();
if (!conv || !conv.files || conv.files.length === 0) {
body.innerHTML = '
No files yet. AI-generated files appear here.
';
var badge = $('#file-tree-count');
if (badge) badge.textContent = '0 files';
return;
}
var tree = buildFileTreeData(conv.files);
body.innerHTML = renderTreeHtml(tree, conv.id, 0);
var badge = $('#file-tree-count');
if (badge) badge.textContent = conv.files.length + ' file' + (conv.files.length !== 1 ? 's' : '');
}
async function renderDeviceFiles(rootPath) {
var body = $('#file-tree-body');
if (!body) return;
var result = await listFiles(rootPath);
if (!result) {
body.innerHTML = '
File manager unavailable.
';
return;
}
var items = result.items || [];
if (items.length === 0) {
body.innerHTML = '
Folder is empty.
';
return;
}
var html = '
';
for (var i = 0; i < items.length; i++) {
var item = items[i];
html += '
';
html += '' + escapeHtml(item.directory ? 'dir' : (item.mimeType || '?')) + '';
html += '' + escapeHtml(item.name) + '';
html += item.directory ? 'DIR' : (isApkPath(item.path) ? 'APK' : '');
html += '
';
}
html += '
';
body.innerHTML = html;
}
async function renderFileManagerHome() {
var body = $('#file-tree-body');
if (!body) return;
if (!FileManager) {
body.innerHTML = '
File manager unavailable.
';
return;
}
try {
var result = await FileManager.getRoots();
var roots = result.roots || [];
var html = '
';
for (var i = 0; i < roots.length; i++) {
var item = roots[i];
html += '
';
html += 'root';
html += '' + escapeHtml(item.name) + '';
html += 'OPEN';
html += '
';
}
html += '
';
body.innerHTML = html;
var title = document.querySelector('#file-tree-panel h3');
if (title) title.textContent = 'File Manager';
var count = $('#file-tree-count');
if (count) count.textContent = roots.length + ' roots';
} catch(e) {
body.innerHTML = '
File manager unavailable.
';
}
}
function toggleFileTree() {
var panel = $('#file-tree-panel');
var overlay = $('#file-tree-overlay');
if (!panel) return;
var isOpen = panel.classList.contains('open');
if (isOpen) {
panel.classList.remove('open');
if (overlay) overlay.classList.remove('active');
} else {
renderFileTree();
panel.classList.add('open');
if (overlay) overlay.classList.add('active');
}
}
function toggleDeviceFileManager() {
var panel = $('#file-tree-panel');
var overlay = $('#file-tree-overlay');
if (!panel) return;
if (panel.classList.contains('open')) {
panel.classList.remove('open');
if (overlay) overlay.classList.remove('active');
return;
}
panel.classList.add('open');
if (overlay) overlay.classList.add('active');
renderFileManagerHome();
}
function showProjectFiles() {
renderFileTree();
var title = document.querySelector('#file-tree-panel h3');
if (title) title.textContent = 'Project Files';
var count = $('#file-tree-count');
if (count) count.textContent = (getConversation() && getConversation().files ? getConversation().files.length : 0) + ' files';
}
function closeFileTree() {
var panel = $('#file-tree-panel');
var overlay = $('#file-tree-overlay');
if (panel) panel.classList.remove('open');
if (overlay) overlay.classList.remove('active');
}
function openFileViewer(convId, path) {
var conv = getConversation(convId);
if (!conv || !conv.files) return;
var file = null;
for (var i = 0; i < conv.files.length; i++) {
if (conv.files[i].path === path) { file = conv.files[i]; break; }
}
if (!file) return;
var viewer = $('#file-viewer');
var nameEl = $('#file-viewer-name');
var langEl = $('#file-viewer-lang');
var contentEl = $('#file-viewer-content');
var textareaEl = $('#file-viewer-textarea');
var bodyEl = $('#file-viewer-body');
var editorEl = $('#file-viewer-editor');
var saveBtn = $('#file-viewer-save');
var editBtn = $('#file-viewer-edit');
if (!viewer || !nameEl || !langEl || !contentEl || !textareaEl || !bodyEl || !editorEl || !saveBtn || !editBtn) return;
nameEl.textContent = file.path;
langEl.textContent = file.language;
contentEl.textContent = file.content;
textareaEl.value = file.content;
bodyEl.style.display = '';
editorEl.style.display = 'none';
saveBtn.style.display = 'none';
editBtn.style.display = '';
editBtn.textContent = 'Edit';
viewer.style.display = 'flex';
viewer.dataset.convId = convId;
viewer.dataset.path = path;
}
async function openFileFromTree(path) {
if (isApkPath(path)) {
await askApproval('Install APK?', path, function() { return installApk(path); });
return;
}
var fileManagerTitle = document.querySelector('#file-tree-panel h3');
if (fileManagerTitle && fileManagerTitle.textContent === 'File Manager') {
await renderDeviceFiles(path);
return;
}
await openFile(path, '');
}
function closeFileViewer() {
var viewer = $('#file-viewer');
if (viewer) viewer.style.display = 'none';
}
function toggleFileEdit() {
var bodyEl = $('#file-viewer-body');
var editorEl = $('#file-viewer-editor');
var saveBtn = $('#file-viewer-save');
var editBtn = $('#file-viewer-edit');
if (bodyEl.style.display !== 'none') {
bodyEl.style.display = 'none';
editorEl.style.display = '';
saveBtn.style.display = '';
editBtn.textContent = 'View';
} else {
bodyEl.style.display = '';
editorEl.style.display = 'none';
saveBtn.style.display = 'none';
editBtn.textContent = 'Edit';
}
}
async function saveFileEdit() {
var viewer = $('#file-viewer');
var textareaEl = $('#file-viewer-textarea');
if (!viewer || !textareaEl) return;
var convId = viewer.dataset.convId;
var path = viewer.dataset.path;
var newContent = textareaEl.value;
var conv = getConversation(convId);
if (!conv) return;
var absPath = resolveFilePath(path);
var dir = absPath.substring(0, absPath.lastIndexOf('/'));
await shellMkdirs(dir);
var ok = await shellWriteFile(absPath, newContent);
if (ok) {
trackFile(conv, path, newContent);
showStatusToast('File saved: ' + path, 'success');
termPrint('[OK] Saved: ' + path + ' (' + newContent.length + ' bytes)', 'success');
$('#file-viewer-content').textContent = newContent;
toggleFileEdit();
renderFileTree();
} else {
showStatusToast('Failed to save file', 'err');
}
}
async function ensureBuildTools() {
var check = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1 && (command -v d8 >/dev/null 2>&1 || command -v dx >/dev/null 2>&1)', termState.homeDir, false);
if (check.exitCode === 0) {
termState.devToolsInstalled = true;
return true;
}
var javaCheck = await shellExec('test -f "$TOOLS/jars/ecj.jar" && test -f "$TOOLS/jars/d8.jar" && test -f "$TOOLS/jars/apksigner.jar"', termState.homeDir, false);
if (javaCheck.exitCode === 0 && termState.nativeLibDir) {
var aapt2Check = await shellExec('test -f "$TOOLS/bin/aapt2"', termState.homeDir, false);
if (aapt2Check.exitCode === 0) {
termState.devToolsInstalled = true;
termState.javaToolsInstalled = true;
return true;
}
}
if (Shell) {
var env = await Shell.getEnv();
termState.homeDir = env.HOME;
termState.toolsDir = env.TOOLS;
termState.projectsDir = env.PROJECTS;
termState.cwd = env.CWD || env.HOME;
termState.hasProot = env.hasProot === true;
termState.prootPath = env.prootPath || '';
termState.nativeLibDir = env.nativeLibDir || '';
}
termPrint('[*] Setting up Java build tools (app_process virtual JVM)...', 'info');
showStatusToast('Installing build tools...', 'info');
var javaOk = await setupJavaTools();
if (javaOk) return true;
var prefix = termState.homeDir ? termState.homeDir.replace('/home', '') : '';
var prefixUsr = prefix + '/usr';
try { await Bootstrap.fixPermissions(); } catch(e) {}
termPrint('[*] Trying Termux pkg install...', 'info');
var pkgOk = await tryPkgInstall(prefixUsr);
if (pkgOk) return true;
if (termState.prootPath) {
termPrint('[*] Trying PRoot...', 'info');
var prootOk = await tryProotExec(termState.prootPath, prefixUsr, prefixUsr + '/bin/pkg', prefixUsr + '/bin/apt');
if (prootOk) return true;
}
termPrint('[*] Checking for Termux...', 'info');
var termuxOk = await tryTermuxInstall();
if (termuxOk) return true;
termPrint('[!] All strategies failed. Install Termux from F-Droid:', 'err');
termPrint(' https://f-droid.org/en/packages/com.termux/', 'warning');
termPrint(' Then: pkg update && pkg install aapt2 ecj dx apksigner', 'warning');
termState.devToolsInstalled = false;
return false;
}
async function setupJavaTools() {
if (!Shell || !Bootstrap) return false;
var toolsDir = termState.toolsDir;
var jarsDir = toolsDir + '/jars';
var binDir = toolsDir + '/bin';
var nativeLibDir = termState.nativeLibDir;
await shellExec('mkdir -p "' + jarsDir + '" "' + binDir + '"', termState.homeDir, false);
termPrint('[*] Extracting bundled JARs from APK assets...', 'info');
try {
var extractResult = await Bootstrap.extractAsset({src: 'jars/ecj.jar', dest: jarsDir + '/ecj.jar'});
termPrint('[OK] ecj.jar extracted (' + Math.round(extractResult.size/1024) + ' KB)', 'success');
} catch(e) {
termPrint('[!] ecj.jar extract failed: ' + e.message, 'err');
return false;
}
try {
var extractResult = await Bootstrap.extractAsset({src: 'jars/apksigner.jar', dest: jarsDir + '/apksigner.jar'});
termPrint('[OK] apksigner.jar extracted (' + Math.round(extractResult.size/1024) + ' KB)', 'success');
} catch(e) {
termPrint('[!] apksigner.jar extract failed: ' + e.message, 'err');
return false;
}
var d8Test = await shellExec('test -f "' + jarsDir + '/d8.jar"', termState.homeDir, false);
if (d8Test.exitCode !== 0) {
termPrint('[*] Extracting d8.jar from APK assets...', 'info');
try {
var d8Extract = await Bootstrap.extractAsset({src: 'jars/d8.jar', dest: jarsDir + '/d8.jar'});
termPrint('[OK] d8.jar extracted (' + Math.round(d8Extract.size/1024/1024) + ' MB)', 'success');
} catch(e) {
termPrint('[!] d8.jar extract failed: ' + e.message, 'warning');
termPrint('[*] Downloading d8.jar (~18MB)...', 'info');
try {
await Bootstrap.downloadFile({url: 'https://dl.google.com/android/repository/build-tools_r36-linux.zip', dest: toolsDir + '/build-tools.zip'});
await shellExec('cd "' + toolsDir + '" && unzip -o build-tools.zip "*/lib/d8.jar" 2>&1 && mv */lib/d8.jar jars/d8.jar && rm -rf build-tools.zip android-*', termState.homeDir, false);
} catch(e2) {
termPrint('[!] d8.jar download also failed: ' + e2.message, 'err');
}
}
}
var d8Check = await shellExec('test -f "' + jarsDir + '/d8.jar" && test -s "' + jarsDir + '/d8.jar"', termState.homeDir, false);
if (d8Check.exitCode !== 0) {
termPrint('[!] d8.jar not available', 'err');
return false;
}
termPrint('[OK] d8.jar ready', 'success');
var aapt2Check = await shellExec('test -f "' + binDir + '/aapt2"', termState.homeDir, false);
if (aapt2Check.exitCode !== 0) {
termPrint('[*] Installing aapt2 from Termux repo...', 'info');
try {
var aapt2Result = await Bootstrap.installAapt2({toolsDir: toolsDir});
termPrint('[OK] aapt2 installed (' + Math.round(aapt2Result.size/1024) + ' KB)', 'success');
} catch(e) {
termPrint('[!] aapt2 install failed: ' + e.message, 'warning');
termPrint('[*] Build will work for resource-less APKs only', 'info');
}
}
termPrint('[*] Creating app_process wrapper scripts...', 'info');
var wrappers = {
ecj: '#!/system/bin/sh\nexec /system/bin/app_process /system/bin --nice-name=zaichat -Djava.class.path=' + jarsDir + '/ecj.jar org.eclipse.jdt.internal.compiler.batch.Main "$@"',
d8: '#!/system/bin/sh\nexec /system/bin/app_process /system/bin --nice-name=zaichat -Djava.class.path=' + jarsDir + '/d8.jar com.android.tools.r8.D8 "$@"',
apksigner: '#!/system/bin/sh\nexec /system/bin/app_process /system/bin --nice-name=zaichat -jar ' + jarsDir + '/apksigner.jar "$@"'
};
for (var name in wrappers) {
try {
await Shell.writeFile({path: binDir + '/' + name, content: wrappers[name]});
await shellExec('chmod 755 "' + binDir + '/' + name + '"', termState.homeDir, false);
} catch(e) {
termPrint('[!] Failed to create ' + name + ' wrapper: ' + e.message, 'err');
}
}
termPrint('[OK] Wrapper scripts created (ecj, d8, apksigner)', 'success');
var verify = await shellExec('test -f "' + jarsDir + '/ecj.jar" && test -f "' + jarsDir + '/d8.jar" && test -f "' + jarsDir + '/apksigner.jar" && test -x "' + binDir + '/ecj" && test -x "' + binDir + '/aapt2"', termState.homeDir, false);
if (verify.exitCode === 0) {
termPrint('[OK] Java build environment ready!', 'success');
showStatusToast('Build tools ready!', 'success');
termState.devToolsInstalled = true;
termState.javaToolsInstalled = true;
return true;
}
termPrint('[OK] Java tools ready (aapt2 optional)', 'success');
termState.devToolsInstalled = true;
termState.javaToolsInstalled = true;
return true;
}
async function tryPkgInstall(prefixUsr) {
var pkgBin = prefixUsr + '/bin/pkg';
var pkgTest = await shellExec('test -f "' + pkgBin + '"', termState.homeDir, false);
if (pkgTest.exitCode !== 0) return false;
var cmd = 'export PREFIX="' + prefixUsr + '" LD_LIBRARY_PATH="' + prefixUsr + '/lib" PATH="' + prefixUsr + '/bin:/system/bin:$PATH" && sh "' + pkgBin + '" update -y 2>&1 && sh "' + pkgBin + '" install -y aapt2 ecj dx apksigner 2>&1';
var result = await shellExec(cmd, termState.homeDir, false);
if (result.output && result.output.length > 200) {
termPrint(result.output.substring(result.output.length - 200), '');
}
return await toolsReady();
}
async function toolsReady() {
var termuxCheck = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1', termState.homeDir, false);
if (termuxCheck.exitCode === 0) {
showStatusToast('Build tools installed!', 'success');
termState.devToolsInstalled = true;
return true;
}
if (termState.toolsDir) {
var javaCheck = await shellExec('test -f "' + termState.toolsDir + '/jars/ecj.jar" && test -f "' + termState.toolsDir + '/jars/d8.jar"', termState.homeDir, false);
if (javaCheck.exitCode === 0) {
termState.devToolsInstalled = true;
termState.javaToolsInstalled = true;
return true;
}
}
return false;
}
async function tryProotExec(prootCmd, prefixUsr, pkgBin, aptBin) {
var hasPkg = await shellExec('test -f "' + pkgBin + '"', termState.homeDir, false);
var pkgCmd = 'sh /usr/bin/pkg update -y 2>&1 && sh /usr/bin/pkg install -y aapt2 ecj dx apksigner 2>&1';
if (hasPkg.exitCode !== 0) pkgCmd = 'sh /usr/bin/apt update -y 2>&1 && sh /usr/bin/apt install -y aapt2 ecj dx apksigner 2>&1';
var wrappedCmd = prootCmd + ' -0 -b /dev -b /proc -b /sys -r ' + prefixUsr + ' /bin/sh -c \'' + pkgCmd.replace(/'/g, "'\\''") + '\'';
termPrint('[*] Running pkg via PRoot...', 'info');
var result = await shellExec(wrappedCmd, termState.homeDir, false);
if (result.output) {
var out = result.output;
if (out.length > 2000) out = out.substring(0, 1000) + '\n... truncated ...\n' + out.substring(out.length - 800);
termPrint(out.replace(/\n$/, ''), '');
}
if (result.exitCode === 0 || result.output.indexOf('Setting up') !== -1) {
if (await toolsReady()) return true;
}
termPrint('[!] PRoot strategy failed (exit ' + result.exitCode + ')', 'err');
return false;
}
async function tryTermuxInstall() {
if (!Bootstrap) return false;
var termuxInfo;
try { termuxInfo = await Bootstrap.isTermuxInstalled(); } catch(e) { return false; }
if (!termuxInfo.installed) {
termPrint('[!] Termux not installed. Opening F-Droid...', 'warning');
try { await Bootstrap.openTermuxPage(); } catch(e) {}
termPrint('[*] After installing Termux:', 'info');
termPrint(' 1. Open Termux app', 'info');
termPrint(' 2. Run: pkg update && pkg install aapt2 ecj dx apksigner', 'info');
termPrint(' 3. Come back to Z.AI Chat', 'info');
return false;
}
termPrint('[OK] Termux detected! Sending install command...', 'success');
try {
await Bootstrap.runInTermux({command: 'pkg update -y && pkg install -y aapt2 ecj dx apksigner'});
termPrint('[*] Command sent to Termux. Waiting...', 'info');
await new Promise(function(r) { setTimeout(r, 15000); });
if (await toolsReady()) return true;
await new Promise(function(r) { setTimeout(r, 30000); });
if (await toolsReady()) return true;
} catch(e) {
termPrint('[!] Termux RUN_COMMAND failed: ' + e.message, 'err');
}
return false;
}
async function checkAutoGLMStatus() {
var AutoGLM = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AutoGLM;
var statusEl = $('#autoglm-status');
var btn = $('#autoglm-enable-btn');
if (!AutoGLM || !statusEl) return;
try {
var result = await AutoGLM.isEnabled();
if (result.enabled) {
statusEl.innerHTML = 'Device control enabled';
if (btn) { btn.textContent = 'Device Control Active'; btn.disabled = true; }
} else {
statusEl.innerHTML = 'Not enabled — tap to open Settings';
}
} catch(e) {
statusEl.innerHTML = 'Checking status...';
}
}
async function checkDevEnvironment() {
if (state.currentMode !== 'coding' && state.currentMode !== 'agentic') return;
if (!Bootstrap) return;
try {
var bsStatus = await Bootstrap.getStatus();
if (!bsStatus.installed) {
showDevToolsBanner('Termux not installed. Tap Dev Setup to install Linux environment + build tools.');
return;
}
} catch(e) {}
if (termState.devToolsInstalled) return;
var termuxCheck = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1', termState.homeDir, false);
if (termuxCheck.exitCode === 0) {
termState.devToolsInstalled = true;
return;
}
if (termState.toolsDir) {
var javaCheck = await shellExec('test -f "' + termState.toolsDir + '/jars/ecj.jar" && test -f "' + termState.toolsDir + '/jars/d8.jar"', termState.homeDir, false);
if (javaCheck.exitCode === 0) {
termState.devToolsInstalled = true;
termState.javaToolsInstalled = true;
return;
}
}
showDevToolsBanner('Build tools not installed. Tap to auto-install via Java virtual environment.');
}
function showDevToolsBanner(msg) {
var existing = $('#dev-tools-banner');
if (existing) existing.remove();
var container = $('#messages');
if (!container) return;
var banner = document.createElement('div');
banner.id = 'dev-tools-banner';
banner.className = 'dev-tools-banner';
banner.innerHTML = '⚠ ' +
'' + msg + '' +
'' +
'';
container.insertBefore(banner, container.firstChild);
banner.querySelector('.dtb-install-btn').addEventListener('click', async function() {
var btn = this;
btn.textContent = 'Installing...';
btn.disabled = true;
var bsStatus;
try { bsStatus = await Bootstrap.getStatus(); } catch(e) { bsStatus = { installed: false }; }
if (!bsStatus.installed) {
try {
await Bootstrap.install();
try { await Bootstrap.fixPermissions(); } catch(e) {}
if (Shell) {
var env = await Shell.getEnv();
termState.homeDir = env.HOME;
termState.toolsDir = env.TOOLS;
termState.projectsDir = env.PROJECTS;
termState.cwd = env.CWD || env.HOME;
}
updateCwdDisplay();
await shellExec('echo shell-ok', termState.homeDir, false);
} catch(e) {
btn.textContent = 'Bootstrap failed: ' + e.message;
return;
}
}
var ok = await ensureBuildTools();
if (ok) {
banner.remove();
showStatusToast('All tools installed!', 'success');
} else {
btn.textContent = 'Retry';
btn.disabled = false;
}
});
banner.querySelector('.dtb-dismiss-btn').addEventListener('click', function() {
banner.remove();
});
}
async function autoExecuteActions(actions, conv) {
var hasFiles = actions.some(function(a) { return a.type === 'create_file'; });
var hasBuild = actions.some(function(a) { return a.type === 'build_apk'; });
var hasInstall = actions.some(function(a) { return a.type === 'install_apk'; });
var hasCommands = actions.some(function(a) { return a.type === 'run_command'; });
var hasDevice = actions.some(function(a) { return a.type && a.type.indexOf('device_') === 0; });
var hasHermes = actions.some(function(a) { return a.type && a.type.indexOf('hermes_') === 0; });
var hasVenv = actions.some(function(a) { return a.type && a.type.indexOf('venv_') === 0; });
var hasSsh = actions.some(function(a) { return a.type && a.type.indexOf('ssh_') === 0; });
var hasRemote = actions.some(function(a) { return a.type === 'remote_exec' || a.type === 'curl_exec'; });
if (!hasFiles && !hasBuild && !hasInstall && !hasCommands && !hasDevice && !hasHermes && !hasVenv && !hasSsh && !hasRemote) return;
_agenticRetryCount = 0;
var resultLog = [];
var fileCount = actions.filter(function(a) { return a.type === 'create_file'; }).length;
if (fileCount > 0) {
showStatusToast('Deploying ' + fileCount + ' file' + (fileCount > 1 ? 's' : '') + '...', 'info');
for (var i = 0; i < actions.length; i++) {
if (actions[i].type === 'create_file') {
var deployOk = await autoDeployFile(actions[i]);
resultLog.push(deployOk);
}
}
showStatusToast(fileCount + ' file' + (fileCount > 1 ? 's' : '') + ' deployed', 'success');
}
if (hasCommands) {
for (var c = 0; c < actions.length; c++) {
if (actions[c].type === 'run_command') {
var cmd = actions[c].command;
showStatusToast('Running: ' + cmd.substring(0, 40), 'info');
var cmdApproved = true;
if (needsApprovalForCommand(cmd)) {
cmdApproved = await askApproval('Run command?', cmd, function() { return true; });
}
if (!cmdApproved) {
termPrint('[DENIED] ' + cmd, 'warning');
resultLog.push('DENIED: ' + cmd.substring(0, 60));
continue;
}
var cmdResult = await shellExec(cmd, termState.cwd, false);
resultLog.push('CMD: ' + cmd.substring(0, 60) + '\nexit: ' + (cmdResult.exitCode !== undefined ? cmdResult.exitCode : '?') + '\n' + (cmdResult.output || '').substring(0, 500));
if (cmdResult.output) {
termPrint('$ ' + cmd, 'cmd');
termPrint(cmdResult.output.replace(/\n$/, ''), cmdResult.exitCode === 0 ? '' : 'err');
}
}
}
}
if (hasSsh) {
for (var s = 0; s < actions.length; s++) {
var sAct = actions[s];
if (sAct.type.indexOf('ssh_') !== 0) continue;
var sshCmd = '';
var sshLabel = '';
if (sAct.type === 'ssh_exec') {
sshCmd = 'ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ' + sAct.host + ' ' + sAct.command;
sshLabel = 'SSH: ' + sAct.host + ' → ' + sAct.command.substring(0, 60);
} else if (sAct.type === 'ssh_upload') {
sshCmd = 'scp -o StrictHostKeyChecking=no -o ConnectTimeout=10 ' + sAct.localPath + ' ' + sAct.remotePath;
sshLabel = 'SCP upload: ' + sAct.localPath + ' → ' + sAct.remotePath;
} else if (sAct.type === 'ssh_download') {
sshCmd = 'scp -o StrictHostKeyChecking=no -o ConnectTimeout=10 ' + sAct.remotePath + ' ' + sAct.localPath;
sshLabel = 'SCP download: ' + sAct.remotePath + ' → ' + sAct.localPath;
}
var sshOk = await askApproval(sshLabel, sshCmd, function() { return true; });
if (!sshOk) {
termPrint('[DENIED] ' + sshLabel, 'warning');
resultLog.push('DENIED: ' + sshLabel);
continue;
}
termPrint('$ ' + sshCmd, 'cmd');
var sshResult = await shellExec(sshCmd, termState.cwd, false);
resultLog.push('SSH: ' + sshLabel + '\nexit: ' + (sshResult.exitCode !== undefined ? sshResult.exitCode : '?') + '\n' + (sshResult.output || '').substring(0, 500));
termPrint((sshResult.output || '').replace(/\n$/, ''), sshResult.exitCode === 0 ? '' : 'err');
if (sshResult.exitCode !== 0 && sshResult.exitCode !== undefined) {
termPrint('[SSH exit: ' + sshResult.exitCode + ']', 'err');
}
}
}
if (hasRemote) {
for (var r = 0; r < actions.length; r++) {
var rAct = actions[r];
if (rAct.type === 'remote_exec') {
var reCmd = 'ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ' + rAct.host + ' ' + rAct.command;
var reOk = await askApproval('Remote exec: ' + rAct.host, reCmd, function() { return true; });
if (!reOk) { termPrint('[DENIED] ' + rAct.host, 'warning'); continue; }
termPrint('$ ' + reCmd, 'cmd');
var reResult = await shellExec(reCmd, termState.cwd, false);
resultLog.push('REMOTE: ' + rAct.host + '\n' + (reResult.output || '').substring(0, 500));
termPrint((reResult.output || '').replace(/\n$/, ''), reResult.exitCode === 0 ? '' : 'err');
} else if (rAct.type === 'curl_exec') {
var curlCmd = 'curl -sL -o - ' + rAct.url;
var curlOk = await askApproval('Fetch URL?', rAct.url, function() { return true; });
if (!curlOk) { termPrint('[DENIED] curl ' + rAct.url, 'warning'); continue; }
termPrint('$ ' + curlCmd, 'cmd');
var curlResult = await shellExec(curlCmd, termState.cwd, false);
resultLog.push('CURL: ' + rAct.url + '\n' + (curlResult.output || '').substring(0, 500));
termPrint((curlResult.output || '').replace(/\n$/, ''), '');
}
}
}
if (hasDevice) {
for (var d = 0; d < actions.length; d++) {
var act = actions[d];
if (act.type.indexOf('device_') !== 0) continue;
try {
var devResult = await executeDeviceAction(act);
resultLog.push(devResult);
termPrint(devResult, '');
} catch(e) {
resultLog.push('DEVICE_ERROR: ' + e.message);
termPrint('[!] Device: ' + e.message, 'err');
}
}
}
if (hasHermes) {
for (var h = 0; h < actions.length; h++) {
var hAct = actions[h];
if (hAct.type.indexOf('hermes_') !== 0) continue;
try {
var hermesResult = await executeHermesAction(hAct);
resultLog.push(hermesResult);
termPrint(hermesResult, '');
} catch(e) {
resultLog.push('HERMES_ERROR: ' + e.message);
termPrint('[!] Hermes: ' + e.message, 'err');
}
}
}
if (hasVenv) {
for (var v = 0; v < actions.length; v++) {
var vAct = actions[v];
if (vAct.type.indexOf('venv_') !== 0) continue;
try {
var venvResult = await executeVirtualEnvAction(vAct);
resultLog.push(venvResult);
termPrint(venvResult, '');
} catch(e) {
resultLog.push('VENV_ERROR: ' + e.message);
termPrint('[!] Venv: ' + e.message, 'err');
}
}
}
if (hasBuild) {
showStatusToast('Building APK...', 'info');
var toolsReady = await ensureBuildTools();
if (!toolsReady) {
var buildFail = '[BUILD FAILED] Build tools not available. Install: pkg install aapt2 ecj dx apksigner';
resultLog.push(buildFail);
await agenticRetryOnError(buildFail, conv);
return;
}
var buildResult = await autoBuildApk(actions);
resultLog.push(buildResult);
if (buildResult.indexOf('[BUILD FAILED]') >= 0) {
await agenticRetryOnError(buildResult, conv);
return;
}
}
if (hasInstall) {
for (var j = 0; j < actions.length; j++) {
if (actions[j].type === 'install_apk') {
showStatusToast('Installing APK...', 'info');
await installApk(actions[j].path);
resultLog.push('INSTALL: ' + actions[j].path);
}
}
}
}
async function executeDeviceAction(action) {
var AutoGLM = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AutoGLM;
if (!AutoGLM) throw new Error('AutoGLM plugin not available');
switch (action.type) {
case 'device_tap':
await AutoGLM.tap({ x: action.x, y: action.y });
return '[DEVICE] Tap(' + action.x + ', ' + action.y + ')';
case 'device_long_press':
await AutoGLM.longPress({ x: action.x, y: action.y });
return '[DEVICE] LongPress(' + action.x + ', ' + action.y + ')';
case 'device_swipe':
await AutoGLM.swipe({ startX: action.startX, startY: action.startY, endX: action.endX, endY: action.endY });
return '[DEVICE] Swipe(' + action.startX + ',' + action.startY + ' -> ' + action.endX + ',' + action.endY + ')';
case 'device_type':
await AutoGLM.typeText({ text: action.text });
return '[DEVICE] Type: "' + action.text + '"';
case 'device_press_back':
await AutoGLM.pressBack();
return '[DEVICE] Press Back';
case 'device_press_home':
await AutoGLM.pressHome();
return '[DEVICE] Press Home';
case 'device_press_recents':
await AutoGLM.pressRecents();
return '[DEVICE] Press Recents';
case 'device_screenshot':
var ssResult = await AutoGLM.takeScreenshot({});
return '[DEVICE] Screenshot: ' + (ssResult.path || 'saved');
case 'device_ui_tree':
var treeResult = await AutoGLM.getUITree({});
var tree = treeResult.tree || '{}';
return '[DEVICE] UI Tree: ' + tree.substring(0, 2000);
case 'device_click_text':
var clickResult = await AutoGLM.clickByText({ text: action.text });
return '[DEVICE] Click "' + action.text + '": ' + (clickResult.ok ? 'OK' : 'not found');
case 'device_click_id':
var idResult = await AutoGLM.clickNode({ viewId: action.viewId });
return '[DEVICE] Click ID "' + action.viewId + '": ' + (idResult.ok ? 'OK' : 'not found');
case 'device_launch':
await AutoGLM.launchApp({ package: action.pkg });
return '[DEVICE] Launch: ' + action.pkg;
case 'device_current_app':
var appResult = await AutoGLM.getCurrentApp({});
return '[DEVICE] Current app: ' + (appResult.package || 'unknown');
default:
return '[DEVICE] Unknown action: ' + action.type;
}
}
async function executeHermesAction(action) {
var Bootstrap = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Bootstrap;
if (!Bootstrap) throw new Error('Bootstrap plugin not available');
switch (action.type) {
case 'hermes_install':
showStatusToast('Installing Hermes agent...', 'info');
var installResult = await Bootstrap.installHermes({});
termState.hermesPath = installResult.path;
termState.hermesVenv = installResult.venv;
return '[HERMES] Installed: ' + installResult.path;
case 'hermes_exec':
if (!termState.hermesVenv) {
try {
var status = await Bootstrap.installHermes({});
termState.hermesPath = status.path;
termState.hermesVenv = status.venv;
} catch(e) {
throw new Error('Hermes not installed: ' + e.message);
}
}
showStatusToast('Hermes: ' + action.command.substring(0, 30), 'info');
var execResult = await Bootstrap.hermesExec({ command: action.command, venv: termState.hermesVenv });
var hermesOutput = execResult.output || '';
if (hermesOutput.length > 3000) hermesOutput = hermesOutput.substring(0, 1500) + '\n...truncated...\n' + hermesOutput.substring(hermesOutput.length - 1000);
return '[HERMES] ' + action.command + '\n' + hermesOutput;
default:
return '[HERMES] Unknown action: ' + action.type;
}
}
async function executeVirtualEnvAction(action) {
var bsPlugin = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Bootstrap;
if (!bsPlugin) throw new Error('Bootstrap plugin not available');
var venvPath = termState.homeDir ? (termState.homeDir.replace(/\/home$/, '') + '/venv/default') : '';
switch (action.type) {
case 'venv_setup': {
showStatusToast('Setting up in-app virtual environment...', 'info');
var setup = await bsPlugin.setupVirtualEnv({ venv: venvPath });
termState.venvPath = setup.venv;
return '[VENV] Ready: ' + setup.venv;
}
case 'venv_pip_install': {
if (!termState.venvPath) {
var init = await bsPlugin.setupVirtualEnv({ venv: venvPath });
termState.venvPath = init.venv;
}
showStatusToast('Installing module(s): ' + action.packages, 'info');
var installed = await bsPlugin.venvPipInstall({ venv: termState.venvPath, packages: action.packages });
return '[VENV] pip install ' + action.packages + '\n' + (installed.output || '').substring(0, 1200);
}
default:
return '[VENV] Unknown action: ' + action.type;
}
}
async function autoDeployFile(action) {
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) {
return '[OK] ' + path + ' (' + action.content.length + ' bytes)';
} else {
return '[FAIL] ' + path;
}
}
async function autoBuildApk(actions) {
var projectDir = termState.projectsDir || (termState.homeDir + '/projects');
for (var i = 0; i < actions.length; i++) {
if (actions[i].type === 'create_file') {
await autoDeployFile(actions[i]);
}
}
var toolsDir = termState.toolsDir || (termState.homeDir + '/tools');
var buildScriptPath = toolsDir + '/build.sh';
await shellMkdirs(toolsDir);
await shellWriteFile(buildScriptPath, BUILD_SCRIPT);
await shellExec('chmod +x ' + buildScriptPath, termState.homeDir, false);
termPrint('\n--- Building APK ---', 'info');
var result = await shellExec('cd ' + projectDir + ' && sh ' + buildScriptPath, termState.homeDir, false);
var output = result.output || '';
termPrint(output.replace(/\n$/, ''), result.exitCode === 0 ? '' : 'err');
if (output.indexOf('[BUILD OK]') >= 0) {
var apkPath = projectDir + '/build/app-signed.apk';
var verifyResult = await shellExec('ls -la ' + apkPath + ' 2>&1', termState.homeDir, false);
if (verifyResult.output && verifyResult.output.indexOf('No such file') < 0) {
var sizeMatch = verifyResult.output.match(/(\d+)\s+/);
var sizeInfo = sizeMatch ? ' (' + Math.round(parseInt(sizeMatch[1]) / 1024) + ' KB)' : '';
var builtName = projectDir.split('/').pop() + '.apk';
var Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer;
var exportedPath = apkPath;
try {
if (Installer) {
var exportResult = await Installer.exportApk({ path: apkPath, name: builtName });
exportedPath = exportResult.path || apkPath;
state.lastBuiltApkPath = exportedPath;
}
} catch(e) {
termPrint('[!] Export to real folder failed: ' + e.message, 'warning');
}
termPrint('\n[VERIFIED] APK built successfully: ' + exportedPath + sizeInfo, 'success');
if (state.autoInstallBuiltApk && Installer) {
try {
await Installer.installApk({ path: exportedPath });
termPrint('[OK] Installer opened for built APK', 'success');
} catch(e) {
termPrint('[!] Auto-install failed: ' + e.message, 'warning');
}
}
return '[BUILD OK] ' + exportedPath;
} else {
termPrint('\n[VERIFY FAILED] Build claimed success but APK not found at ' + apkPath, 'err');
return '[BUILD FAILED] APK file not found after build. Output:\n' + output.substring(0, 1000);
}
} else if (output.indexOf('[BUILD FAILED]') >= 0) {
return '[BUILD FAILED] ' + output;
} else if (result.exitCode !== 0) {
return '[BUILD FAILED] exit=' + result.exitCode + '\n' + output.substring(0, 4000);
}
return output.substring(0, 500);
}
async function agenticRetryOnError(errorOutput, conv) {
_agenticRetryCount++;
if (_agenticRetryCount > getMaxRetries()) {
termPrint('\n[!] Max retries reached (' + getMaxRetries() + '). Fix manually or ask again.', 'err');
showStatusToast('Build failed after ' + getMaxRetries() + ' retries', 'err');
return;
}
termPrint('\n[!] Build failed. Asking AI to fix (attempt ' + _agenticRetryCount + '/' + getMaxRetries() + ')...', 'warning');
showStatusToast('Build failed — AI auto-fixing (attempt ' + _agenticRetryCount + ')...', 'err');
if (!conv || !state.apiKey) return;
var fixMessage = 'The build failed with this error:\n\n```\n' +
errorOutput.substring(0, 4000) +
'\n```\n\nPlease fix the code and rebuild. Write ALL corrected files and use [BUILD_APK] again.';
if (state.keepAwake) setWakeLock(true);
try {
var conv2 = getConversation(conv.id) || conv;
conv2.messages.push({ role: 'user', content: fixMessage });
saveState();
appendMessage('user', fixMessage);
state.isGenerating = true;
state.streamingConvId = conv2.id;
state.streamingContent = '';
updateSendButton();
showThinking();
var systemPrompt = MODE_PROMPTS.agentic;
var apiMessages = [{ role: 'system', content: systemPrompt }];
conv2.messages.forEach(function(m) {
if (m.role === 'user' || (m.role === 'assistant' && !m._streaming)) {
apiMessages.push({ role: m.role, content: m.content });
}
});
var requestBody = {
model: state.model,
messages: apiMessages,
temperature: state.temperature,
max_tokens: state.maxTokens,
stream: state.streaming
};
removeThinking();
var responseDiv = appendMessage('assistant', '');
state.streamingResponseDiv = responseDiv;
if (state.streaming) {
await streamResponseWithRetry(requestBody, responseDiv, conv2);
} else {
var res = await apiRequestWithRetry(requestBody);
updateStreamingMessage(responseDiv, res.choices[0].message.content);
state.streamingContent = res.choices[0].message.content;
conv2.messages.push({ role: 'assistant', content: res.choices[0].message.content });
}
var fixContent = state.streamingContent || '';
if (fixContent) {
conv2.messages.push({ role: 'assistant', content: fixContent });
}
} catch(err) {
termPrint('[!] Auto-fix failed: ' + err.message, 'err');
} finally {
state.isGenerating = false;
state.abortController = null;
state.streamingConvId = null;
state.streamingResponseDiv = null;
updateSendButton();
saveState();
if (state.keepAwake) setWakeLock(false);
var fixActions = parseAiActions(state.streamingContent || '');
if (fixActions.length > 0 && _agenticRetryCount <= getMaxRetries()) {
await autoExecuteActions(fixActions, conv);
}
}
}
async function buildFromActions(actions) {
showScreen('terminal');
termPrint('\n--- Building APK ---', 'info');
var buildResult = await autoBuildApk(actions);
termPrint(buildResult, buildResult.indexOf('[BUILD FAILED]') >= 0 ? 'err' : 'success');
if (state.autoInstallBuiltApk && buildResult.indexOf('[BUILD OK]') >= 0) {
var Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer;
if (Installer && state.lastBuiltApkPath) {
await Installer.installApk({ path: state.lastBuiltApkPath });
}
}
}
// ---- 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 (!Bootstrap) return false;
try {
var status = await Bootstrap.getStatus();
return status.installed === true;
} catch(e) { return false; }
}
async function setupDevTools() {
if (!Bootstrap) {
alert('Bootstrap 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';
progressText.textContent = 'Starting...';
Bootstrap.addListener('bootstrap-progress', function(event) {
if (progressFill) progressFill.style.width = event.percent + '%';
if (progressText) progressText.textContent = event.message;
});
try {
await Bootstrap.install();
progressText.textContent = 'Fixing file permissions...';
try { await Bootstrap.fixPermissions(); } catch(e) {}
if (Shell) {
var env = await Shell.getEnv();
termState.homeDir = env.HOME;
termState.toolsDir = env.TOOLS;
termState.projectsDir = env.PROJECTS;
termState.cwd = env.CWD || env.HOME;
}
updateCwdDisplay();
var shellTest = await shellExec('echo OK', termState.homeDir, false);
if (shellTest.exitCode !== 0) {
statusEl.innerHTML = '